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];
James RobinPosted Aug 17, 2011, 11:41 AM
It would be nice if the source code included the files in a working application. It is not clear how all this fits together.
Mike GoldPosted Aug 14, 2011, 12:46 AM
Interesting article Zoran. I imagine there are cases out there, although I personally never dealt with them, where you don't know the event or event handler signature at compile time. I liked the use of reflection and code injection to make it happen. Pretty cool stuff!