Introduction
Although ASP.NET is a mature technology with broad acceptance, some fundamental concepts are very often unclear to developers. One of those concepts is event handling. The complexity of the subject originates from the ASP.NET Page and Control Life Cycle as well as platform-specific methodologies of defining event handlers.
I'm not going to cover the basics of ASP.NET event handling here. Instead, I recommend that you first read more on the following topics:
The Quick and Dirty Way
For a simple solution, you don't need more than the following:
public class MyControl : Control
{
public event EventHandler MyEvent;
}
This strategy is OK for prototyping and for starting your experiments with the technology. What happens in the above snippet is that your control receives an additional field of type EventHandler
. Despite simplicity, one drawback of this approach is somewhat suboptimal memory usage.
Ask yourself a question: do you always process all events for every control present on your page? Most users define handlers only for a small fraction of events. Even though your control does expose an event, there is no guarantee that it will always be handled. Most of the time, your field will have the value of null
simply occupying memory.
Generally, you should always keep your design approaches subject to analysis. If you really believe that storing the handler in a field is appropriate for your solution, then just go for it! But if you're still open for an experiment, let's just head over to the next section.
Exposing Events the Right Way
Since our control always inherits from the System.Web.UI.Control
class, it makes perfect sense to first investigate what functionality is readily provided to our control by its base class.
If you're working with Visual Studio 2005 or higher, a great way to do it is the Go To Definition command. Visual Studio automatically displays a window that looks pretty much like a source file. The entire class interface is displayed there (all public
and protected
members not marked as internal
). The brief documentation is there as well. Very informative, huh?
What we need is the Events
collection:
protected EventHandlerList Events { get; }
The fact that this property is protected
means that it is to be used in derived classes. Actually, if you do some Reflectoring, you will notice that this is really the case.
Now, let's look at the System.ComponentModel.EventHandlerList
class:
namespace System.ComponentModel
{
public sealed class EventHandlerList : IDisposable
{
public EventHandlerList();
public Delegate this[object key] { get; set; }
public void AddHandler(object key, Delegate value);
public void Dispose();
public void RemoveHandler(object key, Delegate value);
}
}
Although this class may seem a lot like a dictionary meant for storing delegates, there are fundamental differences in the way it is built and used.
First, it is not as effective as a specially made dictionary. The documentation states:
This class uses a linear search algorithm to find entries in the list of delegates. A linear search algorithm is inefficient when working with a large number of entries. Therefore, when you have a large list, finding entries is slow.
We will talk a bit later about how this problem could be addressed.
Second, a most likely Use Case of this class does not imply anything like this:
EventHandlerList aList = new EventHandlerList();
object aKey = new object();
aList[aKey] = new EventHandler(MyHandler);
In fact, I don't even understand why the set
accessor is there at all. If you have thoughts on that, I would be happy to learn them. AddHandler
and RemoveHandler
are the two methods that are used most frequently. So, the new code for our event looks like this:
public event EventHandler MyEvent
{
add { Events.AddHandler(key, value); }
remove { Events.RemoveHandler(key, value); }
}
The above code is almost straightforward, except for one thing: what's the meaning of key
? And, this is where a third difference between a dictionary and an EventHandlerList
comes into play.
I'm surprised the documentation doesn't say anything about this difference. As you probably know, most associative containers in the .NET Framework (Hashtable
s, Dictionary
s, etc.) use both hashing and value equality check to determine if two objects are actually the same. Well, this is not the case for EventHandlerList
, which uses reference equality and no hashing. Armed with this knowledge, answer one simple question: what can be used as a key for a delegate enquiry?
The answer is: any object of a reference type whose lifetime is longer than or equal to the lifetime of the control. In other words, what must not be used as a key:
- Numerics (integers, floating-point numbers, booleans)
- Structs (and consequently enums)
If you're still interested about why you cannot use an int
, look at the prototype of the indexer:
public Delegate this[object key] { get; set; }
Now, what happens if we pass an int
to it? Right, boxing. The value-type entity is first wrapped into a container of a reference type. And then, this new reference-type container is passed into the reference equality operator (==
). Just compile and run the following console program:
static void Main(string[] args)
{
int a = 1;
int b = 1;
object aA = (object)a;
object aB = (object)b;
Console.WriteLine("a==b returns: {0}", a==b);
Console.WriteLine("aA==aB returns: {0}", aA==aB);
}
Just as the second Console.WriteLine
writes False, the indexer will always fail matching your integer against anything already present in the collection. Same ideas apply to structs and enums.
On the other hand, what may be used as a key:
- The
this
reference - Any instance field of a reference type
- Any static field of a reference type
But even here may be quirks. If your control exposes more than one event, you limit yourself to just one, whose key is this
. You simply won't be able to distinguish one event from another. Instance fields are absolutely OK to use, but remember the point why we actually started experimenting with an alternative approach to events: memory optimization. As soon as we are getting rid of a field, why the heck bring another one? So, I recommend that you use static fields every time you expose an event:
static readonly object ourKey = new object();
public event EventHandler MyEvent
{
add { Events.AddHandler(ourKey, value); }
remove { Events.RemoveHandler(ourKey, value); }
}
protected void OnMyEvent(EventArgs e)
{
EventHandler aH = Events[ourKey] as EventHandler;
if (aH != null)
aH(this, e);
}
You could use other types as well. But, object
is the most compact which is just what we want. Also, imagine using strings. Try to determine which of the two usage scenarios would work:
- Scenario 1:
protected void OnMyEvent(EventArgs e)
{
EventHandler aH = Events["MyEventKey"] as EventHandler;
if (aH != null)
aH(this, e);
}
- Scenario 2:
static readonly string ourKey = "MyEventKey";
protected void OnMyEvent(EventArgs e)
{
EventHandler aH = Events[ourKey] as EventHandler;
if (aH != null)
aH(this, e);
}
Even though strings are reference types, they are immutable. That is, every time you construct (or modify) a string, a new instance is created. It doesn't even matter if two strings contain the exact same sequence of characters: if they were constructed independently (or one was transformed to have the value of another), their references would point to different parts in memory. Just compile and run the following console application:
static void Main(string[] args)
{
string a = "foobar";
string b = "foo" + "bar";
string c = b;
object aA = (object)a;
object aB = (object)b;
object aC = (object)c;
Console.WriteLine("a==b returns: {0}", a==b);
Console.WriteLine("b==c returns: {0}", b==c);
Console.WriteLine("a==c returns: {0}", a==c);
Console.WriteLine("-----------------------------------------");
Console.WriteLine("aA==aB returns: {0}", aA==aB);
Console.WriteLine("aB==aC returns: {0}", aB==aC);
Console.WriteLine("aA==aC returns: {0}", aA==aC);
}
Surprisingly, all six lines print "True", making you think that I'm out of my mind. Honestly, I was first surprised at the results. However, after analysing the code, I came to a conclusion that it is the CLR optimization technique that causes such behavior! Basically, the CLR "sees" two exact same string literals and allocates only one block of memory for both. Just do some more sophisticated string processing and you'll lose the optimization effect:
string a = "foobar";
string b = ("0foo" + "bar").Substring(1);
...
If you have other thoughts on why aA==aB
returns true in the original example, please share them in your comments.
In the end, even though both usage scenarios may work, the point I'm trying to make is that it is still awkward to use strings as keys because you may misinterpret the strings' value- and reference-equality. The ideal solution is a static readonly object
field because:
- It occupies the least possible amount of memory
- You can only work with it through obtaining a reference
- Since it has no value, you cannot misinterpret value and reference equality
Another thing worth noting is the way in which you write the conventional OnMyEvent
method:
protected void OnMyEvent(EventArgs e)
{
EventHandler aH = Events[ourKey] as EventHandler;
if (aH != null)
aH(this, e);
}
If you just leave it as it is, you will lose the optimization benefit designed initially by the .NET Framework developers. The Events
property is created on demand when first referenced. To keep things consistent, let's add a condition:
protected void OnMyEvent(EventArgs e)
{
if ( !HasEvents() )
return;
EventHandler aH = Events[ourKey] as EventHandler;
if (aH != null)
aH(this, e);
}
This is almost it, except for my promise to give thoughts about the ineffective linear search algorithm in the EventHandlerList
class. First, if you are really-really concerned about performance and speed, you could stick back to the original field-based approach. Second, if your control exposes several events, you could store them in a separate EventHandlerList
which you can add yourself. You can also add a more optimized class instead. However, you will only notice the difference if your control really exposes a ton of events! That way, they won't mix with standard control events, thus reducing search time. Third, if your control fires an event in a loop manner, it may make perfect sense to completely get rid of the OnMyEvent
method and move the event search outside the loop:
private void FireMyEventForAll(EventArgs e)
{
if ( !HasEvents() )
return;
EventHandler aH = Events[ourKey] as EventHandler;
if (aH == null)
return;
for (int i = 0; i < NumIterations; i++)
aH(this, e);
}
Finally, the developers of the .NET Framework might improve their search algorithms in one of their future releases. A behavior similar to that of a HybridDictionary
would make perfect sense. The change is very unlikely to happen in the .NET Framework 4.0 release as its first beta uses the old, ineffective implementation.
History
- 26th May, 2009: Initial post.
- 8th June, 2009: A small update on the 4.0 release.