WeakEventManager and IWeakEventListener

When my team first starting hitting “memory leaks” due to event handlers we investigated a variety of solutions. There are many examples on the web for implementing some kind of weak listener – but interestingly almost all of them have some flaw. In the end we settled on using WeakEventManager and its associated IWeakEventListener interface.

However this isn’t perfect. We have 3 major complaints with this setup:

· The tedium of implementing a new WeakEventManager

· Implementing the IWeakEventListener interface

· The scalability of WeakEventManager

 

Implementing a WeakEventManager

Implementing a WeakEventManager isn’t hard. In fact it is fairly mechanical. But that doesn’t make it good – especially if you need to implement a dozen of them. Personally I hate implementing this sort of code because:

1. It is boring

2. It is more places where bugs can be introduced

Currently my team copy-and-pastes an existing WeakEventManager and then modifies it.

I’ve thought about how to simplify this by using generics or helper classes or something. Unfortunately none of those ideas have significantly reduced the work enough to justify implementing it.

So I’ve come to the conclusion that code generation is the answer. In order to handle 90+% of the cases all one needs for the metadata is:

· The event source type

· The event name

· The event args type

I hope to explore this in the future and blog about it. As an aside I’d love to see Visual Studio come with this kind of built in support. Imagine being able to right click on an event declaration and select “Generate Weak Event Manager” from a menu and have it generate the code and add it to your project?

Implementing the IWeakEventListener

I believe encapsulation is a good thing, especially for implementation details. Whether a given class uses a WeakEventManager to subscribe to events listens to is an implementation detail. Users of that class shouldn’t care, just so long as the implementation meets the contact and is well behaved (i.e. doesn’t cause memory leaks).

Unfortunately the pattern for using WeakEventManager is to implement IWeakEventListener on the class which needs to do the listening. Look at CollectionViewSource, CompositeCollection and BindingExpressionBase. Due to this choice we know something about their implementation. That doesn’t seem very encapsulated to me. What happens if the class no longer needs to use a WeakEventManager? What happens if I directly call the RecieveWeakEvent method – which is public on those classes?

And don’t get me started on what the implementation of RevieveWeakEvent needs to look like if you subscribe to more than 1 event.

Thankfully we don’t have to rely on this pattern. We can implement a small class which implements IWeakEventListener. This class can either raise its own event, or call a delegate or whatever. We can then contain an instance of this class in whatever class needs to do the listening.

But we don’t want to implement tons of these little listener classes. So generics to the rescue. Using generics are can implement a single listener class that works for all types of events.

And here it is:

   /// <summary>

    /// A common weak event listener which can be used for different kinds of events.

    /// </summary>

    /// <typeparam name="TEventArgs">The EventArgs type for the event.</typeparam>

    class WeakEventListener<TEventArgs> : IWeakEventListener where TEventArgs : EventArgs

    {

        EventHandler<TEventArgs> realHander;

        /// <summary>

        /// Initializes a new instance of the WeakEventListener class.

        /// </summary>

       /// <param name="handler">The handler for the event.</param>

        public WeakEventListener(EventHandler<TEventArgs> handler)

        {

            if (handler == null)

            {

                throw new ArgumentNullException("handler");

            }

            this.realHander = handler;

        }

        /// <summary>

        /// Receives events from the centralized event manager.

        /// </summary>

        /// <param name="managerType">The type of the WeakEventManager calling this method.</param>

        /// <param name="sender">Object that originated the event.</param>

        /// <param name="e">Event data.</param>

        /// <returns>

        /// true if the listener handled the event. It is considered an error by the WeakEventManager handling in WPF to register a listener for an event that the listener does not handle. Regardless, the method should return false if it receives an event that it does not recognize or handle.

        /// </returns>

        bool IWeakEventListener.ReceiveWeakEvent(Type managerType, Object sender, EventArgs e)

        {

            TEventArgs realArgs = (TEventArgs)e;

            this.realHander(sender, realArgs);

            return true;

        }

    }

It is easy to use. For each event you want to listen to you:

1. Add a field of type WeakEventListener<T> to your class

2. Construct the WeakEventListener instance with a line like:

this.collectionChangedListener = new WeakEventListener<NotifyCollectionChangedEventArgs>(this.HandleCollectionChanged);

3. Register the WeakEventListener with a line like:

CollectionChangedEventManager.AddListener(this.sourceCollection, this.collectionChangedListener);

4. And of course implement your handler (HandleCollectionChanged in this example)

We now have a way to use a WeakEventManager in an encapsulated way (and have separate handlers without having to write a switch statement).

Scalability of WeakEventManager

One unfortunate thing we discovered is that as you get thousands of weak event listeners registering/unregistering for events the internal maintenance (called “purge” inside the WeakEventManager code) gets slow. I mean really slow – like the user thinks your app has hung slow.

BTW if you wonder why this is happening on the UI thread it is because WeakEventManager is a subclass of DispatcherObject and therefore has a thread affinity. No doubt the creators thought they were being clever in avoiding synchronization issues.

Currently there is no fix for this other than reverse engineering WeakEventManager and coding a new one with a more performant purge (or at least enable purge to happen on a background thread in a non-blocking way).