Avoiding Events, or how to wrap an Event with a continuation handle
If there is one language feature of .NET that I've become increasingly apprehensive of it is events. On the surface they seem incredibly useful, letting you observe behavior without the observed object having to know anything about the observer. But the way they are implemented has a number of problems that makes me avoid them whenever possible.
Memory Leaks
The biggest pitfall with events is that they are a common source of "memory leaks". Yes, a managed language can leak memory -- it happens anytime you create an object that is still referenced by an active object and cannot be garbage collected. The nasty bit that usually goes unmentioned is that an event subscription represents an object holding a reference to the observed instance. Not only does this go unmentioned, but Microsoft spent years showing off code samples and doing drag and drop demos of subscribing to events without stressing that you need to also unsubscribe from them again.
Every "memory leak" I've ever dealt with in .NET traced back to some subscription that wasn't released. And tracking this down in a large project is nasty work --taking and comparing memory shapshots to see what objects are sticking around, who subscribes to them and whether they should really still be subscribed. All because the observer affects the ability of the observed to go out of scope, which seems like a violation of the Observer pattern.
Alternatives to Events
Weak Event Pattern
A pattern I've implemented from scratch several times (the side-effect of implementing core features in proprietary code) is the Weak Event pattern, i.e. an event that uses a weak reference as the subscription, so that the observed class isn't pinned in memory by a subscriber.
.NET 4 Microsoft has even formalized this with the WeakEventManager to implement the Weak Event Pattern, although I prefer just overriding the add
and remove
on an event and using weak references under the hood. While this changes the expected behavior of events and is unexpected in public facing APIs, I consider it the way events should have been implemented in the first place, and use it as default in my non-public facing code.
IObservable
A better way of implementing the Observer pattern is IObservable
from the Reactive Framework (Rx). Getting a stream of events pushed at you is a lot more natural for observation and allows for following a number of different behaviors in one observer. It also provides a mechanism for terminating the subscription from the observed end, as well a way deal with exceptions occuring in event generation. For new APIs this is definitely my prefered method of pushing state changes at listeners.
Using a continuation handle to subscribe to a single event invocation
A pattern I encounter frequently are one time events that simply signal a change in state, such as a connection being estatblished or closed. What I really want for these is a callback. I've added methods in the vein of AddConnectedCallback(Action callback)
, but always feel like their unintuitive constructs born out of my dislike of events, so generally I just end up creating events for these after all.
I could just use a lambda to subscribe to an event an capture the current scope much like the .WhenDone
handler of Result
, the lambda is anonymous making it impossible to unsubscribe:
The mere fact that lambdas are being shown as convenient ways to subscribe to events without any mention about the reference leaks this introduces just further illustrates how broken both events and their guidance are. Using this closure, simplifies attaching behavior at invocation time and makes sure that unsubscribe is handled cleanly.
Doing a lot of asynchronous programming work with MindTouch DReAM's Result
continuation handle (think TPL's Task
, but available since .NET 2.0), I decided that being able to subscribe to an event with a result would be ideal. Inspired by Rx's Observable.FromEvent
, I created EventClosure
, which can be used like this:
EventClosure.Subscribe(h => xmpp.OnLogin += h, h => xmpp.OnLogin -= h)
.WhenDone(r => xmpp.Send("Hello"));
Unfortunately, like Observable.FromEvent
, you have to set up the subscribe and unsubscribe using an Action
provided handler, since there isn't a way to pass xmpp.OnLogin
as an argument and do it programatically. But at least now the subscribe and unsubscribe are handled in one place and I can concentrate on the logic I want executed at event invocation.
I could have implemented this same pattern using Task
, but until async/await
ships, Result
still has the advantage, aside from continuation via .WhenDone or Blocking via .Block or .Wait, Result also gives me the ability to use a coroutine:
public IEnumerator<IYield> ConnectAndWelcome(Result<Xmpp> result) {
var xmpp = CreateClient();
var loginContinuation = EventClosure.Subscribe(h => xmpp.OnLogin += h, h => xmpp.OnLogin -= h);
xmpp.Connect();
yield return loginContinuation;
xmpp.Send("hello");
result.Return(xmpp);
}
This creates the client, starts the connection and suspends itself until connected, so it can then send a welcome message and return the connected client to its invokee. All this happens asynchronously! The implementation of EventClosure
looks like this (and could easily be adapted to use Task
instead of Result
):
public static class EventClosure {
public static Result Subscribe(
Action<EventHandler> subscribe,
Action<EventHandler> unsubscribe
) {
return Subscribe(subscribe, unsubscribe, new Result());
}
public static Result Subscribe(
Action<EventHandler> subscribe,
Action<EventHandler> unsubscribe,
Result result
) {
var closure = new Closure(unsubscribe, result);
subscribe(closure.Handler);
return result;
}
public static Result<TEventArgs> Subscribe<TEventArgs>(
Action<EventHandler<TEventArgs>> subscribe,
Action<EventHandler<TEventArgs>> unsubscribe
) where TEventArgs : EventArgs {
return Subscribe(subscribe, unsubscribe, new Result<TEventArgs>());
}
public static Result<TEventArgs> Subscribe<TEventArgs>(
Action<EventHandler<TEventArgs>> subscribe,
Action<EventHandler<TEventArgs>> unsubscribe,
Result<TEventArgs> result
) where TEventArgs : EventArgs {
var closure = new Closure<TEventArgs>(unsubscribe, result);
subscribe(closure.Handler);
return result;
}
private class Closure {
private readonly Action<EventHandler> _unsubscribe;
private readonly Result _result;
public Closure(Action<EventHandler> unsubscribe, Result result) {
_unsubscribe = unsubscribe;
_result = result;
}
public void Handler(object sender, EventArgs eventArgs) {
_unsubscribe(Handler);
_result.Return();
}
}
private class Closure<TEventArgs> where TEventArgs : EventArgs {
private readonly Action<EventHandler<TEventArgs>> _unsubscribe;
private readonly Result<TEventArgs> _result;
public Closure(Action<EventHandler<TEventArgs>> unsubscribe, Result<TEventArgs> result) {
_unsubscribe = unsubscribe;
_result = result;
}
public void Handler(object sender, TEventArgs eventArgs) {
_unsubscribe(Handler);
_result.Return(eventArgs);
}
}
}
While this pattern is limited to single fire events, since Result
can only be triggered once, it is a common enough pattern of event usage and one of the cleanest ways to receive that notification asynchronously.