Weak Event Handlers

15 Comments March 17, 2011

In a few editions of my book, I showed how to implement weak event handlers which allow an object to be GCd if no other reference is keeping the object alive. If the object is alive, it recieves the event notification and if it isn’t alive, it doesn’t receive the event notification. It has come to my attention that the code I show in my book is incorrect and does not work as advertised. I have corrected that code and the working version is shown below:

public static class WeakEventHandler {
   private class WeakEventHandlerImpl {
      protected readonly WeakReference m_wrTarget; // WeakReference to original delegate's target object
      protected Delegate m_openEventHandler;       // "Open" delegate to invoke original target's delegate method

      public WeakEventHandlerImpl(Delegate d) { m_wrTarget = new WeakReference(d.Target); }
         
      // Match is used to compare a WeakEventHandlerImpl object with an actual delegate.
      // Typically used to remove a WeakEventHandlerImpl from an event collection.
      public Boolean Match(Delegate strongEventHandler) {
         // Returns true if original target & method match the WeakEventHandlerImpl's Target & method
         return (m_wrTarget.Target == strongEventHandler.Target) && (m_openEventHandler.Method == strongEventHandler.Method);
      }
   }

   // "Open" delegate definition to quickly invoke original delegate's callback
   private delegate void OpenEventHandler<TTarget, TEventArgs>(TTarget target, Object sender, TEventArgs eventArgs)
      where TTarget : class
      where TEventArgs : EventArgs;

   // A proxy object that knows how to invoke a callback on an object if it hasn't been GC'd
   private sealed class WeakEventHandlerImpl<TEventHandler> : WeakEventHandlerImpl where TEventHandler : class {
      // Refers to a method that removes a delegate to this proxy object once we know the original target has been GC'd
      private readonly Action<TEventHandler> m_cleanup;

      // This is the delegate passed to m_cleanup that needs to be removed from an event
      private readonly TEventHandler m_proxyHandler;

      public static TEventHandler Create(TEventHandler eh, Action<TEventHandler> cleanup) {
         Contract.Requires(eh != null && cleanup != null);
         // We don't create weak events for static methods since types don't get GC'd
         Delegate d = (Delegate)(Object)eh;  // We know that all event handlers are derived from Delegate
         if (d.Target == null) return eh;

         var weh = new WeakEventHandlerImpl<TEventHandler>(d, cleanup);
         return weh.m_proxyHandler; // Return the delegate to add to the event
      }

      private WeakEventHandlerImpl(Delegate d, Action<TEventHandler> cleanup) : base(d) {
         m_cleanup = cleanup;

         Type targetType = d.Target.GetType();
         Type eventHandlerType = typeof(TEventHandler);
         Type eventArgsType = eventHandlerType.IsGenericType
            ? eventHandlerType.GetGenericArguments()[0]
            : eventHandlerType.GetMethod("Invoke").GetParameters()[1].ParameterType;

         // Create a delegate to the ProxyInvoke method; this delegate is registered with the event
         var miProxy = typeof(WeakEventHandlerImpl<TEventHandler>)
            .GetMethod("ProxyInvoke", BindingFlags.Instance | BindingFlags.NonPublic)
            .MakeGenericMethod(targetType, eventArgsType);
         m_proxyHandler = (TEventHandler)(Object)Delegate.CreateDelegate(eventHandlerType, this, miProxy);

         // Create an "open" delegate to the original delegate's method; ProxyInvoke calls this
         Type openEventHandlerType = typeof(OpenEventHandler<,>).MakeGenericType(d.Target.GetType(), eventArgsType);
         m_openEventHandler = Delegate.CreateDelegate(openEventHandlerType, null, d.Method);
      }

      private void ProxyInvoke<TTarget, TEventArgs>(Object sender, TEventArgs e)
         where TTarget : class
         where TEventArgs : EventArgs {
         // If the original target object still exists, call it; else call m_cleanup to unregister our delegate with the event
         TTarget target = (TTarget)m_wrTarget.Target;
         if (target != null) 
            ((OpenEventHandler<TTarget, TEventArgs>)m_openEventHandler)(target, sender, e);
         else m_cleanup(m_proxyHandler);
      }
   }

   // We offer this overload because it is so common
   public static EventHandler Wrap(EventHandler eh, Action<EventHandler> cleanup) {
      return WeakEventHandlerImpl<EventHandler>.Create(eh, cleanup);
   }
   public static TEventHandler Wrap<TEventHandler>(TEventHandler eh, Action<TEventHandler> cleanup) where TEventHandler : class {
      return WeakEventHandlerImpl<TEventHandler>.Create(eh, cleanup);
   }
   public static EventHandler<TEventArgs> Wrap<TEventArgs>(EventHandler<TEventArgs> eh, Action<EventHandler<TEventArgs>> cleanup) where TEventArgs : EventArgs {
      return WeakEventHandlerImpl<EventHandler<TEventArgs>>.Create(eh, cleanup);
   }
   public static Boolean Match(Delegate weakEventHandler, Delegate strongEventHandler) {
      return ((WeakEventHandlerImpl) weakEventHandler.Target).Match(strongEventHandler);
   }
}

To use it, instead of registerring an event callback like this:
      someButton.Click += o.ClickHandler;

Do this:
      someButton.Click += WeakEventHandler.Wrap(o.ClickHandler, eh => someButton.Click -= eh);


15 Comments

  • Gravatar Image
    Dew Drop &ndash; March 17, 2011 | Alvin Ashcraft&#039;s Morning Dew March 17, 2011 8:11 AM

    PingBack from http://www.alvinashcraft.com/2011/03/17/dew-drop-march-17-2011/

  • Gravatar Image
    Brian March 17, 2011 8:11 AM

    Can has explanation of what was wrong?

  • Gravatar Image
    Jef Claes March 17, 2011 8:53 AM

    Could you use some kind of syntax highlighting next time? :)

  • Gravatar Image
    herzmeister March 17, 2011 11:59 AM

    Posting doesn't seem to work... Let's test again.

  • Gravatar Image
    herzmeister March 17, 2011 12:18 PM

    1) Thanks, I saw similar solutions around in the web, like here: http://diditwith.net/2007/03/23/SolvingTheProblemWithEventsWeakEventHandlers.aspx . Your example is more flexible for sure, as it allows for other EventHandler delegate types too, but do you see any other problems with it (maybe similar to the version in your book)?

    2) Your example also uses an "cleanup" action. I always wonder, people go great lengths wrapping the event handling delegate's Target into a WeakReference, but what about the Target in the "Action{TEventHandler} cleanup"? It will mostly, if not always point to the same listener object, which will be kept as a strong reference as the cleanup delegate will be stored as a field in WeakEventHandlerImpl. Where am I wrong in my understanding?

    3) Unfortunately, all of the WeakEventHandler examples I found, including this one, will throw a nasty "MethodAccessException" in Silverlight if the event handling method is not public. Mostly those methods will and should be protected or private. Do you see any workaround? I gave up on it already actually. :-(

  • Gravatar Image
    gOODiDEA.NET March 17, 2011 9:05 PM

    .NET Time Period Library for .NET .Net Perf - timing profiler for .Net Weak Event Handlers Other Don

  • Gravatar Image
    Jason Haley March 18, 2011 6:18 AM

    Interesting Finds: March 18, 2011

  • Gravatar Image
    Adam Ralph March 18, 2011 7:25 AM

    Thanks for posting the correction.

    Am I right in assuming that this replaces all the code in pages 562 to 564 of "CLR via C#, Third Edition"?

  • Gravatar Image
    Robert Hegner November 23, 2012 10:52 AM

    How can I make sure that a weak event handler is registered only once?

    One possible approach with strong event handlers is to remove it first, like this:
    someButton.Click -= o.ClickHandler;
    someButton.Click += o.ClickHandler;

    Does this approach work with weak event handlers as well? And should it be:
    someButton.Click -= WeakEventHandler.Wrap(o.ClickHandler, eh => someButton.Click -= eh);
    someButton.Click += WeakEventHandler.Wrap(o.ClickHandler, eh => someButton.Click -= eh);

    Or rather:
    someButton.Click -= o.ClickHandler;
    someButton.Click += WeakEventHandler.Wrap(o.ClickHandler, eh => someButton.Click -= eh);

  • Gravatar Image
    nick November 29, 2012 9:39 PM

    .net version 4.0 and above

  • Gravatar Image
    Eric Ouellet January 9, 2013 2:55 PM

    You've been my reference since 1997 (c++)...
    I'm affraid that you have given your code with bug in few editions of your book. You really do mistake ??? (I wonder what was the bug) ?

    Anyway... thanks for sharing this code... and by the way thanks for your excellent book "Advanced Windows".

  • Gravatar Image
    Eric Ouellet January 9, 2013 3:19 PM

    I do not want to blow you balloon but I think you still have a bug.
    My first usage of it crash into my face... grrrrr !
    I wonder if its me or you ? I know what you're thinking right now.
    But it is possible it is you...
    You take into account that my callback will have at least one parameter what is not always the case... no ? Did I missed something ?

    Then this line give IndexOutOfRangeException because it has 0 argument:
    Type eventArgsType = eventHandlerType.IsGenericType ? eventHandlerType.GetGenericArguments()[0] : eventHandlerType.GetMethod("Invoke").GetParameters()[1].ParameterType;

    I'm at gmail.com and my account is EricOuellet2 (if you ever want to contact me)
    Thanks,
    Eric

  • Gravatar Image
    Eric Ouellet January 9, 2013 4:12 PM

    Jeffrey,

    I think that we should read: GetParameters()[0].ParameterType
    instead of : GetParameters()[1].ParameterType

    On the affectation:
    m_proxyHandler = (TEventHandl....
    I receive: ArgumentException (Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.)

    I wonder if I use you code properly or there is new behavior in .net 4.5 that prevent my code to run ???

  • Gravatar Image
    Eric Ouellet January 10, 2013 11:05 AM

    Hi,

    My last one... I just realize that we also need the delegate portion also.
    I thought that it would have been possible to create a weak event handler hooked to a regular event delegate (hidden throught another object where the creation would have required a call to a static method to insert than third object between the source and the handler).

    I wonder why it is not so sample ?

    Eric

  • Gravatar Image
    Jordan Pickwell June 16, 2014 5:06 PM

    I was looking for the WeakEvent code in chapter 21 (fourth edition), but could not find it. I was wondering if this WeakEvent code was removed because .NET 4.5 has a built-in solution that is analogous to what your code does?

Have a Comment?

Archives