Click here to Skip to main content
Click here to Skip to main content

Exposing Events in ASP.NET Server Controls

, 26 May 2009
Rate this:
Please Sign up or sign in to vote.
Thoughts, approaches, and major pitfalls in exposing ASP.NET server control events.

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); //ery unlikely to be used

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 (Hashtables, Dictionarys, 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:

  1. Scenario 1:
  2. protected void OnMyEvent(EventArgs e)
    {
      EventHandler aH = Events["MyEventKey"] as EventHandler;
    
      if (aH != null)
        aH(this, e);
    }
  3. Scenario 2:
  4. 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 = "foo" + "bar";
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:

  1. It occupies the least possible amount of memory
  2. You can only work with it through obtaining a reference
  3. 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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kirill Osipov
Software Developer
Russian Federation Russian Federation
Kirill Osipov constantly lives in Petrozavodsk, Russia. He enjoys listening to music, playing the guitar, learning German, and, occasionally, drinking beer and whisky.
 
He is extremely passionate about software development and has recently realized he can be most creative in this particular field.
 
He also maintains his part time business at http://www.ko-sw.com.

Comments and Discussions

 
Questioncode sample ? Pinmemberranu.mandan17-Jun-09 2:13 
AnswerRe: code sample ? PinmemberKirill Osipov17-Jun-09 3:02 
GeneralRe: code sample ? Pinmemberranu mandan17-Jun-09 3:54 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140821.2 | Last Updated 26 May 2009
Article Copyright 2009 by Kirill Osipov
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid