One of the well-known pitfalls of using events, is the holding of the subscriber instance by the publisher (if connecting to a delegate holding an instance method). If the subscriber forgets to unsubscribe, the instance cannot be garbage collected because the publisher holds a reference to it. Worse yet, the subscriber continues to receive event notifications even though it’s not interested.
One way to deal with this is presented in Jeffrey Richter’s book “CLR via C#”. The idea is based on a custom event implementation holding a list of WeakReference objects to the incoming subscribers delegates:
class Publisher {
private List<WeakReference> _subscribers = new List<WeakReference>();
public event EventHandler TheEvent {
add {
_subscribers.Add(new WeakReference(value));
}
remove {
//...
}
}
protected virtual void OnTheEvent() {
// raise the event
for(int i = _subscribers.Count - 1; i >= 0; i--) {
EventHandler eh = (EventHandler)_subscribers[i].Target;
if(eh == null) // delegate GC’d
_subscribers.RemoveAt(i);
else // raise the event
eh(this, EventArgs.Empty);
}
}
}
This approach has a fatal flaw: the WeakReference wraps the delegate object – not the subscriber instance itself! This means, that if a GC occurs, all the delegates in the list will be gone, disconnecting all subscribers, even those who are still very much alive.
To correct this, we need to use a WeakReference around the subscriber instance itself and not the delegate. Here’s some code that accomplishes this:
class Publisher {
private List<MethodInfo> _methods = new List<MethodInfo>();
private List<WeakReference> _instances = new List<WeakReference>();
public event EventHandler TheEvent {
add {
_methods.Add(value.Method);
if(value.Target != null)
_instances.Add(new WeakReference(value.Target));
else
_instances.Add(new WeakReference(value));
}
remove {
//...
}
}
public void Fire() {
for(int i = _methods.Count - 1; i >= 0; i--) {
object o = _instances[i].Target;
if(o != null) {
if(o.GetType() == typeof(WeakReference))
((EventHandler)o)(this, EventArgs.Empty);
else
_methods[i].Invoke(o, new object[] { this, EventArgs.Empty });
}
else {
_methods.RemoveAt(i);
_instances.RemoveAt(i);
}
}
}
}
Although this is somewhat involved, it solves the issue. It shouldn’t be too difficult to create some helper class to handle this in a generic way, to be used by any event publisher. (and the remove part must be completed…)
Another approach would be to abandon the event idea and use an interface for notifications. Wrapping an interface by a WeakReference is easy enough. The only downside – a subscriber must supply an object (cannot use static methods as callbacks) implementing the entire interface (even if only some methods are interesting).