Click here to Skip to main content
16,020,424 members
Articles / Desktop Programming / WPF

Weak Events in C#

Rate me:
Please Sign up or sign in to vote.
4.95/5 (240 votes)
25 Apr 2009MIT12 min read 504.6K   5.4K   535   84
Different approaches to weak events

Table of contents

Introduction

When using normal C# events, registering an event handler creates a strong reference from the event source to the listening object.

NormalEvent.png

If the source object has a longer lifetime than the listener, and the listener doesn't need the events anymore when there are no other references to it, using normal .NET events causes a memory leak: the source object holds listener objects in memory that should be garbage collected.

There are lots of different approaches to this problem. This article will explain some of them and discuss their advantages and disadvantages. I have sorted the approaches in two categories: first, we will assume that the event source is an existing class with a normal C# event; after that, we will allow modifying the event source to allow different approaches.

What Exactly are Events?

Many programmers think events are a list of delegates - that's simply wrong. Delegates themselves have the ability to be "multi-cast":

C#
EventHandler eh = Method1;
eh += Method2;

So, what then are events? Basically, they are like properties: they encapsulate a delegate field and restrict access to it. A public delegate field (or a public delegate property) could mean that other objects could clear the list of event handlers, or raise the event - but we want only the object defining the event to be able to do that.

Properties essentially are a pair of get/set-methods. Events are just a pair of add/remove-methods.

C#
public event EventHandler MyEvent {
   add { ... }
   remove { ... }
}

Only adding and removing handlers is public. Other classes cannot request the list of handlers, cannot clear the list, or cannot call the event.

Now, what leads to confusion sometimes is that C# has a short-hand syntax:

C#
public event EventHandler MyEvent;

This expands to:

C#
private EventHandler _MyEvent; // the underlying field
// this isn't actually named "_MyEvent" but also "MyEvent",
// but then you couldn't see the difference between the field
// and the event.
public event EventHandler MyEvent {
  add { lock(this) { _MyEvent += value; } }
  remove { lock(this) { _MyEvent -= value; } }
}

Yes, the default C# events are locking on this! You can verify this with a disassembler - the add and remove methods are decorated with [MethodImpl(MethodImplOptions.Synchronized)], which is equivalent to locking on this.

Registering and deregistering events is thread-safe. However, raising the event in a thread-safe manner is left to the programmer writing the code that raises the event, and often gets done incorrectly: the raising code that's probably used the most is not thread-safe:

C#
if (MyEvent != null)
   MyEvent(this, EventArgs.Empty);
   // can crash with a NullReferenceException
   // when the last event handler is removed concurrently.

The second most commonly seen strategy is first reading the event delegate into a local variable.

C#
EventHandler eh = MyEvent;
if (eh != null) eh(this, EventArgs.Empty);

Is this thread-safe? Answer: it depends. According to the memory model in the C# specification, this is not thread-safe. The JIT compiler is allowed to eliminate the local variable, see Understand the Impact of Low-Lock Techniques in Multithreaded Apps [^]. However, the Microsoft .NET runtime has a stronger memory model (starting with version 2.0), and there, that code is thread-safe. It happens to be also thread-safe in Microsoft .NET 1.0 and 1.1, but that's an undocumented implementation detail.

A correct solution, according to the ECMA specification, would have to move the assignment to the local variable into a lock(this) block or use a volatile field to store the delegate.

C#
EventHandler eh;
lock (this) { eh = MyEvent; }
if (eh != null) eh(this, EventArgs.Empty);

This means we'll have to distinguish between events that are thread-safe and events that are not thread-safe.

Part 1: Listener-side Weak Events

In this part, we'll assume the event is a normal C# event (strong references to event handlers), and any cleanup will have to be done on the listening side.

Solution 0: Just Deregister

C#
void RegisterEvent()
{
    eventSource.Event += OnEvent;
}
void DeregisterEvent()
{
    eventSource.Event -= OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
    ...
}

Simple and effective, this is what you should use when possible. But, often, it's not trivially possible to ensure the DeregisterEvent method is called whenever the object is no longer in use. You might try the Dispose pattern, though that's usually meant for unmanaged resources. A finalizer will not work: the garbage collector won't call it because the event source still holds a reference to our object!

Advantages

Simple if the object already has a notion of being disposed.

Disadvantages

Explicit memory management is hard, code can forget to call Dispose.

Solution 1: Deregister When the Event is Called

C#
void RegisterEvent()
{
    eventSource.Event += OnEvent;
}

void OnEvent(object sender, EventArgs e)
{
    if (!InUse) {
        eventSource.Event -= OnEvent;
        return;
    }
    ...
}

Now, we don't require that someone tells us when the listener is no longer in use: it just checks this itself when the event is called. However, if we cannot use solution 0, then usually, it's also not possible to determine "InUse" from within the listener object. And given that you are reading this article, you've probably come across one of those cases.

But, this "solution" already has an important disadvantage over solution 0: if the event is never fired, then we'll leak listener objects. Imagine that lots of objects register to a static "SettingsChanged" event - all these objects cannot be garbage collected until a setting is changed - which might never happen in the program's lifetime.

Advantages

None.

Disadvantages

Leaks when the event never fires; usually, "InUse" cannot be easily determined.

Solution 2: Wrapper with Weak Reference

This solution is nearly identical to the previous, except that we move the event handling code into a wrapper class that forwards the calls to a listener instance which is referenced with a weak reference. This weak reference allows for easy detection if the listener is still alive.

WeakEventWithWrapper.png

C#
EventWrapper ew;
void RegisterEvent()
{
    ew = new EventWrapper(eventSource, this);
}
void OnEvent(object sender, EventArgs e)
{
    ...
}
sealed class EventWrapper
{
    SourceObject eventSource;
    WeakReference wr;
    public EventWrapper(SourceObject eventSource,
                        ListenerObject obj) {
        this.eventSource = eventSource;
        this.wr = new WeakReference(obj);
        eventSource.Event += OnEvent;
   }
   void OnEvent(object sender, EventArgs e)
   {
        ListenerObject obj = (ListenerObject)wr.Target;
        if (obj != null)
            obj.OnEvent(sender, e);
        else
            Deregister();
    }
    public void Deregister()
    {
        eventSource.Event -= OnEvent;
    }
}

Advantages

Allows garbage collection of the listener object.

Disadvantages

Leaks the wrapper instance when the event never fires; writing a wrapper class for each event handler is a lot of repetitive code.

Solution 3: Deregister in Finalizer

Note that we stored a reference to the EventWrapper and had a public Deregister method. We can add a finalizer to the listener and use that to deregister from the event.

C#
~ListenerObject() {
    ew.Deregister();
}

That should take care of our memory leak, but it comes at a cost: finalizable objects are expensive for the garbage collector. When there are no references to the listener object (except for the weak reference), it'll survive the first garbage collection (and move to a higher generation), have the finalizer run, and then can only be collected after the next garbage collection (of the new generation).

Also, finalizers run on the finalizer thread; this may cause problems if registering/deregistering events on an event source is not thread-safe. Remember, the default events generated by the C# compiler are not thread-safe!

Advantages

Allows garbage collection of the listener object; does not leak wrapper instances.

Disadvantages

Finalizer delays GC of listener; requires thread-safe event source; lots of repetitive code.

Solution 4: Reusable Wrapper

The code download contains a reusable version of the wrapper class. It works by taking the lambda expressions for the code parts that need to be adapted to a specific use: Register event handler, deregister event handler, forward the event to a private method.

C#
eventWrapper = WeakEventHandler.Register(
    eventSource,
    (s, eh) => s.Event += eh, // registering code
    (s, eh) => s.Event -= eh, // deregistering code
    this, // event listener
    (me, sender, args) => me.OnEvent(sender, args) // forwarding code
);

WrapperWithLambdas.png

The returned eventWrapper exposes a single public method: Deregister. Now, we need to be careful with lambda expressions, since they are compiled to delegates that may contain further object references. That's why the event listener is passed back as "me". Had we written (me, sender, args) => this.OnEvent(sender, args), the lambda expression would have captured the "this" variable, causing a closure object to be generated. Since the WeakEventHandler stores a reference to the forwarding delegate, this would have caused a strong reference from the wrapper to the listener. Fortunately, it's possible to check whether a delegate captures any variables: the compiler will generate an instance method for lambda expressions that capture variables, and a static method for lambda expressions that don't. WeakEventHandler checks this using Delegate.Method.IsStatic, and will throw an exception if you use it incorrectly.

This approach is fairly reusable, but it still requires a wrapper class for each delegate type. While you can get pretty far with System.EventHandler and System.EventHandler<T>, you might want to automate this when there are lots of different delegate types. This could be done at compile-time using code generation, or at runtime using System.Reflection.Emit.

Advantages

Allows garbage collection of listener object; code overhead not too bad.

Disadvantages

Leaks wrapper instance when event never fires.

Solution 5: WeakEventManager

WPF has built-in support for listener-side weak events, using the WeakEventManager class. It works similar to the previous wrapper solutions, except that a single WeakEventManager instance serves as a wrapper between multiple sender and multiple listeners. Due to this single instance, the WeakEventManager can avoid the leak when the event is never called: registering another event on a WeakEventManager can trigger a clean-up of old events. These clean-ups are scheduled using the WPF dispatcher, they will occur only on threads running a WPF message loop.

Also, the WeakEventManager has a restriction that our previous solutions didn't have: it requires the sender parameter to be set correctly. If you use it to attach to button.Click, only events with sender==button will be delivered. Some event implementations may simply attach the handlers to another event:

C#
public event EventHandler Event {
    add { anotherObject.Event += value; }
    remove { anotherObject.Event -= value; }
}

Such events cannot be used with WeakEventManager.

There is one WeakEventManager class per event, each with an instance per thread. The recommended pattern for defining these events is a lot of boilerplate code: see "WeakEvent Patterns" on MSDN [^].

Fortunately, we can simplify this with Generics:

C#
public sealed class ButtonClickEventManager
    : WeakEventManagerBase<ButtonClickEventManager, Button>
{
    protected override void StartListening(Button source)
    {
        source.Click += DeliverEvent;
    }

    protected override void StopListening(Button source)
    {
        source.Click -= DeliverEvent;
    }
}

Note that DeliverEvent takes (object, EventArgs), whereas the Click event provides (object, RoutedEventArgs). While there is no conversion between delegate types, C# supports contravariance when creating delegates from method groups [^].

Advantages

Allows garbage collection of listener object; does not leak wrapper instances.

Disadvantages

Tied to a WPF dispatcher, cannot be easily used on non-UI-threads.

Part 2: Source-side Weak Events

Here, we'll take a look at ways to implement weak events by modifying the event source.

All these have a common advantage over the listener-side weak events: we can easily make registering/deregistering handlers thread-safe.

Solution 0: Interface

The WeakEventManager also deserves to be mentioned in this section: as a wrapper, it attaches ("listening-side") to normal C# events, but it also provides ("source-side") a weak event to clients.

In the WeakEventManager, this is the IWeakEventListener interface. The listening object implements an interface, and the source simply has a weak reference to the listener and calls the interface method.

SourceWithListeners.png

Advantages

Simple and effective.

Disadvantages

When a listener handles multiple events, you end up with lots of conditions in the HandleWeakEvent method to filter on event type and on event source.

Solution 1: WeakReference to Delegate

This is another approach to weak events used in WPF: CommandManager.InvalidateRequery looks like a normal .NET event, but it isn't. It holds only a weak reference to the delegate, so registering to that static event does not cause memory leaks.

WeakDelegateBug.png

This is a simple solution, but it's easy for event consumers to forget about it and get it wrong:

C#
CommandManager.InvalidateRequery += OnInvalidateRequery;

//or

CommandManager.InvalidateRequery += new EventHandler(OnInvalidateRequery);

The problem here is that the CommandManager only holds a weak reference to the delegate, and the listener doesn't hold any reference to it. So, on the next GC run, the delegate will be garbage collected, and OnInvalidateRequery doesn't get called anymore even if the listener object is still in use. To ensure the delegate survives long enough, the listener is responsible for keeping a reference to it.

WeakDelegates.png

C#
class Listener {
    EventHandler strongReferenceToDelegate;
    public void RegisterForEvent()
    {
        strongReferenceToDelegate = new EventHandler(OnInvalidateRequery);
        CommandManager.InvalidateRequery += strongReferenceToDelegate;
    }
    void OnInvalidateRequery(...) {...}
}

WeakReferenceToDelegate in the source-code download shows an example event implementation that is thread-safe and cleans the handler list when another handler is added.

Advantages

Doesn't leak delegate instances.

Disadvantages

Easy to get wrong: forgetting the strong reference to the delegate causes events to fire only until the next garbage collection. This can result in hard-to-find bugs.

Solution 2: object + Forwarder

While solution 0 was adapted from the WeakEventManager, this solution is adapted from the WeakEventHandler wrapper: register an object,ForwarderDelegate pair.

SmartEventForwarding.png

C#
eventSource.AddHandler(this,
    (me, sender, args) => ((ListenerObject)me).OnEvent(sender, args));

Advantages

Simple and effective.

Disadvantages

Unusual signature for registering events; forwarding lambda expressions require cast.

Solution 3: SmartWeakEvent

The SmartWeakEvent in the source code download provides an event that looks like a normal .NET event, but keeps weak references to the event listener. It does not suffer from the "must keep reference to delegate"-problem.

C#
void RegisterEvent()
{
    eventSource.Event += OnEvent;
}
void OnEvent(object sender, EventArgs e)
{
    ...
}

Event definition:

C#
SmartWeakEvent<EventHandler> _event
   = new SmartWeakEvent<EventHandler>();

public event EventHandler Event {
    add { _event.Add(value); }
    remove { _event.Remove(value); }
}

public void RaiseEvent()
{
    _event.Raise(this, EventArgs.Empty);
}

How does it work? Using the Delegate.Target and Delegate.Method properties, each delegate is split up into a target (stored as a weak reference) and the MethodInfo. When the event is raised, the method is invoked using Reflection.

SmartEventReflection.png

A possible problem here is that someone might try to attach an anonymous method as an event handler that captures a variable.

C#
int localVariable = 42;
eventSource.Event += delegate { Console.WriteLine(localVariable); };

In this case, the delegate's target object is the closure, which can be immediately collected because there are no other references to it. However, the SmartWeakEvent can detect this case and will throw an exception, so you won't have any difficulty to debug problems because the event handler is deregistered before you think it should be.

C#
if (d.Method.DeclaringType.GetCustomAttributes(
  typeof(CompilerGeneratedAttribute), false).Length != 0)
    throw new ArgumentException(...);

Advantages

Looks like a real weak event; nearly no code overhead.

Disadvantages

Invocation using Reflection is slow; does not work in partial trust because it uses reflection on private methods.

Solution 4: FastSmartWeakEvent

The functionality and usage is identical to the SmartWeakEvent, but the performance is dramatically improved.

Here are the benchmark results of an event with two registered delegates (one instance method and one static method):

Normal (strong) event...   16948785 calls per second
Smart weak event...           91960 calls per second
Fast smart weak event...    4901840 calls per second

How does it work? We're not using Reflection anymore to call the method. Instead, we're compiling a forwarder method (similar to the "forwarding code" in the previous solutions) at runtime using System.Reflection.Emit.DynamicMethod.

Advantages

Looks like a real weak event; nearly no code overhead.

Disadvantages

Does not work in partial trust because it uses reflection on private methods.

Suggestions

  • For anything running on the UI thread in WPF applications (e.g., custom controls that attach events on the model objects), use the WeakEventManager.
  • If you want to provide a weak event, use FastSmartWeakEvent.
  • If you want to consume an event, use WeakEventHandler.

History

  • 24 Apr 2009: code updated (bug fixes)
    • Incorrect 'Derived from EventArgs' check reported by olivianer and Fintan
    • Type safety issue with FastSmartWeakEvent
  • 5 Oct 2008: Article published

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Germany Germany
I am the lead developer on the SharpDevelop open source project.

Comments and Discussions

 
GeneralMessage Closed Pin
10-Mar-22 4:05
Digital Xpressions10-Mar-22 4:05 
Questionnice thing but code overhead is meh anyway -__- Pin
zloidooraque16-May-15 12:31
zloidooraque16-May-15 12:31 
GeneralMy vote of 5 Pin
mishrsud30-Dec-14 19:39
professionalmishrsud30-Dec-14 19:39 
QuestionWorks great, when you know the strongly typed parts, but... Pin
mwpowellhtx1-Nov-14 11:47
mwpowellhtx1-Nov-14 11:47 
Suggestion.Net 4.5 - WeakEventManager<TEventSource, TEventArgs> Pin
ISkomorokh8-Sep-14 23:35
ISkomorokh8-Sep-14 23:35 
GeneralRe: .Net 4.5 - WeakEventManager<TEventSource, TEventArgs> Pin
Member 78820294-Nov-15 7:48
Member 78820294-Nov-15 7:48 
GeneralMy vote of 5 Pin
taleofsixstrings19-Apr-14 5:27
taleofsixstrings19-Apr-14 5:27 
QuestionThis is the best WeakEventManager which is not bound to WPF Pin
Alois Kraus29-Jan-14 2:42
Alois Kraus29-Jan-14 2:42 
AnswerRe: This is the best WeakEventManager which is not bound to WPF Pin
Phil Deg5-Oct-16 17:29
Phil Deg5-Oct-16 17:29 
GeneralRe: This is the best WeakEventManager which is not bound to WPF Pin
Alois Kraus6-Oct-16 9:04
Alois Kraus6-Oct-16 9:04 
BugInvocation order differs from plain events Pin
Robert Važan (SK)4-Sep-13 11:17
Robert Važan (SK)4-Sep-13 11:17 
GeneralRe: Invocation order differs from plain events Pin
Daniel Grunwald4-Sep-13 12:35
Daniel Grunwald4-Sep-13 12:35 
GeneralRe: Invocation order differs from plain events Pin
Robert Važan (SK)5-Sep-13 0:06
Robert Važan (SK)5-Sep-13 0:06 
BugAre you getting MethodAccessException? Skip visibility checks. Pin
Robert Važan (SK)3-Sep-13 9:16
Robert Važan (SK)3-Sep-13 9:16 
GeneralMy vote of 5 Pin
Todd Pichler20-Mar-13 17:38
Todd Pichler20-Mar-13 17:38 
GeneralMy vote of 5 Pin
MehranDVD25-Jan-13 2:18
MehranDVD25-Jan-13 2:18 
QuestionGreat - Thanks - But got some observations... Pin
Eric Ouellet11-Jan-13 7:25
professionalEric Ouellet11-Jan-13 7:25 
AnswerRe: Great - Thanks - But got some observations... Pin
Daniel Grunwald4-Sep-13 16:17
Daniel Grunwald4-Sep-13 16:17 
GeneralRe: Great - Thanks - But got some observations... Pin
Eric Ouellet5-Sep-13 3:25
professionalEric Ouellet5-Sep-13 3:25 
GeneralMy vote of 3 Pin
MB Seifollahi8-Sep-12 3:55
professionalMB Seifollahi8-Sep-12 3:55 
GeneralMy vote of 5 Pin
mmir15046-Mar-12 4:01
mmir15046-Mar-12 4:01 
GeneralMy vote of 5 Pin
Jalal Khordadi26-Feb-12 19:37
Jalal Khordadi26-Feb-12 19:37 
GeneralMy vote of 4 Pin
Qwertie21-Jan-12 4:34
Qwertie21-Jan-12 4:34 
QuestionExcellent -Small idea for improvement (of article), and Thread-safety question Pin
Fade (Amit BS)28-Nov-11 23:00
Fade (Amit BS)28-Nov-11 23:00 
GeneralMy vote of 5 Pin
Fade (Amit BS)28-Nov-11 22:47
Fade (Amit BS)28-Nov-11 22:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.