We use weak events in C# to avoid memory leaks in event-based applications. Let’s learn more about weak events, why they’re needed, and how to implement them.
Let’s get started.
What Are Strong and Weak Events?
A strong event is the default implementation of an event in C#. It enables an object to notify other objects when a change occurs.
Let’s see it in action by creating a Publisher
class:
public class Publisher { public event EventHandler? Event; public void RaiseEvent() { Event?.Invoke(this, EventArgs.Empty); } }
Now, let’s create a Subscriber
class to subscribe to the Event
event:
public class Subscriber { public void HandleEvent(object sender, EventArgs e) { Console.WriteLine("Event received."); } }
When subscribing to the event, we create a strong reference between the publisher and the subscriber. This strong reference doesn’t allow the garbage collector (GC) to collect the subscriber object as the publisher is still alive (i.e. GC hasn’t collected it).
On the other hand, when creating a weak event, the event handler creates a weak reference between the publisher and the subscriber. A weak reference allows the GC to collect the object if there are no other strong references to it.
When we raise an event, the weak reference is checked to see if the target (subscriber) is still alive. If it is, the application invokes the event handler. However, if the target has been collected, the weak reference is removed from the list of event handlers.
Why Do We Need Weak Events?
To understand why we need weak events, we need to review our default implementation of an event.
Let’s imagine a data provider service that raises periodic events to update a part of the application UI. The Publisher
class could represent this service while the Subscriber
class would represent the UI components.
When these UI components subscribe to the service’s events, the service holds a strong reference to the event handlers in each UI component. If we assume a user can remove these UI components on demand, we’d expect the GC to free up the memory when these components are no longer needed.
However, due to the strong references held by the service, the GC can’t reclaim the memory used by these UI components. This leads to a memory leak:
var publisher = new Publisher(); var subscriber = new Subscriber(); publisher.Event += subscriber.HandleEvent; publisher.RaiseEvent(); subscriber = null; GC.Collect(); publisher.RaiseEvent();
Here, we’ve explicitly set the subscriber
object to null
to simulate a component removal. Then, we force garbage collection using the GC.Collect()
method. However, it’s still not eligible for garbage collection as publisher
holds a strong reference to it.
When we run the application, it raises the event even after the garbage collection process:
Event received. Event received.
By using weak references instead, we ensure that the event subscription does not prevent the garbage collection of the subscriber. Thus, weak events help manage memory more efficiently by ensuring that objects are collected as soon as they are no longer needed.
How to Implement Weak Events
We can use the WeakReference
class to implement a weak event mechanism. The WeakReference
class holds a weak reference to an object. This ensures that the application doesn’t prevent the garbage collector from collecting the object if there are no other strong references to it.
Let’s create a WeakEvent
class:
public class WeakEvent<TEventArgs> where TEventArgs : EventArgs { private readonly List<WeakReference<EventHandler<TEventArgs>>> _eventHandlers = []; }
The _eventHandlers field maintains a list of weak references to event handlers. It allows subscribers to subscribe to events raised by publishers without creating strong references between them:
Next, let’s add an AddEventHandler()
method to add event handlers in this class:
public void AddEventHandler(EventHandler<TEventArgs> handler) { if (handler == null) return; _eventHandlers.Add(new WeakReference<EventHandler<TEventArgs>>(handler)); }
And another RemoveEventHandler()
method to remove event handlers:
public void RemoveEventHandler(EventHandler<TEventArgs> handler) { var eventHandler = _eventHandlers.FirstOrDefault(wr => { wr.TryGetTarget(out var target); return target == handler; }); if (eventHandler != null) { _eventHandlers.Remove(eventHandler); } }
Finally, let’s add aRaiseEvent()
method:
public void RaiseEvent(object sender, TEventArgs e) { foreach (var eventHandler in _eventHandlers.ToArray()) { if (eventHandler.TryGetTarget(out var handler)) { handler(sender, e); } } }
This method triggers the event by invoking all the subscribed event handlers. We’re using the TryGetTarget()
method to retrieve the target event handler. If successful, it invokes the event handler with the provided sender and event arguments.
Now let’s create a WeakReferenceSubscriber
class that’ll act as our subscriber:
public class WeakReferenceSubscriber { public void Subscribe(WeakReferencePublisher publisher) { publisher.Event.AddEventHandler(HandleEvent); } public void HandleEvent(object? sender, EventArgs e) { Console.WriteLine("Weak Event received."); } }
This will subscribe to the WeakReferencePublisher
class:
public class WeakReferencePublisher { public WeakEvent<EventArgs> Event { get; } = new WeakEvent<EventArgs>(); public void RaiseEvent() { Event.RaiseEvent(this, EventArgs.Empty); } }
Now, if we try to raise an event after removing the subscriber, the application doesn’t raise the latter event:
var weakEventPublisher = new WeakReferencePublisher(); var weakEventSubscriber = new WeakReferenceSubscriber(); weakEventSubscriber.Subscribe(weakEventPublisher); weakEventPublisher.RaiseEvent(); weakEventSubscriber = null; GC.Collect(); weakEventPublisher.RaiseEvent();
The weak reference allows the GC to collect the subscriber objects that are no longer alive:
Weak Event received.
This prevents the memory leaks we were facing with the default implementation of events.
Conclusion
In this article, we learned about weak events in C#. We saw how they provide a way to handle events without creating strong references. By using weak references, we can ensure that the garbage collector removes subscribers from memory when not needed.