The Problem
Using reflection, you can manipulate almost any aspect of any object in a completely dynamic manner. However, when implementing a .Net based automation or scripting software it becomes painfully obvious that event handlers can not be attached dynamically unless a function with the appropriate signature already exists, which is hardly dynamic at all. There are two solutions to this problem.
Solution: Delegate Contravariance
Delegate contravariance is the creation of a delegate where the target method has at least one argument whose type derives from the corresponding argument's type on the delegate type declaration. For example, examine the following C# code:
This allows you to create a method where all parameter types are object, and then use this method to handle any event where the parameters are all pass-by-value and reference-type.
C# version 2 supports this feature. Older versions and other .Net languages can achieve the same effect through reflection. The problem is that this solution requires a pre-defined handler for each given number of arguments, can not handle value-type parameters, and does not account for the possibility of pass-by-reference parameters (out and ref parameters in C#, ByRef parameters in VB). This solution can use a single pre-defined method to dynamically handle events from the vast majority if not all .Net Framework objects, but can only handle a very limited number of non-event delegates and non-standard events.
Solution: Reflection Emit
A more flexible solution to the problem would be to use Reflection.Emit to dynamically generate "lightweight" methods that can handle a given type of delegate. A lightweight dynamic method is a method created at runtime using Reflection.Emit. This type of method does not need to be tied to an assembly or type (although instance methods can still be invoked on lightweight methods not owned by any type), which means significantly less resources are used for dynamically generated code, hence the name.
Subscriber: The attached code sample defines a class named Subscriber. Subscriber "subscribes to" (handles) events and delegates, generating handlers as needed (not taking contravariance into account), re-routing subscribed events to a universal event handler to which it passes the arguments in the form of an object array. The universal event handler raises an event each time it handles a subscribed event so that outside classes can receive these events, regardless of the event's signature. The following is an explanation of the basic components of the Subscriber class.
CreateHandler Method: This is the heart of the operation. This helper function uses Reflection.Emit to create a dynamic method that loads all the arguments into an array of objects and passes this array onto the universal event handler (a manner of "thunking" if you like to think of it that way). This function accounts for out and ref parameters and works with either value-type or reference-type parameters. The limitations are that it, as implemented, does not allow out and ref parameters to be assigned to, and does not support events and delegates with return types.
Universal Event Handler: This function, which is invoked by all of the dynamic event handlers, raises the EventRaised event to broadcast subscribed events. It can also be overridden in a derived class to handle subscribed events.
EventRaised Event: This single event is raised every time any subscribed event is raised, for fully dynamic event handling.
Subscribe Method: This method has four overloads. Two subscribe to events and two subscribe to delegates. All overloads examine the delegate type, create a handler if one doesn't already exist, and either attaches the handler to an event or returns a multicast delegate with the universal event handler added to the invocation list.
Usage
To use the Subscriber class, first create an instance of it. To subscribe to events, pass the event source (the object that will raise the event you want to handle) and either the name of the event or an EventInfo that represents the event. To subscribe to a delegate, pass either an existing delegate or a System.Type that represents a delegate. A delegate will be returned with the universal event handler appended to the invocation list, so that the delegate will behave exactly as before with the exception that it will also broadcast its invocation through the Subscriber object.
To handle events, either handle the EventRaised event, or inherit the Subscriber class and override the OnEvent method, which is the universal event handler.
Events can also be simulated by explicitly invoking OnEvent.
Possible Improvements
Using reflection, you can manipulate almost any aspect of any object in a completely dynamic manner. However, when implementing a .Net based automation or scripting software it becomes painfully obvious that event handlers can not be attached dynamically unless a function with the appropriate signature already exists, which is hardly dynamic at all. There are two solutions to this problem.
Solution: Delegate Contravariance
Delegate contravariance is the creation of a delegate where the target method has at least one argument whose type derives from the corresponding argument's type on the delegate type declaration. For example, examine the following C# code:
C#:
void DelegateExample(object o1, object o2) {
MessageBox.Show(o1.ToString() + ", " + o2.ToString());
}
void CreateContravariantDelegate() {
// EventHandler is defined as: void EventHandler(object sender, e EventArgs)
this.Click += new EventHandler(this.DelegateExample);
// Although the signatures are not identical, we can still create
// the delegate because the fact that each argument in
// EventHandler is the same type or a derivative type of
// the corresponding argument in DelegateExample, the
// invocation is guaranteed to succeed.
// In this case, the "sender" parameter is the same type,
// and the "e" (event args) parameter is a derived type
// (System.EventArgs inherits System.Object).
}
C# version 2 supports this feature. Older versions and other .Net languages can achieve the same effect through reflection. The problem is that this solution requires a pre-defined handler for each given number of arguments, can not handle value-type parameters, and does not account for the possibility of pass-by-reference parameters (out and ref parameters in C#, ByRef parameters in VB). This solution can use a single pre-defined method to dynamically handle events from the vast majority if not all .Net Framework objects, but can only handle a very limited number of non-event delegates and non-standard events.
Solution: Reflection Emit
A more flexible solution to the problem would be to use Reflection.Emit to dynamically generate "lightweight" methods that can handle a given type of delegate. A lightweight dynamic method is a method created at runtime using Reflection.Emit. This type of method does not need to be tied to an assembly or type (although instance methods can still be invoked on lightweight methods not owned by any type), which means significantly less resources are used for dynamically generated code, hence the name.
Subscriber: The attached code sample defines a class named Subscriber. Subscriber "subscribes to" (handles) events and delegates, generating handlers as needed (not taking contravariance into account), re-routing subscribed events to a universal event handler to which it passes the arguments in the form of an object array. The universal event handler raises an event each time it handles a subscribed event so that outside classes can receive these events, regardless of the event's signature. The following is an explanation of the basic components of the Subscriber class.
CreateHandler Method: This is the heart of the operation. This helper function uses Reflection.Emit to create a dynamic method that loads all the arguments into an array of objects and passes this array onto the universal event handler (a manner of "thunking" if you like to think of it that way). This function accounts for out and ref parameters and works with either value-type or reference-type parameters. The limitations are that it, as implemented, does not allow out and ref parameters to be assigned to, and does not support events and delegates with return types.
Universal Event Handler: This function, which is invoked by all of the dynamic event handlers, raises the EventRaised event to broadcast subscribed events. It can also be overridden in a derived class to handle subscribed events.
EventRaised Event: This single event is raised every time any subscribed event is raised, for fully dynamic event handling.
Subscribe Method: This method has four overloads. Two subscribe to events and two subscribe to delegates. All overloads examine the delegate type, create a handler if one doesn't already exist, and either attaches the handler to an event or returns a multicast delegate with the universal event handler added to the invocation list.
Usage
To use the Subscriber class, first create an instance of it. To subscribe to events, pass the event source (the object that will raise the event you want to handle) and either the name of the event or an EventInfo that represents the event. To subscribe to a delegate, pass either an existing delegate or a System.Type that represents a delegate. A delegate will be returned with the universal event handler appended to the invocation list, so that the delegate will behave exactly as before with the exception that it will also broadcast its invocation through the Subscriber object.
To handle events, either handle the EventRaised event, or inherit the Subscriber class and override the OnEvent method, which is the universal event handler.
Events can also be simulated by explicitly invoking OnEvent.
Possible Improvements
- Dynamic methods could assign values back to parameters from the array after invoking the universal handler in order to support ref and out parameters.
- Support for return-values.
- Reduce the number of dynamic methods created by using a delegate contravariance mechanism with a small number of pre-defined handlers and using System.Object for all reference-type parameters in dynamic methods. This would require a re-write of the handler caching mechanism.
- Provide a mechanism to persist dynamic methods to a DLL so that they don't need to be created every time an application is run.