WeakReference Event Handlers

A good rule of thumb to live by is that long-lived objects should avoid referencing short-lived objects.

The reason for this is that the .NET garbage collector uses a mark and sweep algorithm to detemine if it can delete and reclaim an object. If it determines that a long-lived object should be kept alive (because you are using it, or because it's in a static field somewhere), it also assumes anything it references is being kept alive.

Conversely, going the other way is fine - a short-lived object can reference a long-lived object because the garbage collector will happily delete it if nothing else uses it.

For example:

  1. You shouldn't add items to a static collection, if those items won't be around for a while
  2. You shouldn't subscribe to static events from a short-lived object

The second example often throws people not familiar with how events work in .NET. When you subscribe to an event, the event handler keeps a list of subscribers. When the event is raised, it loops through the subscribers and notifies each one - it's a simple form of the observer pattern.

If you do find yourself needing to write this kind of code, and there isn't a good alternative design, then you generally need to have an unhook option. You might have a way to "remove" the short-lived object from the collection managed by the long-lived object, or you might unsubscribe from an event.

When unsubscribing isn't an option (because you don't trust people to call your Dispose/Unsubscribe method), you can make use of weak event handlers. WPF has its own implementation, but it's too complex for my feeble mind. Here's a simple snippet that I use:

[DebuggerNonUserCode]
public sealed class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
{
    private readonly WeakReference _targetReference;
    private readonly MethodInfo _method;

    public WeakEventHandler(EventHandler<TEventArgs> callback)
    {
        _method = callback.Method;
        _targetReference = new WeakReference(callback.Target, true);
    }

    [DebuggerNonUserCode]
    public void Handler(object sender, TEventArgs e)
    {
        var target = _targetReference.Target;
        if (target != null)
        {
            var callback = (Action<object, TEventArgs>)Delegate.CreateDelegate(typeof(Action<object, TEventArgs>), target, _method, true);
            if (callback != null)
            {
                callback(sender, e);
            }
        }
    }
}

When subscribing to events, instead of writing:

alarm.Beep += Alarm_Beeped;

Just write:

alarm.Beeped += new WeakEventHandler<AlarmEventArgs>(Alarm_Beeped).Handler;

Your subscriber can now be garbage collected without needing to manually unsubscribe (and without having to remember to). Here are some tests:

[TestFixture]
public class WeakEventsTests
{
    #region Example

    public class Alarm
    {
        public event PropertyChangedEventHandler Beeped;

        public void Beep()
        {
            var handler = Beeped;
            if (handler != null) handler(this, new PropertyChangedEventArgs("Beep!"));
        }
    }

    public class Sleepy
    {
        private readonly Alarm _alarm;
        private int _snoozeCount;

        public Sleepy(Alarm alarm)
        {
            _alarm = alarm;
            _alarm.Beeped += new WeakEventHandler<PropertyChangedEventArgs>(Alarm_Beeped).Handler;
        }

        private void Alarm_Beeped(object sender, PropertyChangedEventArgs e)
        {
            _snoozeCount++;
        }

        public int SnoozeCount
        {
            get { return _snoozeCount; }
        }
    }

    #endregion

    [Test]
    public void ShouldHandleEventWhenBothReferencesAreAlive()
    {
        var alarm = new Alarm();
        var sleepy = new Sleepy(alarm);
        alarm.Beep();
        alarm.Beep();

        Assert.AreEqual(2, sleepy.SnoozeCount);
    }

    [Test]
    public void ShouldAllowSubscriberReferenceToBeCollected()
    {
        var alarm = new Alarm();
        var sleepyReference = null as WeakReference;
        new Action(() =>
        {
            // Run this in a delegate to that the local variable gets garbage collected
            var sleepy = new Sleepy(alarm);
            alarm.Beep();
            alarm.Beep();
            Assert.AreEqual(2, sleepy.SnoozeCount);
            sleepyReference = new WeakReference(sleepy);
        })();

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        Assert.IsNull(sleepyReference.Target);
    }

    [Test]
    public void SubscriberShouldNotBeUnsubscribedUntilCollection()
    {
        var alarm = new Alarm();
        var sleepy = new Sleepy(alarm);

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        alarm.Beep();
        alarm.Beep();
        Assert.AreEqual(2, sleepy.SnoozeCount);
    }
}

Got to love passing tests

Observant readers will note that this example does keep a small "sacrifice" object alive in the form of the weak event handler wrapper, but it allows the subscriber to be collected. A more complicated API would allow you to unsubscribe the weak handler when the target is null. In my case, I'll keep the simple API and sacrifice the small object.

A picture of me

Welcome, my name is Paul Stovell. I live in Brisbane and work full time bootstrapping my own product company around Octopus Deploy, an automated deployment tool for .NET applications.

Prior to Octopus Deploy, I worked for an investment bank in London building WPF applications, and before that I worked for Readify, an Australian .NET consulting firm, where I was lucky enough to work with some of the best in the business. I also worked on a number of open source projects and was an active user group presenter. I've been a Microsoft MVP for WPF since 2006.

01 Jul 2010

Nice post, Paul.

I see you're using reflection there. You may want to look into using delegates instead; for the performance gain you get, I think it's worth the trivial amount of added complexity. Check out Jon Skeet's blog post:

http://msmvps.com/blogs/jon_skeet/archive/2008/08/09/making-reflection-fly-and-exploring-delegates.aspx

If you're targeting .NET 3.5 or greater, you could use expressions to clean up the code a little:

static Func<T, object, object> MagicMethod<T>(MethodInfo method)
{
   var parameter = method.GetParameters().Single();
   var instance = Expression.Parameter(typeof (T), "instance");
   var argument = Expression.Parameter(typeof (object), "argument");

   var methodCall = Expression.Call(
       instance,
       method,
       Expression.Convert(argument, parameter.ParameterType)
       );

   return Expression.Lambda<Func<T, object, object>>(
       Expression.Convert(methodCall, typeof (object)),
       instance, argument
       ).Compile();
}

Using an "open delegate", as discussed in Skeet's post, will give you the same functionality as the MethodInfo, only faster.

With that said, I am a bit of a perf addict, so perhaps most sane people would consider that a premature optimization.

Cheers,

-Charles

02 Jul 2010

Charles,

Depending on how frequently the subscribed event is actually raised, I suspect you might incur a much greater performance penalty for the lambda compilation than you would for using reflection to invoke the handler.

My approach to weak event handling was to create a 'generic' weak event manager based on those used by WPF, which would work with any event type. Subscribing to an event looks like this:

GenericWeakEventManager.AddHandler(
    sourceObject,
    "SomeEvent", // could also be an EventDescriptor instance
    new DelegatingWeakEventListener((EventHandler<EventArgs>)this.OnSomeEvent)));

My original implementation of GenericWeakEventManager used runtime-compiled lambda expressions much like yours, but having spent a lot of time in the LINQ/DLR sources (and its lambda compiler), I began to realize just how complex the lambda compilation process is. Since I'm a "perf addict" too, I went back and replaced the lambda expression compilation with a simple call to Delegate.Create, like the one Paul uses. The only significant difference is that my implementation only creates the delegate once per subscription, where his recreates the delegate for each invocation.

Just some food for thought, from one obsessive-compulsive perf nut to another :).

Cheers, Mike

02 Jul 2010

Whoops, my code snippet above is actually wrong--as written, the DelegatingWeakEventHandler would likely get garbage collected prematurely. The subscriber would need to retain a reference to it like so:

// Here, _someEventListener is an instance field:
_someEventListener = new DelegatingWeakEventListener((EventHandler<EventArgs>)this.OnSomeEvent));

GenericWeakEventManager.AddHandler(
    sourceObject,
    "SomeEvent", // could also be an EventDescriptor instance
    _someEventListener);

The field required to hold the weak event listener reference is the price I pay not to use a 'sacrifice' object as Paul does. It's a trade-off either way.

tobi
tobi
02 Jul 2010

You can let the weakreference track the delegate. Then you don't need to recreate it when the event fires.

tobi
tobi
02 Jul 2010

Which would not work because the target does not reference the delegate^^

02 Jul 2010

@Mike Strobel

Very neat. What does your implementation look like?

For those who are interested, Daniel Grunwald wrote an awesome article about Weak Events on CodeProject here:

http://www.codeproject.com/KB/cs/WeakEvents.aspx

Cheers,

-Charles

02 Jul 2010

Hi Charles,

You can find my implementation here. The accompanying implementation of DelegatingWeakEventListener is written in C++/CLI, but here's a C# version for you:

public class DelegatingWeakEventListener : IWeakEventListener
{
    private readonly Delegate _handler;

    public DelegatingWeakEventListener(Delegate handler)
    {
        if (handler == null)
            throw new ArgumentNullException("handler");
        _handler = handler;
    }

    public DelegatingWeakEventListener(EventHandler handler)
    {
        if (handler == null)
            throw new ArgumentNullException("handler");
        _handler = handler;
    }

    #region Implementation of IWeakEventListener
    bool IWeakEventListener.ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
    {
        _handler.DynamicInvoke(sender, e);
        return true;
    }
    #endregion
}

Here's a simple rundown:

  • The DelegatingWeakEventListener holds a reference to the subscriber's handler, but the GenericWeakEventManager holds a weak reference to the DelegatingWeakEventListener. This prevents the listener from keeping the subscriber alive.
  • All records related to the subscriber are purged from the GenericWeakEventManager when the listener has been garbage collected. This purging process is automatically kicked off after garbage collection.
  • You can use the same GenericWeakEventManager to subscribe to any type of event, as long as the signature has exactly two arguments: a 'sender' argument and an EventArgs-derived argument.
  • There are some annotation attributes used in GenericWeakEventManager that you can remove; they simply provide some hints to ReSharper's code analysis engine.

Cheers,
Mike

Dmitry
Dmitry
28 Sep 2010

I'd suggest a small correction to allow static methods to be used within handlers:

public void Handler(object sender, TEventArgs e) { var target = _targetReference.Target; if ((target != null) || _method.IsStatic) { ... } }