Auditing Events in .NET Applications


Introduction

In previous article titled "How to Change Order of Event Handlers Execution at Run Time" (http://www.c-sharpcorner.com/UploadFile/b81385/8741/)  we have provided certain level of insight into implementation of event handling mechanism in .NET. In this article we are pushing further down into murky depths of .NET event driven application internals. Several concepts used to implement event subscriptions in practice (all strictly based on MSDN) will be explained and their functioning used to implement otherwise hardly implementable entity: event auditor.

Event auditing is a common task in programming and it boils down to getting informed about events (all or some) that have been raised and about their exact order of being raised. First goal can be relatively easily met by simply adding subscribers to all events of interest. But the second goal may become a source of trouble because entity which audits events would have to be the first one to subscribe to all events or otherwise recursive event raising might cause auditor to receive invocations in reversed order of events.

We have discussed this issue of reversed event handlers invocations in full detail in previous article titled How to Change Order of Event Handlers Execution at Run Time and we will not repeat the analysis in this article. Instead, solution presented there will be pushed further in this article so that it now covers wider set of classes that can be audited.

Solution presented in previous article is not complete because it covers only the simplest method in which event handlers are declared. But most of the classes that expose multiple events (like most of the GUI classes) do not use this method, but rather employ data structure to store handlers for all their events at one place. This method is explained in MSDN article How to: Handle Multiple Events Using Event Properties, so please refer to that text before proceeding.

Our previous solution solves only the case when invocation list of the event handler can be accessed directly by searching for the non-public field with name equal to event name. However, when class uses collection, like EventHandlerList class, then we cannot be certain which EventHandler delegate instance is connected with which event. This is because EventHandler instances are inserted into collection using System.Object keys which are only known to the class which implements the events.

In the following sections we will first examine the problem of accessing EventHandler instances for events, and then we will propose a solution which allows auditor to insert own event handlers at the beginning of every invocation list in the system, thus ensuring that all event notifications will be received by the auditor in their correct order.

Problem Statement

In this section we will try to find all EventHandler instances within a given object such that they are either declared directly, or stored in an EventHandlerList instance. We will not cover other endless possibilities, like using custom collections to store event handlers.

We will start with a custom form which publishes event named CustomEvent, in addition to all events inherited from System.Windows.Forms.Form class:

class MyForm: Form
{
    public event EventHandler CustomEvent;
    public void DoWork()
    {
        if (CustomEvent != null)
            CustomEvent(this, new EventArgs());
    }
}

This form can be instantiated and events handled in the application like this:

static void CustomEventHandler(object sender, EventArgs e)
{
    Console.WriteLine("CustomEvent handled.");
    ((Form)sender).Text = "Changed text";
}

static void FormTextChanged(object sender, EventArgs e)
{
    Console.WriteLine("TextChanged handled.");
}

static void FormShown(object sender, EventArgs e)
{
    Console.WriteLine("Shown handled.");
    ((MyForm)sender).DoWork();
}

static void Main(string[] args)
{

    MyForm form = new MyForm();

    form.Shown += new EventHandler(FormShown);
    form.TextChanged += new EventHandler(FormTextChanged);
    form.CustomEvent += new EventHandler(CustomEventHandler);

    form.ShowDialog();

}

This is simple case in which events are raised recursively: Shown event causes form's DoWork method to be invoked, which in turn raises CustomEvent, further causing form's text to be changed. However, since main function is the only subscriber in the system, all events will be noted on the output in their correct order. Output of this program looks like this:

Shown handled.
CustomEvent handled.
TextChanged handled.

Now we can try to build a new class, called MyFormAuditor, which subscribes the same set of events and simply prints them out to the console. Note that auditor should not change state of the system because that could cause changes in other events order and argument values. Auditing class could look like this:

class MyFormAuditor
{
    public MyFormAuditor(MyForm form)
    {
        form.CustomEvent += new EventHandler(CustomEvent);
        form.Shown += new EventHandler(Shown);
        form.TextChanged += new EventHandler(TextChanged);
    }
    void CustomEvent(object sender, EventArgs e) { Console.WriteLine("Audit CustomEvent."); }
    void Shown(object sender, EventArgs e) { Console.WriteLine("Audit Shown."); }
    void TextChanged(object sender, EventArgs e) { Console.WriteLine("Audit TextChanged."); }
}

This class can be used to subscribe all events of interest on any instance of MyForm class, but we must understand that such subscriptions typically come last in invocation lists, simply because objects are instantiated and linked before auditor comes into play. Now we can change the main function, so that auditor is added and linked with the form:

static void Main(string[] args)
{

    MyForm form = new MyForm();

    form.Shown += new EventHandler(FormShown);
    form.TextChanged += new EventHandler(FormTextChanged);
    form.CustomEvent += new EventHandler(CustomEventHandler);

    MyFormAuditor auditor = new MyFormAuditor(form);

    form.ShowDialog();

}

This is typical situation in practice – all objects are instantiated and all event handlers subscribed before auditor is created. Output now looks like this:
Shown handled.

CustomEvent handled.
TextChanged handled.
Audit TextChanged.
Audit CustomEvent.
Audit Shown.

As we can see, auditor has received event notifications in reverse order. This is consequence of recursive events, i.e. events being raised from inside other event handlers. In order to correct the issue, we will have to discover where event handlers are stored in the MyForm instance and then to insert new delegates at the front of their invocation lists.

Listing Event Handlers

First step in solving the auditing problem is to find EventHandler delegates contained in target object. As stated above, event handlers are either found instantiated on their own (default implementation in .NET), or as members of EventHandlerList instance, which is typical for most of the complex classes. For instance, all classes deriving from System.ComponentModel.Component class, and that means Windows forms and controls, use EventHandlerList to store subscribed event handlers.

In the first case, EventHandler instance is declared as a non-public field with name equal to corresponding event's name. In the second case, object contains non-public field named events, of type EventHandlerList, and it also declares number of static non-public fields of type System.Object, which are used as keys to the list. In both cases any particular EventHandler instance can be discovered by examining contained fields, and it is done by the following function:

static void ExamineSubscribers(object target)
{

    Hashtable objects = new Hashtable();
    List<EventHandlerList> handlerLists = new List<EventHandlerList>();
    BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
    Type t = target.GetType();

    while (t != null)
    {

        FieldInfo[] fields = t.GetFields(bf);

        for (int i = 0; i < fields.Length; i++)
        {
            if (fields[i].FieldType == typeof(System.Object) && fields[i].GetValue(target) != null)
                objects.Add(fields[i].GetValue(target), fields[i].Name);
            else if (fields[i].FieldType == typeof(EventHandlerList))
                handlerLists.Add((EventHandlerList)fields[i].GetValue(target));
            else if (fields[i].FieldType == typeof(EventHandler) && t.GetEvent(fields[i].Name, bf) != null)
            {
                EventHandler eh = (EventHandler)fields[i].GetValue(target);
                Delegate[] invocationList = eh.GetInvocationList();
                if (invocationList.Length > 0)
                    Console.WriteLine("{0} event handler(s) directly subscribed to event {1}", invocationList.Length, fields[i].Name);
            }
        }

        t = t.BaseType;

    }

    foreach (EventHandlerList handlerList in handlerLists)
        foreach (object key in objects.Keys)
            if (handlerList[key] != null)
            {
                Delegate dlg = handlerList[key];
                Delegate[] invocationList = dlg.GetInvocationList();
                if (invocationList.Length > 0)
                    Console.WriteLine("{0} event handler(s) mapped by key {1}", invocationList.Length, (string)objects[key]);
            }

}

This function iterates through type's derivation chain and for every type along it visits all fields. If field is of type System.Object, then it is possibly a key in some EventHandlerList associated with some event. If it is EventHandlerList then it might contain event handlers of interest. Otherwise, if it is EventHandler and has name equal to some event published by the target object, then it is treated as handler for that event. When this function is invoked on our MyForm instance, it prints out the following report:

2 event handler(s) directly subscribed to event CustomEvent
2 event handler(s) mapped by key EventText
2 event handler(s) mapped by key EVENT_SHOWN

This report shows that three events have two subscribers each, as expected. However, locations holding these EventHandler instances are different. In first case, CustomEvent is implemented as private EventHandler field with same name. TextChanged event handler is located in the list under key indicated by private static System.Object field named EventText, which is defined in the System.Windows.Forms.Control class. Last event, Shown, is also located in the same list, but under key indicated by private static System.Object field named EVENT_SHOWN, which is defined in the System.Windows.Forms.Form class.

These results show how complicate it is to locate each particular event handler in a given object. Locations and naming conventions are not uniform, not even within classes provided by the same vendor. Further on, we will address the auditing problem in the MyForm instance, as it closely resembles most of the practical cases.

Solution to Auditing Problem

In previous section we have seen that some event handlers are subscribed under key which are class-specific. Every class using EventHandlerList is free to create and name keys as it wants and there is no way to discover the naming in general case. One consequence of such state of matters is that we cannot say which EventHandler in the list handles which event.

In previous example we have identified objects named EventText and EVENT_SHOWN that are connected with existing handlers. From these names, and having source code which has subscribed to events, we know that first one marks TextChanged event and the second marks Shown event, both raised by the Form object. However, in general case, this could be all different – there is no way to say what was the naming convention obeyed by the coder.
So here is the major question: how do we audit events CustomEvent, TextChanged and Shown, all raised by the MyForm class? To do so, we want to remove existing subscriptions, then to subscribe the auditor to all three events, and then to return back all previous subscriptions. In that way, auditor will always be the first one to receive every event notification. However, in any practical case, we cannot determine which subscribers are subscribed to which event, simply because we do not know which contained EventHandler instance is bound to which event, due to arbitrary naming of the keys to the EventHandlerList.

Hard solution would be to somehow understand naming conventions for every particular class. But this way is not general and it is not easily extendible. Much simpler solution is to remove all existing event handlers, then add auditor's event handlers, and finally re-subscribe all originally subscribed handlers. This solution might look too heavy, especially if number of existing subscriptions is large, but it essentially solves the problem of unknown keys to known events.

As a matter of demonstration, we will implement this solution in the MyFormAuditor class. Complete source code of this class is as follows:

class MyFormAuditor
{
    public MyFormAuditor(MyForm form)
    {
               
        Queue<object> subscriptions = RemoveExistingSubscriptions(form);

        form.CustomEvent += new EventHandler(CustomEvent);
        form.Shown += new EventHandler(Shown);
        form.TextChanged += new EventHandler(TextChanged);

        RestoreOriginalSubscriptions(form, subscriptions);

    }
    void CustomEvent(object sender, EventArgs e) { Console.WriteLine("Audit CustomEvent."); }
    void Shown(object sender, EventArgs e) { Console.WriteLine("Audit Shown."); }
    void TextChanged(object sender, EventArgs e) { Console.WriteLine("Audit TextChanged."); }

    Queue<object> RemoveExistingSubscriptions(object target)
    {

        Queue<object> subscriptions = new Queue<object>();

        Hashtable objects = new Hashtable();
        List<EventHandlerList> handlerLists = new List<EventHandlerList>();
        BindingFlags bf = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static;
        Type t = target.GetType();
 
        while (t != null)
        {
 
            FieldInfo[] fields = t.GetFields(bf);

            for (int i = 0; i < fields.Length; i++)
            {
                if (fields[i].FieldType == typeof(System.Object) && fields[i].GetValue(target) != null)
                {
                    object value = fields[i].GetValue(target);
                    if (!objects.ContainsKey(value))
                        objects.Add(value, null);
                }
                else if (fields[i].FieldType == typeof(EventHandlerList))
                    handlerLists.Add((EventHandlerList)fields[i].GetValue(target));
                else if (fields[i].FieldType == typeof(EventHandler) && t.GetEvent(fields[i].Name, bf) != null)
                {

                    EventHandler eh = (EventHandler)fields[i].GetValue(target);
                    Delegate[] invocationList = eh.GetInvocationList();

                    Delegate dlg = (Delegate)eh;
                    for (int j = 0; j < invocationList.Length; j++)
                        dlg = Delegate.Remove(dlg, invocationList[j]);
                    fields[i].SetValue(target, dlg);

                    subscriptions.Enqueue(fields[i]);       // This means that EventHandler field indicated by fields[i]
                    subscriptions.Enqueue(invocationList); 
// should be subscribed by delegates listed in the invocation list

                }
            }

            t = t.BaseType;

        }

        foreach (EventHandlerList handlerList in handlerLists)
            foreach (object key in objects.Keys)
                if (handlerList[key] != null)
                {
                    Delegate dlg = handlerList[key];
                    Delegate[] invocationList = dlg.GetInvocationList();

                    for (int i = 0; i < invocationList.Length; i++)
                        handlerList.RemoveHandler(key, invocationList[i]);

                    subscriptions.Enqueue(handlerList);     // This means that in specified handler list under specified key
                    subscriptions.Enqueue(key);             // delegates listed in the invocation list should be subscribed
                    subscriptions.Enqueue(invocationList);

                }

        return subscriptions;

    }

    void RestoreOriginalSubscriptions(object target, Queue<object> subscriptions)
    {

        while (subscriptions.Count > 0)
        {

            if (subscriptions.Peek() is FieldInfo)
            {

                FieldInfo fi = (FieldInfo)subscriptions.Dequeue();
                Delegate[] invocationList = (Delegate[])subscriptions.Dequeue();

                Delegate dlg = (Delegate)fi.GetValue(target);
                for (int i = 0; i < invocationList.Length; i++)
                    dlg = Delegate.Combine(dlg, invocationList[i]);

                fi.SetValue(target, dlg);

            }
            else
            {

                EventHandlerList handlerList = (EventHandlerList)subscriptions.Dequeue();
                object key = subscriptions.Dequeue();
                Delegate[] invocationList = (Delegate[])subscriptions.Dequeue();

                for (int i = 0; i < invocationList.Length; i++)
                    handlerList.AddHandler(key, invocationList[i]);

            }

        }

    }

}

In this version, two methods have been added. First method, named RemoveExistingSubscriptions, iterates through the fields contained in the MyForm instance, and determines whether each field has anything to do with events exposed by the same instance. All subscriptions are carefully pushed to the queue and then removed. Second method, named RestoreOriginalSubscriptions, reads items from the queue and subscribes all previously removed handlers back to their original EventHandler delegates.

Now take a look at the MyFormAuditor's constructor, which is now changed. Before subscribing to the events of interest, it first invokes RemoveExistingSubscriptions method to clear the target object of all existing event subscriptions. Once done, event handlers for the three events are registered, and only after that RestoreOriginalSubscriptions method is invoked so that all original subscriptions can be returned into place, but after auditor's handlers in all invocation lists.

When program is started again, we can see that auditor has been first to know about all events raised by the MyForm instance. Output of the program now looks as expected:

Audit Shown.
Shown handled.
Audit CustomEvent.
CustomEvent handled.
Audit TextChanged.
TextChanged handled.

Conclusion

In this article we have demonstrated how application can force specific event handlers to be subscribed as first in event's invocation list, regardless of any other objects that may have subscribed to same events before.

Prerequisites for this method to be functional is that target object implements events only in default manner, or using EventHandlerList class in cooperation with System.Object fields that contain keys to the list. If these conditions are met, then specific event handlers may be registered as event auditors and order of their invocations is guaranteed to be the same as the order of events being raised by the target objects.

This text continues in the next article titled "General Event Handling in .NET" (http://www.c-sharpcorner.com/UploadFile/b81385/8798/), which uses all knowledge about CLR event handling obtained so far to design a completely general event handler capable to subscribe to events with signatures unknown at compile time.


Similar Articles