General Event Handling in .NET


Introduction

In previous articles in this series we have gradually increased the insight into .NET event handling, starting with simplest cases and following towards more complex topics. Please refer to previous articles before reading the text that follows:

  • Advances in .NET Event Handling (http://www.c-sharpcorner.com/UploadFile/b81385/8715/)  - Provides an introduction for deeper event handling analysis which followed.
  • How to Change Order of Event Handlers Execution at Run Time (http://www.c-sharpcorner.com/UploadFile/b81385/8741/)  - Makes step further by explaining default event handling implementation in .NET and then uses that knowledge to interfere with built-in event handling mechanisms.
  • Auditing Events in .NET Applications (http://www.c-sharpcorner.com/UploadFile/b81385/8776/)  - Explains most general event handling implementation in .NET and then shows how to interfere with it to audit events in classes that are part of .NET framework or at least implement custom event handling according to .NET Framework guidelines.

This article continues the analysis by posing a very high goal - attempting to subscribe handlers to events which are completely unknown at compile time. As will be shown, CLR does not support such intentions and specific approach must be employed to solve the problem.

Problem Statement

When handling an event raised by a .NET object caller must know signature of the event handler at compile time or otherwise there is no regular way to subscribe to the event. This can be a serious problem in some applications where event handler signature is not known in advance. These applications are very rare in practice but every programmer sometimes encounters such a problem. There is no general solution to the problem available at compile time. Instead of finding approximate solutions (like allowing handling of events from only a limited set of supported handler signatures, like the one proposed by EventHandler delegate type) we can solve the problem at run time when particular event handler signature becomes known.

In this article we will present a fully functional solution to the problem of general event handling in .NET applications. Classes that we are proposing here are capable to subscribe to events no matter of their actual signature and to handle all events and pass them on in the form of an event with quite general signature as will be shown below. In this way, applications can handle that single event and then process its arguments to find all details provided by the original event which was unknown at compile time.

Solution

The first step in solving the problem is to provide event arguments that look something like this:

public class DynamicEventArgs: EventArgs
{
    public DynamicEventArgs(string eventName, string[] argNames, object[] argValues);
    public string EventName { get; }
    public string[] ArgNames { get; }
    public object[] ArgValues { get; }
}

This event arguments class provides original event name and two arrays of the same length - first one containing argument names of the original event and the second array containing actual values of the original event arguments. In this way we can present any event imaginable that was raised by any object in the system.

Now that we have suitable event arguments, we can define a class called DynamicEventsHandler which would bridge event notifications raised by the target object into events represented by DynamicEventArgs class. Here is the interface of the new class:

public class DynamicEventsHandler
{
    public event EventHandler<DynamicEventArgs> EventRaised;
    protected virtual void OnEventRaised(DynamicEventArgs e);
    public object TargetObject { get; set; }
}

This class provides one public property named TargetObject, which contains object owning events that are handled and passed further to the output through a public event called EventRaised. This event indicates that target object has raised some event. Off course DynamicEventArgs object passed to the EventRaised event handler would contain all relevant details about the original event. DynamicEventsHandler class will be used as base class for classes specialized in handling particular events, as will be shown shortly. Derived classes will actually handle all events of interest raised by the target object, and then invoke OnEventRaised method to raise the general event, having all arguments

Now we are ready to design the third class named DynamicEventsSubscriber, which will organize subscribing to events of interest published by target objects. Public interface of this class is richer than previous ones and looks like this:

public class DynamicEventsSubscriber: IDisposable
{
    public event EventHandler<TargetEventArgs> TargetEventRaised;
    public DynamicEventsSubscriber();
    public void AddMonitoredObject(object obj);
    public void AddMonitoredObjectExact(object obj, string eventName);
    public void AddMonitoredObjectWildcard(object obj, string pattern);
    public void AddMonitoredObjectWildcard(object obj, string pattern, char singleWildcard, char multipleWildcard);
    public void AddMonitoredObjectRegex(object obj, string regexPattern);
    public void AddMonitoredObject(object obj, IStringMatcher eventNameMatcher);
    public void RemoveMonitoredObject(object obj);
    public void RemoveAllMonitoredObjects();
    public bool AuditMode { get; set; }
}

This class exposes the TargetEventRaised event, which is enriched EventRaised event. Its delegate type relies on TargetEventArgs event arguments class, which is declared as follows:

public class TargetEventArgs: DynamicEventArgs
{
    public TargetEventArgs(string eventName, string[] argNames, object[] argValues, object target;
    public TargetEventArgs(DynamicEventArgs e, object target;
    public object Target { get; }
}

This event arguments class is the same as DynamicEventArgs class, only adding the target object reference. Anyone handling this event would then have complete information about the original event raised by any object.

Now we can get back to the DynamicEventsSubscriber class. Most important methods exposed by this class are AddMonitoredObject, RemoveMonitoredObject and RemoveAllMonitoredObjects. These methods are used to add and remove objects from the set of monitored objects. When an object is monitored, DynamicEventsSubscriber ensures that event handlers are subscribed to its events, so that TargetEventRaised event can be raised whenever any event is raised on a monitored object.

AddMonitoredObject method has five more flavors, each of them allowing the caller to specify string pattern used to match event names of interest, so that only a subset of events are handled, rather than all events exposed by the target object. AddMonitoredObjectExact would handle only the event which exactly matches the specified name. Overloaded AddMonitoredObjectWildcard method would subscribe to events with names that match specified string pattern which includes wildcards. AddMonitoredObjectRegex method subscribes to events with names that match specified regular expression pattern. Finally, most general method which overloads AddMonitoredObject receives IStringMatcher instance which defines event names to subscribe to in any way convenient. For more details on IStringMatcher, please refer to one of previous articles titled Configurable String Matching Solution.

Finally, there is a read-write Boolean property AuditMode, which determines whether target object's events should be subscribed to in auditing mode or not. In auditing mode, event handlers are subscribed in such a way that they come to the first position in every invocation list. Having this accomplished, listener can be sure to receive all events in their actual order, rather than reversed as it happens when events are recursively raised. For more details on auditing events, please refer to article titled General Method for Auditing Events in .NET Applications. Please refer to that article for list of conditions that must be met so that auditing can be performed correctly. In some rare situations, for which we hope will never occur in practice, setting AuditMode to true is not sufficient to guarantee correct order of events being handled, although no event notifications will be lost in the process.

And now we have come to the central point of this article, and that is the question how to subscribe to events of an arbitrary object when signatures of their corresponding event handlers are not known at compile time. The solution is not simple and this is how it looks. Using reflection on target object, we can extract all its events and read their signatures at run time. Then we can dynamically build a class which derives from DynamicEventsHandler class and adds methods that have exactly the same signatures as required event handlers. Further on, we can design a method body for each of these methods, so that it builds a DynamicEventArgs object from actual event handler's input arguments. Then, such dynamically built method would raise the EventRaised event by invoking OnEventRaised method of its base class. This is not easy to implement and here comes the key part of the solution.

Below is the CreateEventHandlerMethod method of the DynamicEventsSubscriber class which receives TypeBuilder and EventInfo instances. TypeBuilder corresponds with type which will contain the dynamically created method and EventInfo describes the event which would be handled by that method. Here is the listing:

private void CreateEventHandlerMethod(TypeBuilder tb, EventInfo evInfo)
{

    Type eventHandlerType = evInfo.EventHandlerType;
    MethodInfo dlgMethodInfo = eventHandlerType.GetMethod("Invoke");
    Type returnType = dlgMethodInfo.ReturnType;

    if (returnType == typeof(void))
    {   // Events returning non-void types are ignored
 
        ParameterInfo[] parameters = dlgMethodInfo.GetParameters();
        Type[] paramTypes = new Type[parameters.Length];

        for (int i = 0; i < parameters.Length; i++)
            paramTypes[i] = parameters[i].ParameterType;

        string methodName = evInfo.Name + "Handler";
        MethodAttributes ma = MethodAttributes.Public;
 
        MethodBuilder mb = tb.DefineMethod(methodName, ma, returnType, paramTypes);

        CreateEventHandlerMethodBody(evInfo, mb, parameters);

    }

}

New method is dynamically created according to the signature of the Invoke method of the EventInfo instance. Note that we are ignoring all events that expect handler to return a value (i.e. have non-void return type). This should not be understood as the limitation, because returning non-void values from event handlers is never a good idea anyway (at least, all events in .NET are actually implemented using multicast delegates and then there would be no single return value when event handlers are invoked from inside the class). You can find more on issues that may occur when returning a value from the event handler in previous article titled Advances in .NET Event Handling.

New method is created by simply iterating through the parameters of the Invoke method and then using their data types to create a public method which receives parameters with same types. Then we create a public method with name starting with the event name to avoid ambiguity, and with parameter types copied from the Invoke method. Now comes the hard part because we have to define method body, which must be done directly in intermediate language. So here is the listing of the corresponding method:

private void CreateEventHandlerMethodBody(EventInfo evInfo, MethodBuilder mb, ParameterInfo[] parameters)
{

    ILGenerator il = mb.GetILGenerator();
    LocalBuilder argNames = il.DeclareLocal(typeof(string[]));
    LocalBuilder argValues = il.DeclareLocal(typeof(object[]));
    LocalBuilder eventArgs = il.DeclareLocal(typeof(TargetEventArgs));

    // Allocate array of strings, total parameters.Length items
    il.Emit(OpCodes.Ldc_I4, parameters.Length);
    il.Emit(OpCodes.Newarr, typeof(string));
    il.Emit(OpCodes.Stloc, argNames.LocalIndex);

    // Allocate array of objects, total parameters.Length items
    il.Emit(OpCodes.Ldc_I4, parameters.Length);
    il.Emit(OpCodes.Newarr, typeof(object));
    il.Emit(OpCodes.Stloc, argValues.LocalIndex);

    for (int i = 0; i < parameters.Length; i++)
    {

        // (i+1)th argument should be copied to ith element of arrays
        // argument with index 0 is skipped because it represents this pointer

        il.Emit(OpCodes.Ldloc, argNames.LocalIndex);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldstr, parameters[i].Name);
        il.Emit(OpCodes.Stelem, typeof(string));

        il.Emit(OpCodes.Ldloc, argValues.LocalIndex);
        il.Emit(OpCodes.Ldc_I4, i);
        il.Emit(OpCodes.Ldarg, i + 1);   // skip argument 0, it represents this pointer
        il.Emit(OpCodes.Stelem, typeof(object));
    }

    // Now prepare stack for calling protected base.OnEventRaised(this, object[] args)

    // First push this pointer to stack
    il.Emit(OpCodes.Ldarg_0);

    // Now create DynamicEventArgs object on stack
    Type eventArgsType = typeof(DynamicEventArgs);
    ConstructorInfo ctor = eventArgsType.GetConstructor(new Type[] { typeof(string), typeof(string[]), typeof(object[]) });
    il.Emit(OpCodes.Ldstr, evInfo.Name);
    il.Emit(OpCodes.Ldloc, argNames.LocalIndex);
    il.Emit(OpCodes.Ldloc, argValues.LocalIndex);
    il.Emit(OpCodes.Newobj, ctor);

    // We are ready to call OnEventRaised now
    Type baseType = mb.DeclaringType.BaseType;
    MethodInfo onEventRaised = baseType.GetMethod("OnEventRaised", BindingFlags.NonPublic | BindingFlags.Instance);
    il.Emit(OpCodes.Call, onEventRaised);

    il.Emit(OpCodes.Ret);

}

In this method we are first allocating array of strings which will contain parameter names of the original event, and then we allocate array of objects which will contain actual parameter values received by the event handler. Then we fill these two arrays with actual values, based on EventInfo instance which describes the target event. Finally, we use these arrays to create a DynamicEventArgs object. Once preparations are finished, stack will contain this pointer and DynamicEventArgs object (in that order), which is sufficient to invoke base class's OnEventRaised method. What remains after that is just to return from current method to the caller.

There is one more thing left to explain about these classes, and that is when these dynamically created types get instantiated. It is obvious that these types need only be created when new data type is submitted for monitoring. If multiple instances of the same type are monitored, then there is obviously no need to create dynamic classes for each instance because they share the same set of public events. So DynamicEventsSubscriber class checks data type of each object added for monitoring, and only when previously unknown type is submitted, it would create new dynamic type which handles its events. Dynamic behavior of this class requires all such types to be loaded immediately when they are created.

Object model created at run time would then look something like this. There is a set of target objects freely created by the application, and DynamicEventsSubscriber instance will keep all these objects references in a collection. It also keeps references to all unique data types of all these objects. For each data type in this collection, there will be one dynamically created type which handles its events.

How to Use

In this section we will demonstrate how to use the DynamicEventsSubscriber class. We will define a form with one button on it and then we'll subscribe to all events of the form and its child control. Here is the complete listing of the test application:

using System;
using System.Windows.Forms;
using System.Drawing;
using SysExpand.Reflection;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
 
            Form form = new Form();

            Button btn = new Button();
            btn.Text = "Close";
            btn.AutoSize = true;
            btn.Location = new Point(10, 10);
            btn.Click += new EventHandler(ButtonClick);
            form.Controls.Add(btn);

            DynamicEventsSubscriber des = new DynamicEventsSubscriber();
            des.AuditMode = true;
            des.AddMonitoredObject(form);
            des.AddMonitoredObject(btn);

            des.TargetEventRaised += new EventHandler<TargetEventArgs>(TargetEventRaised);
            form.ShowDialog();

            Console.ReadLine();

        }

        static void TargetEventRaised(object sender, TargetEventArgs e)
        {
            Console.WriteLine("{0}.{1}", e.Target.GetType().Name, e.EventName);
        }

        static void ButtonClick(object sender, EventArgs e)
        {
            Button btn = (Button)sender;
            Form form = (Form)btn.Parent;
            form.Close();
        }
    }
}

As we can see, subscribing to all events raised by the form and its child control boils down to five lines of code. Output of this code is much longer and it lists all events that have occurred within the form since its creation until its shut down:

Form.Move
Form.LocationChanged
Form.HandleCreated
Form.Invalidated
Form.StyleChanged
Button.HandleCreated
Form.BindingContextChanged
Button.BindingContextChanged
Form.Load
Button.Invalidated
Form.Invalidated
Form.Layout
Button.ChangeUICues
Button.Invalidated
Form.ChangeUICues
Form.Invalidated
Form.VisibleChanged
Button.VisibleChanged
Button.Enter
Button.Invalidated
Button.GotFocus
Button.Invalidated
Form.Activated
Form.Shown
Form.Paint
Button.Paint
Button.KeyUp
Button.PreviewKeyDown
Button.Validating
Button.Validated
Button.Invalidated
Button.Paint
Button.Click
Form.Closing
Form.FormClosing
Form.Closed
Form.FormClosed
Form.VisibleChanged
Form.Deactivate
Button.LostFocus
Button.Invalidated
Form.HandleDestroyed
Button.HandleDestroyed

Since DynamicEventsHandler instance has been used in auditing mode (note the AuditMode = true statement), the output listing given above notes all events in their correct order of occurrences. This is very important in all applications that wish to track changes in the system at run time.

Conclusion

In this article we have introduced classes that can be used to subscribe to .NET events with signatures that are not known at compile time. Using these classes is very simple since all events raised by the monitored objects are re-packaged into a single, uniform event with general list of arguments. However, no information is lost in the process: object which is origin of the event is passed to the event handler, as well as names and values of all arguments originally passed by the event. Having all these information available, application can handle events in any way desirable. In addition to all this, we have seen that events can be successfully handled in the auditing mode, which guarantees that no two event notifications received will ever be reversed.

Please feel free to download the attached source code. It contains full definition of all classes described in this article.
 


Similar Articles