The Event Handler That Cried Wolf
I ran into an interesting and unexpected behavior in ASP.NET AJAX event handling the other day. Once I figured out what was going on, I was almost positive it was a bug, so I started looking into what a solution would be. But just out of curiosity, I looked at what the behavior was for a server-side event. Much to my surprise, the behavior was the same. The behavior then was consistent with the server-side behavior, not a bug. But is it the "correct" behavior? Tell me what you think...
To put it in the most shocking way possible: Removing a handler from an event does not guarantee that handler will not be called when the event fires!
But it's true. Take a look at this code...
public class Bar {
public event EventHandler SomeEvent;
public void RaiseEvent() {
if (SomeEvent != null) {
SomeEvent(this, null);
}
}
}
public class Foo {
private Bar _bar;
private EventHandler _handler;
private ArrayList _list;
public void Initialize(Bar bar) {
_bar = bar;
_list = new ArrayList();
// listen to the event on bar
_handler = new EventHandler(OnSomeEvent);
_bar.SomeEvent += _handler;
}
private void OnSomeEvent(object sender, EventArgs args) {
// event was raised, do something
// this cant happen if Stop() was called.... right?
_list.Add("blah");
Console.WriteLine("OnSomeEvent");
}
public void Stop() {
// tidy up, stop listening to the event and clear the list
_bar.SomeEvent -= _handler;
_list = null;
}
}
Now, this is a totally contrived example, so the code here isn't exactly useful. But the basic idea can occur in a real program. Here, there's a Bar component that exposes an event, SomeEvent. Normally a component raises it's event when appropriate and doesn't have an actual RaiseEvent() method to raise it, but for demo purposes, it's got one so we can fire the event later on.
The Foo component has two public methods -- Initialize, and Stop. Initialize takes a Bar component, and it adds an event listener to the event. Stop removes the event listener, since after all, it is important to clean up after yourself when it comes to events. If the handler was never removed, the Bar component will forever have a reference to the Foo, and Foo will live as long as Bar lives.
When the SomeEvent fires, Foo adds to an ArrayList which it has internally created. Notice in Stop() it sets the ArrayList to null. The event handler OnSomeEvent adds to the list without checking it for null.
If somehow the event handler were called even after Stop(), we'd have a null reference exception! Can't happen, right? When you think about a typical use of these components, it doesn't appear to be possible... Here would be one way of raising the event and removing the handler.
public class Program {
public static void Main(string[] args) {
Bar bar = new Bar();
Foo foo = new Foo();
foo.Initialize(bar);
bar.RaiseEvent();
foo.Stop();
Console.ReadLine();
}
}
No problem there. But what if an event handler for SomeEvent removes the event handler of a separate component? Something like this...
public static void Main(string[] args) {
Bar bar = new Bar();
Foo foo = new Foo();
bar.SomeEvent +=
delegate(object sender, EventArgs a) {
foo.Stop();
};
foo.Initialize(bar);
bar.RaiseEvent();
foo.Stop();
Console.ReadLine();
}
The result? BOOM...
Even though Foo has removed its listener, the listener is called. The reason is because its listener was removed after the event was already underway -- and, I suppose for multi-threaded reasons, once an event begins, the listeners that existed at the time will fire even if they are removed in the process.
Again, this is a very contrived example. But it could happen more innocently and not be so obvious. Perhaps when SomeEvent occurs, under a certain condition, a listener in the application decides to shut down some container which contains a Foo component, and that shutdown process calls Stop() on Foo. The first listener doesn't know that will happen. And Foo doesn't know Stop() was called as a result of an earlier listener to the very event it is unhooking from. Who is supposed to know what is going on?
So be careful, I suppose is the lesson to be learned. If a component may end up removing an even listener as a result of code external to it, it is entirely possible that it is the very event it is removing the listener from that is currently executing and causing it. In that scenario, you shouldn't assume you are safe from the listener being called.