Advances in .NET Event Handling

Introduction

In this article we shall discuss the means in which event handlers are executed in .NET applications. This is a very important topic, because most .NET applications are event driven and mistakes and misunderstandings in event handling may cause them to work incorrectly. As will be shown, the order of execution of event handlers may become a major issue in event driven applications, especially when event handlers change the state of the object that has raised the event or changes the contents of the event arguments that are shared among handlers. Due to these interactions, application behavior may become unexpected and unpredictable, leading to errors that are hard to trace when debugging the application.

How Events in .NET Get Raised and Handled

The events for a .NET object is nothing more than a list of delegates that can be associated with an event. Raising the event actually causes the .NET Framework to walk through the list of subscribed delegates and to invoke each one of them. This idea is very simple and very effective, but it has one serious side effect that must always be taken into account when designing events in applications. Invoking one event handler, in other words delegate subscribed to the event, may cause other events to be raised recursively. In that case, delegates subscribed to the latter event will be invoked before the remaining handlers of the first event are called.

This situation leads to a tree of delegates being invoked, the tree that is traversed depth-first and it may become very deep. What may become a problem here is that one deeply nested delegate is free to change the state of the object during its execution in such a way that execution of the next event handler on the upper level is affected by that change. This is not something to fear as long as every event handler precisely does its own purpose. In that case, interference among event handlers will be exactly what is required and expected by the application. But this execution model can create a wide vulnerability for mistakes and bad designs. In the following sections we will present several designs and emphasize their downsides, so that designers can avoid making some typical mistakes.

Issues with Event That Returns Value

In this section we will analyze events that have a return type other than void, as in the following class declaration:

class EventServer
{
    public delegate int EventDelegate(int x);
    public event EventDelegate Inform;
}

Event handlers suitable for this event are supposed to do whatever they do in response to an event being raised and then to return an appropriate value as a result. What becomes a problem with the design of events like this is that the event may have multiple handlers subscribed to it. Let's look at this example:

class EventServer
{
    public delegate int EventDelegate(int x);
    public event EventDelegate Inform;
    public void DoWork()
    {
        if (Inform != null)
            Console.WriteLine(Inform(5));
    }
}

class Program
{

    static int Handler1(int x) { return 1; }
    static int Handler2(int x) { return 2; }

    static void Main(string[] args)
    {
        EventServer srv = new EventServer();
        srv.Inform += new EventServer.EventDelegate(Handler1);
        srv.Inform += new EventServer.EventDelegate(Handler2);
        srv.DoWork();
    }
}

Event handlers are invoked in code by literally invoking the event name, as it is done in the DoWork method. What happens under the hood at that line is that all event handlers subscribed to the event are called one by one in the order of their subscriptions to the event. Actually, the event is implemented as a MulticastDelegate instance, that internally maintains an invocation list as new handlers are added to the event. This means that handlers will be invoked in order of their subscriptions, but that should not be taken for granted: in a general case, it is better to consider that order of invocations is not known in advance, simply because later changes in code might unintentionally change the order of subscriptions and thus change the overall application execution.

Instead of depending on such circumstances, it is better to design and code an application so that it does not depend on the order of handler invocations.

Now if we look at the DoWork method in the code above, the question is: what exactly is the return value of the event that will be printed out to the console? If there are multiple event handlers subscribed to the event, then there will also be multiple return values available. But only one will be printed to the output. What happens here is that only the last value returned will be assigned to the variable. This may be a major issue, because every event handler has something to say with its return value, but only one return value will be remembered and taken into account. To show this in operation, we have first subscribed Handler1 and then Handler2, that resulted in value 2 appearing at the output. When the order of subscriptions is inverted, so that Handler2 is subscribed first, then the modified code will print value 1 on the output. This shows that the overall return value depends on the order of subscriptions, that is certainly not a desirable behavior of the application.

The conclusion from this case is that events should never be designed to return a value other than void. Otherwise we are running into a risk that another piece of code might find it interesting to subscribe to the same event later, and thus unintentionally change the application behavior by supplementing a previous return value with its own return value, that might differ. A better way to deal with return values from events is to store them in event arguments object passed as the parameter to the event handlers. We can design event arguments to store multiple values and then let the object that has raised the event to decide what is a compound return value calculated out of all values returned by all event handlers invoked. We can also design event arguments objects to aggregate multiple return values and thus provide an already combined result to the caller. This technique is presented in the following section.

Modifying Event Arguments Object

In this section we will show one common technique in which event handlers may return values to the function that has raised the event. This is important because objects that raise events often need feedback from event handlers. We have already shown one technique to do that in a previous section, and that is to return a value from the event handling method. But we have also shown that this technique is not appropriate because multiple event handlers may interfere with each others in an unpredictable way.

Making event arguments object editable sounds like a more reasonable idea, because it does not define in advance how multiple feedbacks will be combined into one consistent value. For example, we might decide that only one event handler should process the event. In that case, we can create event arguments class that contains a Boolean flag called Handled with appropriate getter and setter. Whenever event handler finds that flag set to false, it is free to handle the event, if capable, but then it is required to set the Handled flag to true, so to prevent all further handlers that might be invoked downstream from repeating the operation. This is applicable, for example, when sending email from event handlers. If any event handler is able to send email, it should do so, but then it should set the Handled flag to true so that no other handler sends the same email afterwards. With this solution, the function that has raised the event also has information whether any event handler has performed the operation or not. This information is gathered by inspecting the Handled flag after the event has been raised. If it is still false, then it is obvious that none of the subscribed handlers has executed the request.

Subscribing an Auditor to Events

It is a common practice to trace events raised by an object. For example, we might ask for a given Windows Form, what is the precise sequence of Paint, Layout and SizeChanged events that have occurred in form's operation. This quite simple question comes to be a very difficult one in fact and here will be shown why.

Consider a class that raises two events: MethodInvoked when one method is invoked, and PropertyChanged when one property's value changes:

class EventServer
{

    public delegate void MethodInvokedDelegate();
    public delegate void PropertyChangedDelegate();
    public event MethodInvokedDelegate MethodInvoked;
    public event PropertyChangedDelegate PropertyChanged;

    public void DoWork()
    {
        if (MethodInvoked != null)
            MethodInvoked();
    }
    public int Value
    {
        get
        {
            return _value;
        }
        set
        {
            if (_value != value)
            {
                _value = value;
                if (PropertyChanged != null)
                    PropertyChanged();
            }
        }
    }

    private int _value;

}

Now suppose that we have an event handler for the MethodInvoked event. From that event handler we just wish to change the value of the property that in turn raises the PropertyChanged event. Here is the code for these operations:

static void MethodInvokedHandler(object sender)
{
    EventServer srv = (EventServer)sender;
    srv.Value += 2;
}

static void Main(string[] args)
{
    EventServer srv = new EventServer();
    srv.MethodInvoked += new EventServer.MethodInvokedDelegate(MethodInvokedHandler);
    srv.DoWork();
}

These objects, when the application is running, operate in a very predictable way and there should be nothing suspicious about the two events. But let's introduce a third object, that audits events on its own, and that is oblivious of presence of the first event handler:

class Auditor
{
    private void AuditMethodInvoked(object sender)
    {
        Console.WriteLine("MethodInvoked");
    }
    private void AuditPropertyChanged(object sender)
    {
        Console.WriteLine("PropertyChanged");
    }
    public Auditor(EventServer srv)
    {
        srv.MethodInvoked += new EventServer.MethodInvokedDelegate(AuditMethodInvoked);
        srv.PropertyChanged += new EventServer.PropertyChangedDelegate(AuditPropertyChanged);
    }
}

In this way we have simply allowed an auditor to subscribe to all events of the monitored object and to log their occurrences. But then one might be surprised to see that PropertyChanged event is logged before MethodInvoked event. Why is this so? The answer becomes clear if we carefully draw out the sequence of operations in this program:

  1. EventServer instantiated

  2. MethodInvokedHandler subscribed to MethodInvoked event

  3. Auditor instantiated
     

    • AuditMethodInvoked subscribed to MethodInvoked event (after MethodInvokedHandler in invocation list)
    • AuditPropertyChanged subscribed to PropertyChanged event
       
  4. DoWork invoked from code
     

    • DoWork raises MethodInvoked event that invokes subscribed handlers in order of their subscriptions

      ->MethodInvokedHandler invoked; makes change to property value, that causes recursive raising of PropertyChanged event

           ->AuditPropertyChanged handler invoked; prints "PropertyChanged" on output
       
    • MethodInvokedHandler completes execution; next handler in invocation list is invoked
    • AuditMethodInvoked handler invoked; prints "MethodInvoked" on output

Now it is obvious that an auditor's handler for PropertyChanged event executes before its MethodInvoked event handler. This is a simple consequence of the event execution model in .NET Framework.

One way to ensure that auditor is notified about events in their proper order is to ensure that auditor is the first one to subscribe to all monitored events in the system. In our example, that would be done like this:

EventServer srv = new EventServer();
Auditor a = new Auditor(srv);

srv.MethodInvoked += new EventServer.MethodInvokedDelegate(MethodInvokedHandler);
srv.DoWork();

Now everything works fine; the MethodInvoked event is logged first, followed by the PropertyChanged event. Unfortunately, this is rarely achievable in practice as we often do not control the code that instantiates child objects, as to subscribe to their events on first sight. But even if we can subscribe auditor before other handlers, the entire idea may fail if the application handles events in the form of overridden base class methods and a base class method is called at the end of the handler as in this example:

class CustomForm : Form
{
    protected override void OnSizeChanged(EventArgs e)
    {
        DoWork();
        base.OnSizeChanged(e);
    }
}

Such implementation can be treated as bad design, since base implementation should be called first, and only then functionality should be added. But this kind of code may interfere with the auditor, preventing it from observing events in their correct order. Hence this solution remains applicable in general case only when it is required to know that specific event has occurred, rather than to know that it has occurred before or after some other event.

Conclusion

In this article we have shown several concepts and solutions applied in event driven applications. Emphasis remains on suggestion to program and maintain event handlers carefully. If there is interference between handlers, then it should be managed by carefully applying designs that help avoid unintentional data corruption and similar problems.

This topic is covered further in the following article titled How to Change Order of Event Handlers Execution at Run Time (http://www.c-sharpcorner.com/UploadFile/b81385/8741/), that explores implementation of multicast delegates. Note that multicast delegates are the core implementation used by the .NET Framework to invoke subscribed event handlers, although they are normally not observable by the code that subscribes to an event. Please refer to that article for a deeper analysis of event implementation internals in .NET applications.

Up Next
    Ebook Download
    View all
    FileInfo in C#
    Read by 9.7k people
    Download Now!
    Learn
    View all