Weak event patterns (WPF .NET)

In applications, it's possible that handlers attached to event sources won't be destroyed in coordination with the listener object that attached the handler to the source. This situation can lead to memory leaks. Windows Presentation Foundation (WPF) introduces a design pattern that can be used to address this issue. The design pattern provides a dedicated manager class for particular events, and implements an interface on listeners for that event. This design pattern is known as the weak event pattern.

Prerequisites

The article assumes a basic knowledge of routed events, and that you've read Routed events overview. To follow the examples in this article, it helps if you're familiar with Extensible Application Markup Language (XAML) and know how to write Windows Presentation Foundation (WPF) applications.

Why implement the weak event pattern?

Listening for events can lead to memory leaks. The usual technique for listening to an event is to use language-specific syntax to attach a handler to an event on a source. For example, the C# statement source.SomeEvent += new SomeEventHandler(MyEventHandler) or the VB statement AddHandler source.SomeEvent, AddressOf MyEventHandler. However, this technique creates a strong reference from the event source to the event listener. Unless the event handler is explicitly unregistered, the object lifetime of the listener will be influenced by the object lifetime of the source. In certain circumstances, you might want the object lifetime of the listener to be controlled by other factors, such as whether it currently belongs to the visual tree of the application. Whenever the object lifetime of the source extends beyond the useful object lifetime of the listener, the listener is kept alive longer than necessary. In this case, the unallocated memory amounts to a memory leak.

The weak event pattern is designed to solve the memory leak problem. The weak event pattern can be used when a listener needs to register for an event, but the listener doesn't explicitly know when to unregister. The weak event pattern can also be used when the object lifetime of the source exceeds the useful object lifetime of the listener. In this case, useful is determined by you. The weak event pattern allows the listener to register for and receive the event without affecting the object lifetime characteristics of the listener in any way. In effect, the implied reference from the source doesn't determine whether the listener is eligible for garbage collection. The reference is a weak reference, thus the naming of the weak event pattern and the related APIs. The listener can be garbage collected or otherwise destroyed, and the source can continue without retaining noncollectible handler references to a now destroyed object.

Who should implement the weak event pattern?

The weak event pattern is primarily relevant to control authors. As a control author, you're largely responsible for the behavior and containment of your control and the impact it has on the applications in which it's inserted. This includes the control's object lifetime behavior, in particular the handling of the described memory leak problem.

Certain scenarios inherently lend themselves to the application of the weak event pattern. One such scenario is data binding. In data binding, it's common for the source object to be independent of the listener object, which is a target of a binding. Many aspects of WPF data binding already have the weak event pattern applied in how the events are implemented.

How to Implement the weak event pattern

There are four ways to implement the weak event pattern, and each approach uses a different event manager. Select the event manager that best suits your scenario.

  • Existing weak event manager:

    Use an existing weak event manager class when the event you want to subscribe to has a corresponding WeakEventManager. For a list of weak event managers that are included with WPF, see the inheritance hierarchy in the WeakEventManager class. Because the included weak event managers are limited, you'll probably need to choose one of the other approaches.

  • Generic weak event manager:

    Use a generic WeakEventManager<TEventSource,TEventArgs> when an existing WeakEventManager isn't available and you're looking for the easiest way to implement weak events. However, the generic WeakEventManager<TEventSource,TEventArgs> is less efficient than the existing or custom weak event manager because it uses reflection to discover the event from its name. Also, the code needed to register the event using the generic WeakEventManager<TEventSource,TEventArgs> is more verbose than using an existing or custom WeakEventManager.

  • Custom weak event manager:

    Create a custom WeakEventManager when an existing WeakEventManager isn't available and efficiency is crucial. Although more efficient than a generic WeakEventManager, a custom WeakEventManager requires you to write more upfront code.

  • Third-party weak event manager:

    Use a third-party weak event manager when you need functionality that's not provided by the other approaches. NuGet has some weak event managers. Many WPF frameworks also support the pattern.

The following sections describe how to implement the weak event pattern through use of the different event manager types. For the generic and custom weak event manager examples, the event to subscribe to has the following characteristics.

  • The event name is SomeEvent.
  • The event is raised by the SomeEventSource class.
  • The event handler has type EventHandler<SomeEventArgs>.
  • The event passes a parameter of type SomeEventArgs to the event handlers.

Use an existing weak event manager class

  1. Find an existing weak event manager. For a list of weak event managers included with WPF, see the inheritance hierarchy of the WeakEventManager class.

  2. Use the new weak event manager instead of the normal event hookup.

    For example, if your code uses the following pattern to subscribe to an event:

    source.LostFocus += new RoutedEventHandler(Source_LostFocus);
    
    AddHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    Change it to the following pattern:

    LostFocusEventManager.AddHandler(source, Source_LostFocus);
    
    LostFocusEventManager.AddHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

    Similarly, if your code uses the following pattern to unsubscribe from an event:

    source.LostFocus -= new RoutedEventHandler(Source_LostFocus);
    
    RemoveHandler source.LostFocus, New RoutedEventHandler(AddressOf Source_LostFocus)
    

    Change it to the following pattern:

    LostFocusEventManager.RemoveHandler(source, Source_LostFocus);
    
    LostFocusEventManager.RemoveHandler(
        source, New EventHandler(Of RoutedEventArgs)(AddressOf Source_LostFocus))
    

Use the generic weak event manager class

Use the generic WeakEventManager<TEventSource,TEventArgs> class instead of the normal event hookup.

When you use WeakEventManager<TEventSource,TEventArgs> to register event listeners, you supply the event source and EventArgs type as the type parameters to the class. Call AddHandler as shown in the following code:

WeakEventManager<SomeEventSource, SomeEventArgs>.AddHandler(source, "SomeEvent", Source_SomeEvent);
WeakEventManager(Of SomeEventSource, SomeEventArgs).AddHandler(
    source, "SomeEvent", New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))

Create a custom weak event manager class

  1. Copy the following class template to your project. The following class inherits from the WeakEventManager class:

    class SomeEventWeakEventManager : WeakEventManager
    {
        private SomeEventWeakEventManager()
        {
        }
    
        /// <summary>
        /// Add a handler for the given source's event.
        /// </summary>
        public static void AddHandler(SomeEventSource source,
                                      EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
    
            CurrentManager.ProtectedAddHandler(source, handler);
        }
    
        /// <summary>
        /// Remove a handler for the given source's event.
        /// </summary>
        public static void RemoveHandler(SomeEventSource source,
                                         EventHandler<SomeEventArgs> handler)
        {
            if (source == null)
                throw new ArgumentNullException(nameof(source));
            if (handler == null)
                throw new ArgumentNullException(nameof(handler));
    
            CurrentManager.ProtectedRemoveHandler(source, handler);
        }
    
        /// <summary>
        /// Get the event manager for the current thread.
        /// </summary>
        private static SomeEventWeakEventManager CurrentManager
        {
            get
            {
                Type managerType = typeof(SomeEventWeakEventManager);
                SomeEventWeakEventManager manager =
                    (SomeEventWeakEventManager)GetCurrentManager(managerType);
    
                // at first use, create and register a new manager
                if (manager == null)
                {
                    manager = new SomeEventWeakEventManager();
                    SetCurrentManager(managerType, manager);
                }
    
                return manager;
            }
        }
    
        /// <summary>
        /// Return a new list to hold listeners to the event.
        /// </summary>
        protected override ListenerList NewListenerList()
        {
            return new ListenerList<SomeEventArgs>();
        }
    
        /// <summary>
        /// Listen to the given source for the event.
        /// </summary>
        protected override void StartListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent += new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Stop listening to the given source for the event.
        /// </summary>
        protected override void StopListening(object source)
        {
            SomeEventSource typedSource = (SomeEventSource)source;
            typedSource.SomeEvent -= new EventHandler<SomeEventArgs>(OnSomeEvent);
        }
    
        /// <summary>
        /// Event handler for the SomeEvent event.
        /// </summary>
        void OnSomeEvent(object sender, SomeEventArgs e)
        {
            DeliverEvent(sender, e);
        }
    }
    
    Class SomeEventWeakEventManager
        Inherits WeakEventManager
    
        Private Sub New()
        End Sub
    
        ''' <summary>
        ''' Add a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [AddHandler](source As SomeEventSource,
                                       handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedAddHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Remove a handler for the given source's event.
        ''' </summary>
        Public Shared Sub [RemoveHandler](source As SomeEventSource,
                                          handler As EventHandler(Of SomeEventArgs))
            If source Is Nothing Then Throw New ArgumentNullException(NameOf(source))
            If handler Is Nothing Then Throw New ArgumentNullException(NameOf(handler))
            CurrentManager.ProtectedRemoveHandler(source, handler)
        End Sub
    
        ''' <summary>
        ''' Get the event manager for the current thread.
        ''' </summary>
        Private Shared ReadOnly Property CurrentManager As SomeEventWeakEventManager
            Get
                Dim managerType As Type = GetType(SomeEventWeakEventManager)
                Dim manager As SomeEventWeakEventManager =
                    CType(GetCurrentManager(managerType), SomeEventWeakEventManager)
    
                If manager Is Nothing Then
                    manager = New SomeEventWeakEventManager()
                    SetCurrentManager(managerType, manager)
                End If
    
                Return manager
            End Get
        End Property
    
        ''' <summary>
        ''' Return a new list to hold listeners to the event.
        ''' </summary>
        Protected Overrides Function NewListenerList() As ListenerList
            Return New ListenerList(Of SomeEventArgs)()
        End Function
    
        ''' <summary>
        ''' Listen to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StartListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Stop listening to the given source for the event.
        ''' </summary>
        Protected Overrides Sub StopListening(source As Object)
            Dim typedSource As SomeEventSource = CType(source, SomeEventSource)
            AddHandler typedSource.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf OnSomeEvent)
        End Sub
    
        ''' <summary>
        ''' Event handler for the SomeEvent event.
        ''' </summary>
        Private Sub OnSomeEvent(sender As Object, e As SomeEventArgs)
            DeliverEvent(sender, e)
        End Sub
    End Class
    
  2. Rename SomeEventWeakEventManager, SomeEvent, SomeEventSource, and SomeEventArgs to match your event name.

  3. Set the access modifiers for the weak event manager class to match the accessibility of the event it manages.

  4. Use the new weak event manager instead of the normal event hookup.

    For example, if your code uses the following pattern to subscribe to an event:

    source.SomeEvent += new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    AddHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    Change it to the following pattern:

    SomeEventWeakEventManager.AddHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.AddHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

    Similarly, if your code uses the following pattern to unsubscribe to an event:

    source.SomeEvent -= new EventHandler<SomeEventArgs>(Source_SomeEvent);
    
    RemoveHandler source.SomeEvent, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent)
    

    Change it to the following pattern:

    SomeEventWeakEventManager.RemoveHandler(source, Source_SomeEvent);
    
    SomeEventWeakEventManager.RemoveHandler(
        source, New EventHandler(Of SomeEventArgs)(AddressOf Source_SomeEvent))
    

See also