Mustafa Can Yücel
blog-post-19

Custom Weak Event Pattern Implementation in .NET

Event Subscriptions and Garbage Collection

In applications that use garbage collected languages, it is possible that handllers (subscribers) attached to event sources will not be destroyed in coordination with the listener object that attached the handler to the source. This can lead to memory leaks, as the event source will keep a reference to the listener object, which in turn will keep a reference to the handler. This is a common problem in Windows Presentation Foundation (WPF) and MAUI applications, where the event source is a static object, and the listener object is a UI element that is created and destroyed multiple times during the application's lifetime. WPF introduces a WeakEventManager class to solbe this issue. However, this is a platform-dependent solution, and it is not available in MAUI. Moreover, using it in platform-agnostic libraries is incorrect, if ever possible, as it violates the platform independence principle.

Weak Event Pattern

Weak Event Patterns are design patterns developed to prevent memory leaks that can occur when event handlers keep objects alive longer than necessary. This usually happens when an object subscribes to an event of another object, but the subscriber object is not unsubscribed from the event when it is no longer needed. While it is possible to track the subscriptions manually (either in Destructors/Finalizers, or using IDisposable), when the number of event sources and subscribers increase, it becomes harder to manage them. The Weak Event Pattern provides a solution to this problem by using weak references to the event sources and subscribers.

Purpose

  • Prevent strong references from subscibers to event sources.
  • Allow subscribers to be garbage collected (GC'ed) even if they haven't explicitly unsubscribed from their events.

How It Works

  • The event source holds a weak reference to the subscriber.
  • When the event is raised, this weak reference is used to check if the subscriber is still alive.
  • If the subscriber is alive, the event source calls the event handler.
  • If the subscriber is no longer alive (since source holds only a weak reference, subscribers can be GC'ed throughout the app), the event source removes the weak reference from its list of subscribers.

Benefits

  • Reduces the risk of memory leaks by eliminating failed GC due to event subscriptions.
  • Simplifies the management of event subscription ans there is no need to manually unsubscribe from events.

Drawbacks

  • It is more complex to implement compared to traditional event subscription (which is usually a single line).
  • Requires a custom implementation most of the times.
  • It causes a slight performance overhead due to the extra indirection wshen checking if subscribers are still alive.

Implementation

The custom class WeakEventManager is responsible for all the work. This class provides methods to add and remove weak event handlers, and raise events in a way that prevents memory leaks. It uses weak references to keep track of the event sources and subscribers, and automatically removes the subscribers when they are no longer needed (i.e. GC’ed). This ensures that the subscribers can be GC’ed even if they have not explicitly unsubscribed from the event.

Code

public class WeakEventManager<TEventArgs> where TEventArgs : EventArgs
{
    private readonly ConditionalWeakTable<object, Dictionary<string, List<WeakReference<EventHandler<TEventArgs>>>>> eventHandlers = [];

    public void AddEventHandler(object source, string eventName, EventHandler<TEventArgs> handler)
    {
        if (!eventHandlers.TryGetValue(source, out var sourceEvents))
        {
            sourceEvents = [];
            eventHandlers.Add(source, sourceEvents);
        }
        if (!sourceEvents.TryGetValue(eventName, out var handlers))
        {
            handlers = [];
            sourceEvents[eventName] = handlers;
        }
        handlers.Add(new WeakReference<EventHandler<TEventArgs>>(handler));
    }

    public void RemoveEventHandler(object source, string eventName, EventHandler<TEventArgs> handler)
    {
        if (eventHandlers.TryGetValue(source, out var sourceEvents) &&
                    sourceEvents.TryGetValue(eventName, out var handlers))
        {
            handlers.RemoveAll(weakHandler => !weakHandler.TryGetTarget(out var eventHandler) || eventHandler == handler);
        }
    }

    public void RaiseEvent(object sender, string eventName, TEventArgs eventArgs)
    {
        if (eventHandlers.TryGetValue(sender, out var sourceEvents) &&
            sourceEvents.TryGetValue(eventName, out var handlers))
        {
            // Create a copy of the handlers to safely iterate over
            var handlersCopy = handlers.ToList();
            var handlersToRemove = new List<WeakReference<EventHandler<TEventArgs>>>();

            foreach (var weakHandler in handlersCopy)
            {
                if (weakHandler.TryGetTarget(out var handler))
                {
                    handler(sender, eventArgs);
                }
                else
                {
                    handlersToRemove.Add(weakHandler);
                }
            }

            handlers.RemoveAll(handlersToRemove.Contains);

            // If all handlers have been removed, remove the entry for this sender
            if (handlers.Count == 0)
            {
                eventHandlers.Remove(sender);
            }
        }
    }
}

Explanations

ConditionalWeakTable<TKey, TValue>

This is a specialized collection class in .NET that provides a way to associate additional data with object instances without preventing those instances from being garbage collected. It is a part of the System.Runtime. CompilerServices namespace, and has been available since .NET Framework 4.0 and .NET Core/ .NET 5.0. The key characteristics of this collection are:

  • Weak References: It holds weak references as its keys, allowing them to be garbage collected when they are no longer references elsewhere in the application.
  • Conditional: It is conditional in the sense that it does not prevent the keys from being garbage collected, but it does prevent the values from being garbage collected as long as the keys are alive.
  • Lifetime Management: When a key object is garbage collected, the corresponding entry in the table is automatically removed.
  • Thread Safety: It is thread-safe, and can be accessed from multiple threads both for read and write.
  • No Key Comparison: Unlike conventional dictionaries, it does not use the key's Equals or GetHashCode method to compare keys. Instead, it uses reference equality for keys.
  • Key Constraints: The keys must be of reference type.
  • Value Behaviors: Values can be either a reference or a value type. If it is of value type, it is boxed when stored in the table. If it is a reference type, it is held by a strong reference in the table.
  • Primary use cases are:
    • Adding properties to objects at runtime without modifying the underlying classes.
    • Implementing weak event patterns to prevent memory leaks in the event handling scenarios.
    • Caching additional data related to objects without preventing their garbase collection.

Client Transparency

As seen in the sample below, the client code is not aware of the weak event pattern implementation. It uses the WeakEventManager class to add and remove event handlers, and raise events. The client code does not need to worry about unsubscribing from events, as the WeakEventManager class takes care of this automatically. This allows to make no changes to the client code when switching from traditional event subscription to weak event pattern.

Sample Use

The Event Source Class

public class UnitOfWork()
{
    public event EventHandler<EventArgs> SomeProgressChanged
    {
        add => weakEventManager.AddEventHandler(this, nameof(SomeProgressChanged), value);
        remove => weakEventManager.RemoveEventHandler(this, nameof(SomeProgressChanged), value);
    }

    public event EventHandler<EventArgs> SomethingHappened
    {
        add => weakEventManager.AddEventHandler(this, nameof(SomethingHappened), value);
        remove => weakEventManager.RemoveEventHandler(this, nameof(SomethingHappened), value);
    }

    #region Raising Events
    weakEventManager.RaiseEvent(this, nameof(SomeProgressChanged), SomeProgressChangedArgs.Create(new SomeValue()));
    weakEventManager.RaiseEvent(this, nameof(SomethingHappened), EventArgs.Empty);
    #endregion
}

Note that the custom event arguments have to inherit from EventArgs to be used with the WeakEventManager class:

public class SomeProgressChangedArgs : EventArgs

Subsciber Class

unitOfWork.SomeProgressChanged += OnSomeProgressChanged;
unitOfWork.SomethingHappened += OnSomethingHappened;

As seen, the subscriber class does not need to know about the weak event pattern implementation. It subscribes to the events of the UnitOfWork class as if it was a traditional event subscription. The WeakEventManager class takes care of the rest. It also does not need to unsubscribe from the events, as the WeakEventManager class will automatically remove the subscriber when it is no longer needed.