Virtual Events in C#

Technorati Tags: Events

One of the things that the language designers considered when designing the C# language was the ability to notify external callers of certain events happening. To solve this problem, they (surprise surprise) introduced the event construct.

One of the oddities in the design however, comes in the form of virtual events. This is one of those design decisions that we recognize is something we would like to change, but as my colleague, Eric Lippert explains in a series of posts concerning breaking changes, we aren't able to fix everything that we would like to.

How virtual events work

So first off, lets quickly describe how virtual events work. Section 10.8.4 of the spec describes this for us:

A virtual event declaration specifies that the accessors of that event are virtual. The virtual modifier applies to both accessors of an event.
The accessors of an inherited virtual event can be overridden in a derived class by including an event declaration that specifies an override modifier. This is known as an overriding event declaration. An overriding event declaration does not declare a new event. Instead, it simply specializes the implementations of the accessors of an existing virtual event.

Notice that an overriding event does not declare a new event. Let's now quickly refresh field-like events and consider how they work in conjunction with virtual and overriding events. They are described in 10.8.1:

Within the program text of the class or struct that contains the declaration of an event, certain events can be used like fields. To be used in this way, an event must not be abstract or extern, and must not explicitly include event-accessor-declarations. Such an event can be used in any context that permits a field. The field contains a delegate (ยง15) which refers to the list of event handlers that have been added to the event. If no event handlers have been added, the field contains null.

Declaring a virtual field-like event then, will cause the compiler to generate a delegate field to back the event, and virtual accessors for the event. Declaring an overriding field-like event will not cause the compiler to generate a new backing field for the overriding event in the case of field-like events, but will cause it to generate overriding accessors.

A concrete example of virtual and overriding field-like events

Now, consider some parent class P which declares a virtual event, and some derived class D which overrides it. First, note that we have four combinations:

  1. P declares a field-like event, and D declares a field-like event
  2. P declares a field-like event, and D declares a user-defined event
  3. P declares a user-defined event, and D declares a field-like event
  4. P declares a user-defined event, and D declares a user-defined event

In case (1), P contains a delegate field backing the event, and two virtual accessors that add and remove from the delegate. D contains two overriding accessors, who add and remove from the delegate contained in P. Notice that for this to work, the delegate in P must be elevated from private to protected.

In case (2), P contains a delegate field and two accessors, and D contains the two user-defined overloaded accessors. D does not have access to the field contained in P.

In case (3), P contains the two virtual user-defined accessors, and D contains a delegate field, along with two overriding accessors that add and remove from the backing field.

In case (4), P contains the two virtual user-defined accessors, and D contains the two overriding user-defined accessors.

Here's the code for it:

 class P
{
    public virtual event EventHandler case1_event;
    public virtual event EventHandler case2_event;
    public virtual event EventHandler case3_event
    {
        add { }
        remove { }
    }
    public virtual event EventHandler case4_event
    {
        add { }
        remove { }
    }
}

class D : P
{
    // D has access to P.case1_event's backing field.
    public override event EventHandler case1_event;

    // D does not have access to P.case2_event's backing field.
    public override event EventHandler case2_event
    {
        add { }
        remove { }
    }

    // D has a backing field generated for case3_event, which is private
    public override event EventHandler case3_event;

    // This is just the typical virtual/override pattern.
    public override event EventHandler case4_event
    {
        add { }
        remove { }
    }
}

A bug in the compiler

The current C# compiler (in Visual Studio 2008 Beta2) has a bug when dealing with scenario (1) above. Consider the following scenario:

 class P
{
    public virtual event EventHandler myEvent;
    public void parentEventCall()
    {
        myEvent(this, null);
    }
}
class D : P
{
    public override event EventHandler myEvent;
    public void derivedEventCall()
    {
        myEvent(this, null);
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Create an instance of D, and create a P reference to it.
        D derived = new D();
        P parent = derived;

        // Hook a handler up through the derived and parent references.
        derived.myEvent += new EventHandler(derivedHandler);
        parent.myEvent += new EventHandler(parentHandler);

        // Fire both events.
        derived.derivedEventCall();
        parent.parentEventCall();

    }
    static void parentHandler(object sender, EventArgs e)
    {
        Console.WriteLine("parent handler");
    }

    static void derivedHandler(object sender, EventArgs e)
    {
        Console.WriteLine("derived handler");
    }
}

Now, we would expect this code to output the sequence of derived/parent/derived/parent handlers being fired, because in this scenario, we should have one backing field for both the virtual event and the overriding one, so both of the trigger calls should act upon the same event, and both of the handlers should be hooked onto the same event.

However, if you go ahead and paste this code into Visual Studio, compile it, then run it, you'll get a NullReferenceException being thrown. Debugging it will show us two thing:

Firstly, the compiler does not generate a protected backing field on the parent class P. Instead, it generates two private backing fields - one in P and one in D.

Secondly, when we execute both handler hookups, we'll see that it is the derived delegate field that gets both of the handlers hooked up to it.

When we step into parentEventCall then, we'll notice that the this pointer is of type P, and that the only visible backing field is P.myEvent, which is null. Attempting to trigger the delegate then throws us the NullReferenceException, as expected.

The work-around

The simple work-around for the issue is to use a virtual method for triggering the event as well. An easy to remember rule of thumb is that if you have a virtual event, have a virtual triggering method. If you override a virtual event, override its trigger method as well.

A quick argument for not-fixing this issue

There are two main reasons that prompted us to choose to not fix this issue.

The first is the common practice for declaring event trigger methods. Whenever you write a trigger method, you must do a null check - if you don't and someone calls your trigger method without ever adding a handler, you'll get a NullreferenceException. This means that anyone applying good coding practices will already be safeguarded from this erroneous exception being thrown.

However, fixing this issue will be a breaking change. Code that used to never execute because the backing field in the parent class was always null will now execute because the backing field is the same in both the parent and derived classes, and will have a value once a handler is added. This is undesirable.

Secondly, there really is no need to be in this scenario in the first place. Using a virtual field-like event in the parent class and not changing any of its behavior in the overriding derived class is not necessary. You can simply omit the override in the derived class to get the desired behavior.

kick it on DotNetKicks.com