Weak events in C#

 
 
  • Gérald Barré

Events are a common source of memory leaks when a subscriber forgets to unsubscribe from an event and the subscriber and publisher have different life cycles. In such cases, weak events can help. Weak events do not prevent the subscriber from being garbage collected.

Weak events rely on WeakReference<T>, a reference that does not prevent its target from being garbage collected. Use the TryGetTarget method to retrieve the target if it is still alive. Here, WeakReference<EventHandler<TEventArgs>> stores the event handlers, allowing them to be collected when no longer referenced elsewhere.

C#
internal sealed class WeakEvent<TEventArgs>
{
    private ImmutableList<WeakReference<EventHandler<TEventArgs>>> _listeners = ImmutableList<WeakReference<EventHandler<TEventArgs>>>.Empty;

    // Keep the delegates alive with their target. This prevent anonymous delegates from being garbage collected prematurely.
    private readonly ConditionalWeakTable<object, List<object>> _delegateKeepAlive = new();

    public void AddListener(EventHandler<TEventArgs> handler)
    {
        if (handler == null)
            return;

        var weakReference = new WeakReference<EventHandler<TEventArgs>>(handler);
        _listeners = _listeners.Add(weakReference);
        if (handler.Target != null)
        {
            _delegateKeepAlive.GetOrCreateValue(handler.Target).Add(handler);
        }
    }

    public void RemoveListener(EventHandler<TEventArgs> handler)
    {
        if (handler == null)
            return;

        // Remove the handler and all handlers that have been garbage collected
        _listeners = _listeners.RemoveAll(wr => !wr.TryGetTarget(out var target) || handler.Equals(target));
        if (handler.Target != null && _delegateKeepAlive.TryGetValue(handler.Target, out var weakReference))
        {
            weakReference.Remove(handler);
        }
    }

    public void Raise(object? sender, TEventArgs args)
    {
        foreach (var listener in _listeners)
        {
            if (listener.TryGetTarget(out var target))
            {
                target.Invoke(sender, args);
            }
            else
            {
                // Remove the listener if the target has been garbage collected
                _listeners = _listeners.Remove(listener);
            }
        }
    }
}

Here's an example of how to use the WeakEvent<TEventArgs> class:

C#
var sample = new Sample();
sample.CustomEvent += (sender, e) => Console.WriteLine("handler");
sample.DoAction();

public class Sample
{
    private readonly WeakEvent<EventArgs> _customEvent = new();

    public event EventHandler<EventArgs> CustomEvent
    {
        add
        {
            _customEvent.AddListener(value);
        }
        remove
        {
            _customEvent.RemoveListener(value);
        }
    }

    public void DoAction()
    {
        _customEvent.Raise(this, EventArgs.Empty);
    }
}

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?