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:
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.