
March 26, 2007: Added performance tests, corrected several typos and clarified the performance issues with the lightweight code generation approach.
The Problem (A Recap)
As discussed last time, delegates can produce memory leaks in our
applications if not used carefully. Most often, this happens with events when we add
a handler to an event, forget to remove
it and the object on which the event is declared lives longer than the object
that handles it. The following diagram illustrates the problem.

In the diagram above, there is an object ("eventExposer") that
declares an
event ("SpecialEvent"). Then, a form is created ("myForm") that adds a handler
to the event. The form is closed and the expectation is that the
form will be released to garbage collection but it isn't. Unfortunately, the
underlying delegate of the event still maintains a strong reference to the form
because the form's handler wasn't removed.
The question is, how do we fix this problem?
Most of you are probably saying "Fix the form you idiot! Remove the handler!"
That might seem reasonable—especially if we have
control over all of the code. But what if we don't? What if this is a large plug-in based
architecture where the event is surfaced by some event service that exists for
the lifetime of the application and the form is in a third-party plug-in for
which we don't have the code? In this scenario, we would have a leaky
application that couldn't be corrected by simply modifying the form to
properly remove the handler on close.
Some Solutions
As I mentioned in my
before, the
ideal solution would be a CLR-level mechanism for creating "weak delegates"
which hold weak references to their target objects instead of strong references.
Unfortunately, such a mechanism doesn't yet exist (and won't for the
foreseeable future) so we have to roll our own.
Greg Schechter presented a
well-known
solution a couple of years ago that is used in Avalon (err... WPF). This
solution works perfectly except that it isn't all that convenient to use. It requires the
declaration of a new class that has intimate knowledge of both the container and containee (or
subject and observer). Schechter's essay inspired several developers in the
community to come up with architectures that make this approach easier to use and a couple of articles were submitted to code project. Notably, "Observable
property pattern, memory leaks, and weak delegates for .NET" and "An
Easy to Use Weak Referenced Event Handler Factory for .NET 2.0".
Unfortunately, neither of these solutions are ideal. First, both of them have several support classes. In addition, the first is
extraordinarily heavy because it uses Refelection.Emit to generate types and
assemblies on the fly (more on this type of thing later) and the second one
requires extra client code and uses Reflection to add add and remove handlers
from an event. I should mention however that both solutions do work.
Over the past few years, the community has offered several "magic class" approaches (where a special
class is instantiated that wraps your event handler). A "magic class" is
an attractive solution because it results in far less client code. Unfortunately, the
majority of these offerrings got it wrong. Most of them make the mistake of creating a weak
reference to the event handler itself rather than the target of the event
handler. Shawn A. Van Ness eventually
removed his solution because of problems. Ian Griffiths
kept
his available with a note that it doesn't actually work. Even Joe Duffy (of whom I'm
a big fan) presented a "magic class"-type solution that
doesn't really do the job. A very interesting approach that does work is the one
presented by
Xavier Musy. Xavier chose to essentially re-implement System.MulticastDelegate
but it suffers from performance issues and is not safe multi-threaded
scenarios. Finally, Gregor R. Peisker presented a real working (and performant)
solution just a few months ago that
takes advantage of a technique that is crucial to getting this right.
Obstacles
I have to admit, there are several obstacles to creating a working solution. First of all, you can't
simply inherit from System.Delegate or System.MulticastDelegate and override the
appropriate methods. That would be great but it simply isn't possible. You can't
even do it IL (believe me—I've tried). In addition,
there isn't a generic constraint for delegates. We can work around that but it
certainly makes some code that we might write less elegant.
However, the biggest obstacle is performance. There are two areas of performance that
we can consider: 1) adding/removing a handler and 2) actually invoking the
handler. On the average, invocation occurs far more often than adding and
removing handlers so we'll focus our attention on that. There are a lot of ways
to dynamically invoke a delegate and some of them are very slow when
compared with a normal typed delegate invocation. Using a weak delegate or event
handler is expected to have some overhead but we should keep it to a minimum.
Another potentially serious issue is removing the handler. Let me explain
what I mean. When we use a "magic class" to wrap an event handler and
add it to an event, we create a weak reference to the event handler's target but
the event still maintains a strong reference to our wrapper object. So, when the target
object is garbage collected, the wrapper object is kept alive for the same
reason that the target wasn't being garbage collected before. Granted, it
will likely take far less memory than some large object (e.g. the form in the diagram
that I mentioned earlier) but it's still a leak. That might be acceptable to
some of you but there's actually a very simple solution to this so we should
deal with it properly.
The last obstacle that I want to mention is how to create a class that will
handle any delegate type. I'm going to present a solution that work with any delegate but
we can get much higher-performance if we can use a specific delegate type.
Because of that, I'll focus most of my attention on that only work with the handy
generic
System.EventHandler<TEventArgs> delegate from the .NET 2.0 framework.
A First Stab
OK, let's dig into some code and take a shot at creating a "magic class" so that I
can introduce the players. Here's a very a naive implementation:
using System;
using System.Reflection;
public class
WeakDelegate
{
private WeakReference
m_TargetRef;
private MethodInfo
m_Method;
public WeakDelegate(Delegate
del)
{
m_TargetRef = new
WeakReference(del.Target);
m_Method = del.Method;
}
public object
Invoke(params object[] args)
{
object target = m_TargetRef.Target;
if
(target != null)
return m_Method.Invoke(target, args);
}
}
This approach would actually work in very simple delegate scenarios but it
isn't robust enough to be used with an event. However, it serves to demonstrate
how we can use the
System.WeakReference clsss to create a weak reference to the delegate's
target. To use, instantiate a new WeakReference and pass the object
that you want to track to its constructor. Then, you can check the
WeakReference.IsAlive or WeakReference.Target properties to see if the object
has been garbage collected. Keep in mind that you should never write
code like this:
if (m_TargetRef.IsAlive)
return m_Method.Invoke(m_TargetRef.Target, args);
I've seen way too many developers (including myself many moons ago) make this mistake. The
problem is that the garbage collector runs on a different thread. This causes a
race condition to occur between the calls to m_TargetRef.IsAlive and
m_TargetRef.Target. In other words, m_TargetRef.IsAlive could return true but the
garbage collector could kick in and reclaim the target instance before
m_TargetRef.Target is called, potentially causing a NullReferenceException to be
thrown. The proper way to use this class is to store the
value of m_TargetRef.Target in a local variable. If m_TargetRef.Target doesn't
return null, the local variable creates a strong reference to the target
instance that can safely be used.
So, using the System.WeakReference is fairly simple but it has one
non-obvious problem that I'd like to highlight: it is finalizable but does not
implement IDisposable. What does that mean and why is it bad? It means
that any System.WeakReference instance will add a small amount of pressure to
the garbage collector because its finalizer method must be called before it can be
reclaimed. Internally, WeakReference uses a System.GCHandle to track the target
object and it declares a finalizer to clean up that GCHandle. If it implemented
IDisposable properly, we could call the Dispose method when finished with the
WeakReference. That would release the GCHandle and call GC.SuppressFinalize(this)
to keep the finalizer from being called (thus removing the GC pressure). (In
CLR via C#, Jeffrey Richter refers to this lack of an IDisposable
implementation as a bug.) Now, we could use System.GCHandle ourselves
instead of System.WeakReference but there is a problem with that. The public
methods on GCHandle require a link demand for the unmanaged code security
permission. That means that our WeakDelegate class would need the same
permission. WeakReference actually gets around those security permissions by
using internal methods on GCHandle that don't declare them (it does have an
inheritance demand though—beware inheritors!). Because it requires less security checks, using WeakReference is actually a
little bit faster. Sigh...
There is one other important player from our initial attempt that I'd like to point out. The delegate is invoked by calling MethodInfo.Invoke() and passing the
target instance and the arguments. This is really slow. Directly invoking the
delegate would be much faster but we can't store a reference to the original delegate
because it strongly references the target. So, we
have to rely upon late-bound mechanisms that are much slower. This is the
main reason that we will shift our focus from wrapping System.Delegate to
System.EventHandler<TEventArgs> shortly.
The robustness of this solution can be improved by making WeakDelegate multi-casting.
Currently, it only supports single delegates which is why it is insufficient for
using with events. To do this, we would need to use a similar approach to the
one presented by
Xavier Musy. The idea is that we add Combine and Remove static methods that
are equivalent to those that are found on
System.Delegate. With these in place, we could use this class to declare a
weak event like this:
public class
EventProvider
{
private WeakDelegate
m_WeakEvent;
protected virtual
void OnWeakEvent(EventArgs
e)
{
if (m_WeakEvent !=
null)
m_WeakEvent.Invoke(this,
e);
}
public event
EventHandler WeakEvent
{
add
{
m_WeakEvent = WeakDelegate.Combine(m_WeakEvent,
value);
}
remove
{
m_WeakEvent = WeakDelegate.Remove(m_WeakEvent,
value);
}
}
}
The main issue here is that all handlers are weak—whether they need to be or not. It would be
more flexible to have a weak delegate class that can be used like an ordinary delegate. That way,
a subscriber could make specific handlers weak and other handlers not. The ideal syntax for a
subscriber might look something like this:
public class
EventProvider
{
public event
EventHandler MyEvent;
}
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += new
WeakDelegate(MyWeakEventHandler);
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
The Second Attempt
At this point, we're in a bit of a pickle if we want to continue trying to
provide a solution that wraps any System.Delegate. In fact, to go down this
road, we have use the System.Reflection.Emit classes to generate a new assembly,
a new module, a new type, a WeakReference field, a new method that matches the
signature of the delegate and all of the IL necessary to access the WeakReference.Target
property, load the parameters onto the stack and call the method referenced by
the delegate. Now, this is certainly possible to do (yes, I did it and it made
me weep like a child) but the
result is a mechanism that is so heavy that it's almost worthless. Plus, the
assembly, module and type are not reclaimable by garbage collection unless you
actually create a new AppDomain to contain the code so it can be
unloaded later. However, using another AppDomain will cause invocations of the
delegate to cross AppDomain boundaries (ahem... remoting) and it is fraught with
peril because you need to work out some serialization mechanism for the delegate
and its target. Are you beginning to realize why I'm not showing any code here?
Obviously, this approach kills both performance and memory use. And, since we're trying to
create something that's both fast and lightweight, this is simply not acceptable.
OK, so I'm going to abandon the idea of creating a magic class that works
with any delegate type and focus on creating a solution for a specific delegate type:
System.EventHandler<TEventArgs>. That will limit the solution to .NET 2.0-only but
that's ok. The techniques that we'll employ are 2.0 anyway. Microsoft actually
recommends
that you use EventHandler<TEventArgs> delegate for declaring events if you are
targeting .NET 2.0+ anyway. (Microsoft even managed to follow their own
recommendation and used this delegate type throughout WCF
and WF. They seemingly forgot about WPF though. Sigh...)
With a specific signature, it is much easier to create a "magic"
WeakEventhandler class that achieves the syntax that we're looking for.
using System;
using System.Reflection;
public class
WeakEventHandler<E>
where E: EventArgs
{
private WeakReference
m_TargetRef;
private MethodInfo
m_Method;
private EventHandler<E>
m_Handler;
public WeakEventHandler(EventHandler<E>
eventHandler)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_Method = eventHandler.Method;
m_Handler = Invoke;
}
public void
Invoke(object sender, E e)
{
object target =
m_TargetRef.Target;
if (target !=
null)
m_Method.Invoke(target, new
object[] { sender, e });
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<E>
weh)
{
return weh.m_Handler;
}
}
This version is starting to look a little more like what we want. Being tied to a specific delegate type, we know what the method signature will
be and we can create our own method that matches to use as a sort of "delegate proxy".
And, thanks to the implicit conversion we can write the golden syntax that we're looking
for.
public class
EventProvider
{
public event
EventHandler<EventArgs>
MyEvent;
}
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += new
WeakEventHandler<EventArgs>(MyWeakEventHandler);
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
I ran a simple performance test that adds and fires 100 event handlers in
the standard way and with the WeakEventHandler class. Here are the results:
Added 100 normal listeners to notifier: 0.000289 seconds.
Added 100 weak listeners to notifier: 0.002701 seconds.
Fired 100 normal listeners: 0.000263 seconds.
Fired 100 weak listeners: 0.001531 seconds.
Obviously, this approach is pretty slow. Invocation through
MethodInfo.Invoke() is nearly 6 times slower than normal invocation.
This leaves us with two problems left to solve:
- Invocation performance. We're still using MethodInfo.Invoke() and
it's slow.
- Removal of the weak event handler from the event after the target has
been
garbage collected.
The second problem is actually very simple so let's look at the performance
issue first.
Tweaking Performance
Using MethodInfo.Invoke() is really slow. What we'd really like is some way
to generate a delegate that does not actually store a reference to its target
but allows us to specify the target when we call it. That way, our invocation
performance characteristic is very similar to that of a standard delegate
invocation. In .NET 1.1 there really wasn't a way to create such a delegate but
there are two options available to us (that I am aware of) in .NET 2.0.
The first option that I want to explore is
lightweight code generation (LCG). Lightweight code generation is a
mechanism in the .NET 2.0 framework that allows methods to be generated on the
fly without the requirement of an assembly, module, type, etc. These "dynamic
methods" can be associated with a specific module (making them effectively global) or
a specific type (making them static). Also, unlike the Reflection.Emit story
in 1.0/1.1, they have the benefit of being reclaimable by the garbage collector. In
fact, the only real downside of LCG is that you have to spit out the IL of the
dynamic method. There is a small hit when creating a dynamic method but, as
discussed earlier, the invocation performance is far more important.
using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
public class
WeakEventHandler<E>
where E: EventArgs
{
private delegate
void EventHandlerThunk(object
@this, object sender, E e);
private static
int g_NextThunkID = 1;
private WeakReference
m_TargetRef;
private
EventHandlerThunk m_Thunk;
private EventHandler<E>
m_Handler;
public WeakEventHandler(EventHandler<E>
eventHandler)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_Thunk = CreateDynamicThunk(eventHandler);
m_Handler = Invoke;
}
private
EventHandlerThunk CreateDynamicThunk(EventHandler<E>
eventHandler)
{
MethodInfo method =
eventHandler.Method;
Type declaringType =
method.DeclaringType;
int id =
Interlocked.Increment(ref g_NextThunkID);
DynamicMethod dm =
new DynamicMethod("EventHandlerThunk"
+ id, typeof(void),
new
Type[] { typeof(object),
typeof(object),
typeof(E) }, declaringType);
ILGenerator il =
dm.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Castclass,
declaringType);
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg_2);
il.Emit(method.IsVirtual ? OpCodes.Callvirt
: OpCodes.Call, method);
il.Emit(OpCodes.Ret);
return (EventHandlerThunk)dm.CreateDelegate(typeof(EventHandlerThunk));
}
public void
Invoke(object sender, E e)
{
object target =
m_TargetRef.Target;
if (target !=
null)
m_Thunk(target, sender, e);
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<E>
weh)
{
return
weh.m_Handler;
}
}
Clearly, this code is far more complicated than when it used
MethodInfo.Invoke(). First, there is a new delegate declared ("EventHandlerThunk") which has the same signature as the EventHandler that we're
wrapping with an additional parameter to take the target. We create an
EventHandlerThunk as a DynamicMethod and when invoking it, we pass the target
along with the parameters (see the Invoke method above). Most of the magic code
is in the CreateDynamicThunk method. I don't want to get too detailed about the
IL that is generated but it essentially calls the method referenced by "eventHandler"
directly instead of through the MethodInfo.Invoke(). Very snazzy.
Using LCG, the invocation performance is not great. I ran the same test that
I ran earlier and got these results:
Added 100 normal listeners to notifier: 0.000292 seconds.
Added 100 weak listeners to notifier: 0.010599 seconds.
Fired 100 normal listeners: 0.000272 seconds.
Fired 100 weak listeners: 0.01572 seconds.
Whoa! It turns out that that invoking the LCG solution is almost 58 times
slower than normal invocation! Obviously, this is a step backward and not really
what we're looking for. Part of this poor performance is caused by the fact that
we're actually creating three method calls here: WeakEventHandler.Invoke() calls
the dynamic EventHandlerThunk which calls the real method. The really problem,
however, is that the "lightweight" in "lightweight code generation" really
refers to memory and not performance. So, this isn't an acceptable solution for
is.
The best option that is available to us was mentioned by Joe Duffy in a
comment on another blog. Instead of creating a DynamicMethod and generating
IL, we can use an
open instance or unbound delegate.
Now, some of you might be scratching your heads right now and saying, "what's
an open instance delegate and why haven't I heard of them before?"
An open
instance delegate is a delegate that references an instance method but doesn't
reference a target. Instead the target is to be specified when the delegate is
called. To allow for this, the delegate type of the open instance delegate must declare
an additional parameter that takes the instance that the delegate is called on
when invoked (just like our EventHandlerThunk delegate type).
That explains what they are but why haven't you heard of them?
Well, there's no language support for them in C# and VB. In order to create one,
you have to use an overload of the Delegate.CreateDelegate() method. Open
instance delegates were designed to support STL.NET and C++/CLI (where they're called
unbound
delegates). Additionally, nobody has really gotten excited about them except
for Gregor R. Peisker. His working
solution that I mentioned earlier uses an open instance delegate.
Here is the code reworked to use an open instance delegate:
using System;
public class
WeakEventHandler<E>
where E: EventArgs
{
private delegate
void OpenEventHandler(object
@this, object sender, E e);
private WeakReference
m_TargetRef;
private
OpenEventHandler m_OpenHandler;
private EventHandler<E>
m_Handler;
public WeakEventHandler(EventHandler<E>
eventHandler)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
}
public void
Invoke(object sender, E e)
{
object target =
m_TargetRef.Target;
if (target !=
null)
m_OpenHandler(target, sender, e);
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<E>
weh)
{
return
weh.m_Handler;
}
}
Oops! That compiles but it doesn't actually work. When a WeakEventHandler is
instantiated, the call to Delegate.CreateDelegate fails with a
System.ArgumentException and the (unhelpful) message: "Error binding to the target
method." What the heck is going on? Well, if you look at the documentation for
unbound delegates in C++ it states that "the first parameter of the delegate
signature is the type of this for the object you want to call." In
other words, using "object" as the type of the first parameter won't work. It
has to match. To correct this problem, we have to add another generic type parameter to
our WeakEventHandler class to represent the type of the instance on which the
handler is declared. Here's the new version:
using System;
public class
WeakEventHandler<T, E>
where T: class
where E: EventArgs
{
private delegate
void OpenEventHandler(T
@this, object sender, E e);
private WeakReference
m_TargetRef;
private
OpenEventHandler m_OpenHandler;
private EventHandler<E>
m_Handler;
public WeakEventHandler(EventHandler<E>
eventHandler)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
}
public void
Invoke(object sender, E e)
{
T target =
(T)m_TargetRef.Target;
if (target !=
null)
m_OpenHandler(target, sender, e);
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<T, E>
weh)
{
return
weh.m_Handler;
}
}
This works perfectly but now we've broken our client code syntax. The second generic type parameter has to be specified like this:
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += new
WeakEventHandler<EventSubscriber,
EventArgs>(MyWeakEventHandler);
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
I really dislike the additional generic type parameter because it requires cerebral
activity on the part of the user to determine what type needs to be filled in. But,
we've corrected the problem that we set out to fix: invocation performance.
There is a performance bump when the open instance delegate is created but
that's acceptable because the invocation performance is very good.
Automatic Unregistration
Ideally, our WeakEventHandler would be automatically removed from the event
on which it is registered when its target is reclaimed by the garbage collector.
Unfortunately, that's not possible. There simply isn't any sort of notification
when a garbage collection occurs (unless you're using the CLR hosting, debugging
or profiling APIs). Because of this, we have to enlist an idea from
Joe Duffy and do the next best thing: we pass a delegate to the
WeakEventHandler's constructor that will be called during invocation if the
target has been garbage collected. Here's the code:
using System;
public class
WeakEventHandler<T, E>
where T: class
where E: EventArgs
{
public delegate
void UnregisterCallback(EventHandler<E>
eventHandler);
private delegate
void OpenEventHandler(T
@this, object sender, E e);
private WeakReference
m_TargetRef;
private
OpenEventHandler m_OpenHandler;
private EventHandler<E>
m_Handler;
private
UnregisterCallback m_Unregister;
public WeakEventHandler(EventHandler<E>
eventHandler, UnregisterCallback unregister)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null, eventHandler.Method);
m_Handler = Invoke;
m_Unregister = unregister;
}
public void
Invoke(object sender, E e)
{
T target =
(T)m_TargetRef.Target;
if (target !=
null)
m_OpenHandler(target, sender, e);
else if
(m_Unregister != null)
{
m_Unregister(m_Handler);
m_Unregister = null;
}
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<T, E>
weh)
{
return
weh.m_Handler;
}
}
If we pass an UnregisterCallback into the constructor of WeakEventHandler, it
will be called once if the WeakEventHandler is invoked after the target has been
garbage collected. That neatly solves the problem. Of course, our
WeakEventHandler leaks if the event never fires again but I think that's
acceptable. Here is what the client code looks like now:
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += new
WeakEventHandler<EventSubscriber,
EventArgs>(MyWeakEventHandler,
delegate(EventHandler<EventArgs>
eh)
{
provider.MyEvent -= eh;
});
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
Now that we have a working "magic class" solution, let's take a look at how
we might improve the syntax.
Making It Pretty
The most important thing is to get rid of the additional generic type
parameter that we were forced to add. It just requires too much thinking to
assign it properly. The use of an
anonymous method might confuse the code for some but it's easy enough to move
that into a separate method.
To remove the generic type parameter from the client code, we'll have to
resort to reflection to
construct the WeakEventHandler<T, E> type (because we have to fill that
type parameter in somehow). Gregor R.
Peisker's solution employs this trick so we can use it as a model. Here's my
version:
public delegate
void UnregisterCallback<E>(EventHandler<E>
eventHandler)
where E: EventArgs;
public interface
IWeakEventHandler<E>
where E: EventArgs
{
EventHandler<E> Handler {
get; }
}
public class
WeakEventHandler<T, E>:
IWeakEventHandler<E>
where T: class
where E: EventArgs
{
private delegate
void OpenEventHandler(T
@this, object sender, E e);
private WeakReference
m_TargetRef;
private
OpenEventHandler m_OpenHandler;
private EventHandler<E>
m_Handler;
private
UnregisterCallback<E> m_Unregister;
public WeakEventHandler(EventHandler<E>
eventHandler, UnregisterCallback<E>
unregister)
{
m_TargetRef = new
WeakReference(eventHandler.Target);
m_OpenHandler = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler),
null,
eventHandler.Method);
m_Handler = Invoke;
m_Unregister = unregister;
}
public void
Invoke(object sender, E e)
{
T target = (T)m_TargetRef.Target;
if (target !=
null)
m_OpenHandler.Invoke(target, sender, e);
else if
(m_Unregister != null)
{
m_Unregister(m_Handler);
m_Unregister = null;
}
}
public EventHandler<E>
Handler
{
get {
return m_Handler; }
}
public static
implicit operator
EventHandler<E>(WeakEventHandler<T,
E> weh)
{
return weh.m_Handler;
}
}
public static
class EventHandlerUtils
{
public static
EventHandler<E> MakeWeak<E>(EventHandler<E>
eventHandler, UnregisterCallback<E>
unregister)
where E:
EventArgs
{
if (eventHandler ==
null)
throw
new
ArgumentNullException("eventHandler");
if (eventHandler.Method.IsStatic
|| eventHandler.Target == null)
throw
new ArgumentException("Only
instance methods are supported.", "eventHandler");
Type wehType =
typeof(WeakEventHandler<,>).MakeGenericType(eventHandler.Method.DeclaringType,
typeof(E));
ConstructorInfo wehConstructor
= wehType.GetConstructor(new
Type[] { typeof(EventHandler<E>),
typeof(UnregisterCallback<E>)
});
IWeakEventHandler<E> weh = (IWeakEventHandler<E>)wehConstructor.Invoke(
new
object[] { eventHandler, unregister });
return weh.Handler;
}
}
That looks like a lot more code but it's not too bad. Our magic class has
remained relatively unchanged except that the UnregisterCallback delegate type
is no longer nested inside and it has a Handler property to implement the new IWeakEventHandler<E>
interface. The meat is in EventHandlerUtils.MakeWeak<E>(). This generic method
performs the reflection magic necessary to instantiate WeakEventHandler<T, E>
with its generic type parameters dynamically filled in. This is where the
IWeakEventHandler<E> interface becomes handy. Without it, we would have to
use reflection to access the m_Handler field.
At this point, I expect that you're wondering what the performance
characteristics of this approach are. Well, using the same performance test with
the EventHandlers.MakeWeak<E>() method, I got the following results:
Added 100 normal listeners to notifier: 0.000298 seconds.
Added 100 weak listeners to notifier: 0.011509 seconds.
Fired 100 normal listeners: 0.000288 seconds.
Fired 100 weak listeners: 0.000745 seconds.
This is a much better performance story and closer to what we're looking for.
This test demonstrates that using an open instance delegate in the
WeakEventHandler class results in performance that is only about 2.5 times
slower than standard delegate invocation. That is actually about what we should
expect since a WeakEventHandler causes two delegate invocations.
There is room for improvement in EventHandlerUtils.MakeWeak(). A final
version could cache the ConstructorInfo (probably using the .NET 2.0 reflection
token APIs) and use the values of the two generic type parameters as a composite key.
Also, a dynamic method might be employed (and cached) so that ConstructorInfo.Invoke() doesn't have to be called. So, there are several tweaks
available.
We have improved the client code syntax a bit. It's easier to use now:
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += EventHandlerUtils.MakeWeak<EventArgs>(MyWeakEventHandler,
delegate(EventHandler<EventArgs>
eh)
{
provider.MyEvent -= eh;
});
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
The biggest syntax improvement occurs with C# 3.0. When calling
EventHandlerUtils.MakeWeak in C# 3, we can use a lambda expression instead of an
anonymous method like this:
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += EventHandlerUtils.MakeWeak<EventArgs>(MyWeakEventHandler,
eh => provider.MyEvent -= eh);
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
Even better, the EventHandlerUtils.MakeWeak method can be turned into an
extension method for EventHandler<TEventArgs>. Then we can call it like this:
public class
EventSubscriber
{
public EventSubscriber(EventProvider
provider)
{
provider.MyEvent += new
EventHandler<EventArgs>(MyWeakEventHandler).MakeWeak(eh
=> provider.MyEvent -= eh);
}
private void
MyWeakEventHandler(object sender,
EventArgs e)
{
}
}
Now that's syntax that I can really get behind. If you're wondering how
you might use this from within the class that declares an event to make all
subscribed event handlers weak, here's one possibility:
public class
EventProvider
{
private EventHandler<EventArgs>
m_MyEvent;
public event
EventHandler<EventArgs>
MyEvent
{
add
{
m_Event += value.MakeWeak(eh => m_Event -= eh);
}
remove
{
}
}
}
Have fun!
