Weak events in C#

 
 
  • Gérald Barré

Events are a common source of memory leaks when a subscriber forgets to unsubscribe to an event and the subscriber and event publisher don't have the same life cycle. Depending on the use case, you can use weak events. Weak events are events that do not prevent the subscriber from being garbage collected. In this post, I describe how to create weak events in C#.

Weak events rely on WeakReference<T>. A WeakReference<T> is a reference to an object that does not prevent the object from being garbage collected. You can use the TryGetTarget method to get the object if it has not been garbage collected. In our case, we use WeakReference<EventHandler<TEventArgs>> to store the event handlers, so the handlers can be collected if needed.

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?Buy Me A Coffee💖 Sponsor on GitHub