Introduction
By now we are all familiar with delegates in .NET. They are so simple yet so powerful. We use them for events, callbacks, and many other wonderful derivative functions.
However delegates have a nasty little secret. When you subscribe to an event, the delegate backing that event will keep a strong reference to you. What does this mean?
It means that you (the caller) will not be able to be garbage collected since the garbage collection algorithm will be able to find you. Unless you like memory
leaks this is a problem. This article will demonstrate how to avoid this issue
without forcing callers to manually unsubscribe. The aim of this article
is make using weak events extremely simple.
Background
Now I've seen my share of weak event implementations. They more or less do the job including
the WeakEventManager
in WPF. The issue I always encountered is either their setup code or memory usage. My implementation of the weak event pattern will address memory
issues and will be able to match any event delegate signature while making it very easy to incorporate into your projects.
Implementation
The first thing developers should know about delegates is that they are classes just like any other .Net class. They have properties that we can use to create our own weak events.
The issue with the delegate as I mentioned before is that they keep a strong reference to the caller. What we need to do is to create a weak reference to the caller.
This is actually simple. All we have to do is use a WeakReference
object to accomplish this.
RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
if (delegateToMethod.Target != null)
{
WeakReference weak = new WeakReference(delegateToMethod.Target);
subscriptions.Add(weak, mInfo);
}
else
{
staticSubscriptions.Add(mInfo);
}
In the above example 'delegateToMethod
' is our delegate. We can get to the method that it will eventually invoke and most importantly we can get to it's Target, the subscriber.
We then create a weak reference to the target. This allows the target to be garbage collected if it is no longer in scope.
I have also saved a 'pointer' to the method using its handle in a RuntimeMethodHandle
field. The reason for this is that even though I'm creating
a weak reference to the target I am still holding on to a MethodInfo
. The collection of
MethodInfo
objects will grow as subscribers increase.
This in not memory efficient. By using a RuntimeMethodHandler
I am essentially creating a pointer to the
MethodInfo
instead.
Then later only when I need them I'll 'bring them to life' so to speak. The
RuntimeMethodHandle
only has one property of type IntPtr
which uses
a lot less memory than the MethodInfo
object. The following code demonstrates how this works.
public void RaiseEvent(object[] parameters = null)
{
List<weakreference> deadTargets = new List<weakreference>();
foreach (var subcription in subscriptions)
{
object target = subcription.Key.Target;
if (target != null)
{
try
{
MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
}
catch (Exception ex)
{
}
}
else
{
deadTargets.Add(subcription.Key);
}
}
foreach (var deadTarget in deadTargets)
{
subscriptions.Remove(deadTarget);
}
}
This solution is memory efficient. However if you are worried about performance just ask yourself how often events are fired in 99% of the cases.
Now we will also need a mechanism to remove delegate subscriptions as well. The entire functionality can be wrapped in my
SmartDelegate
class.
private class SmartDelegate
{
private readonly Dictionary<<weakreference,> subscriptions = new Dictionary<weakreference,>();
private readonly List<runtimemethodhandle> staticSubscriptions = new List<runtimemethodhandle>();
#region Constructors
public SmartDelegate(Delegate delegateToMethod)
{
RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
if (delegateToMethod.Target != null)
{
WeakReference weak = new WeakReference(delegateToMethod.Target);
subscriptions.Add(weak, mInfo);
}
else
{
staticSubscriptions.Add(mInfo);
}
}
#endregion
#region Public Methods
public void RaiseEvent(object[] parameters = null)
{
List<weakreference> deadTargets = new List<weakreference>();
foreach (var subcription in subscriptions)
{
object target = subcription.Key.Target;
if (target != null)
{
try
{
MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
}
catch (Exception ex)
{
}
}
else
{
deadTargets.Add(subcription.Key);
}
}
foreach (var deadTarget in deadTargets)
{
subscriptions.Remove(deadTarget);
}
}
public bool Remove(Delegate handler)
{
WeakReference removalCandidate = null;
foreach (var subscription in subscriptions)
{
if (subscription.Key.Target != null && subscription.Key.Target == handler.Target)
{
removalCandidate = subscription.Key;
break;
}
}
if (removalCandidate != null)
{
subscriptions.Remove(removalCandidate);
return true;
}
return false;
}
#endregion
}
Don't worry about the type of the Dictionary
or list they are actually:
private readonly Dictionary<WeakReference, RuntimeMethodHandle> subscriptions =
new Dictionary<WeakReference, RuntimeMethodHandle>();
private readonly List<RuntimeMethodHandle> staticSubscriptions = new List<RuntimeMethodHandle>();
For some reason Code Project article editor is not allowing me to put them in.
We now need some type of container for the SmartDelegates that will manage subscribing, unsubscribing and raising events. The
EventHostSubscription
class accomplishes this:
public class EventHostSubscription
{
#region Private Fields
private readonly Dictionary<string,>> subscriptions = new Dictionary<string,>>();
private int flag;
#endregion
#region Public Methods
public void Add(string eventName, Delegate handler)
{
if (handler == null)
{
throw new ArgumentNullException("handler");
}
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (!subscriptions.ContainsKey(eventName))
{
subscriptions.Add(eventName, new List<smartdelegate>());
}
SmartDelegate smartDelegate = new SmartDelegate(handler);
subscriptions[eventName].Add(smartDelegate);
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
public void Remove(string eventName, Delegate handler)
{
if (handler == null)
{
throw new ArgumentNullException("handler");
}
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (subscriptions.ContainsKey(eventName))
{
List<smartdelegate> smartDelegates;
if (subscriptions.TryGetValue(eventName, out smartDelegates))
{
for (int i = 0; i < smartDelegates.Count; i++)
{
SmartDelegate smartDelegate = smartDelegates[i];
smartDelegate.Remove(handler);
}
}
}
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
public void RaiseEvent(string eventName, params object[] parameters)
{
List<smartdelegate> smartDelegates;
while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
try
{
if (subscriptions.TryGetValue(eventName, out smartDelegates))
{
object[] delegateParameters = null;
if (parameters.Length > 0)
{
delegateParameters = parameters;
}
for (int i = 0; i < smartDelegates.Count; i++)
{
SmartDelegate smartDelegate = smartDelegates[i];
smartDelegate.RaiseEvent(delegateParameters);
}
}
}
finally
{
Interlocked.Exchange(ref flag,0);
}
}
}
}
Again the dictionary type declaration is screwed up in the editor. The download will work just fine.
Using the Code
Using the code is simple. Let's say we have a class with an event called
OnChanged
, the code hookup is demonstrated below.
public class WeakEventControl
{
private readonly EventHostSubscription subscriptions = new EventHostSubscription();
public delegate void OnChangedDelegate(object sender, EventArgs args);
public event OnChangedDelegate OnChanged
{
add
{
subscriptions.Add("OnChanged", value);
}
remove
{
subscriptions.Remove("OnChanged", value);
}
}
public void RaiseEvent()
{
subscriptions.RaiseEvent("OnChanged", this, EventArgs.Empty);
}
}
That's all there is to it. Just call the RaiseEvent
method with the name of the event and the arguments as you would normally.
Any feedback or improvement ideas welcomed.
History
CodeProject member Sacher Barber pointed out that between the check if the target of a weak reference is null and its actual usage the target could be garbage collected. I've modified my code to cater for this.
CodeProject member Thomas Olsson pointed out that I should 'lock' the Remove and RaiseEvent methods. I've modified my code to cater for this as well.