Article and Code Update (16-07-2008)
I've updated the article and code based on reader comments—thank you very much for reading and voting! The original event tracer class would trace only events that followed the .NET Framework design pattern for events—that's most of the events you'll encounter but not all of the ones that C# allows. I've added a second event tracer class that will trace many of the rest of the events you might find. I've also improved the robustness of the class.
Contents
The events of any object can be traced, by use of a single class, through the use of .NET Reflection.
Have you tried to understand the patterns of raised events of a complicated object? I needed to work with a very capable, very full-featured grid control from a ISV's popular control collection. It wasn't very long before I was very puzzled trying to figure out which of the many events provided by the control were the ones I wanted, and in which order they were raised. I started to write individual event handlers for the events I was interested in, but there were so many I thought I'd be better off generating tracing event handlers for all the events using a nice editor macro. But it turned out there were over 260 events for this grid control, and they were declared at various levels of the class hierarchy. I looked for a better way.
.NET Reflection turned out to be a very easy way to get access to the definition of the class I was interested in—or any other class for that matter. It is a simple process to get all the events that a class declares, and then to attach handlers to any or all of the events.
This article presents the simple-but-general event hooking/tracing class I built, which is provided (with a demo) as a download.
To use the Tracer
class, you need three things:
- An object to trace
- A
Tracer
instance
- An event handler to get the trace events
The event handler must match the following delegate
:
public delegate void OnEventHandler( object sender,
object target,
string eventName,
EventArgs e );
The sender
and the EventArgs e
are the parameters to the event that was raised. The target
is the object being traced. (The target may be different from the sender if the event is a static
event.) And the eventName
is the name of the event, of course.
So, for example, a simple trace routine may look like this:
private void OnEvent( object sender,
object target,
string eventName,
EventArgs e )
{
string s = String.Format( "{0} - args {1} - sender {2} - target {3}",
eventName,
e.ToString( ),
sender ?? "null",
target ?? "null" );
System.Diagnostics.Trace.TraceInformation( s );
}
Now all that is left is to create an event Tracer
on your object of interest, and to hook the events:
EventTracer.Tracer tracer = new EventTracer.Tracer( targetObject, OnEvent );
tracer.HookAllEvents( );
Alternatively, you could just hook specific events by name:
tracer.UnhookAllEvents( );
tracer.HookEvent( "Click" );
tracer.HookEvent( "DoubleClick" );
The Tracer
class has methods for hooking a specific event, by name, or hooking all events, and likewise for unhooking events. It has a property that will return a collection of the names of all the events that can be raised by the target object. The events considered are all the public
events, instance or static
, that are defined by the target object's class, or superclasses.
It is probably the case that at least some of the events you hook with the tracer you're already handling in your application. In that case, does the tracer's event handler execute before or after your event handler? For events without user-specified accessors (add
and remove
) and where the class lets event itself call the handlers normally, the subscribing handlers are called in the order in which they are added to the event. If you subscribe to the event in designer-generated code but create the event Tracer
later, during a Form's Load
event, for example, then the call to your handler will occur before the call to the trace routine.
(If the class implements its own add
or remove
accessors for the event, or if it takes over calling the subscribed handlers itself (by calling GetInvocationList
on the event and processing the delegates returned) then the order may be different: it depends on the classes' code.)
Well, actually, no. The Tracer
class just described traces only those events which match the .NET Framework event design pattern. All .NET Framework event delegate
s take two parameters, the first is of type object
(and is the object that is raising the event), and the second is of type System.EventArgs
or a class derived from System.EventArgs
:
namespace System {
public delegate void EventHandler( object sender, EventArgs e );
}
The event's delegate
type must also return void
. The .NET Framework 2.0 introduced the generic delegate
type System.EventHandler<TEventArgs>
so you can easily declare standard events like so:
public class MyEventArgs : EventArgs {
...
}
public event EventHandler<MyEventArgs> MyEvent;
System.EventHandler<TEventArgs>
has a constraint that TEventArgs
derives from System.EventArgs
, so that's how it matches the .NET Framework event design pattern.
But even though all of the events in the .NET Framework meet this pattern, and thus can be hooked by the Tracer
, the C# language doesn't restrict events to this pattern. In fact, an event can be declared using any delegate
type. The Tracer
class checks each event exposed by the target object and only lets you hook the ones which match the pattern. Those events are listed in the collection returned by the EventNames
property. If the class has any non-traceable events, their names are returned by the UntraceableEvents
property.
I've implemented a second class, TracerEx
that can handle more event types, though still not all. It is very similar to Tracer
except that the event handler you pass to it must match a different delegate
type:
public delegate void OnEventHandler( object target,
string eventName,
object[] parameters );
When this delegate
is called on an event raised by the target object, it is given the target object, the event's name, and an array of the parameters of the event. (If it is in fact a .NET Framework standard event type, then parameters[0]
will be the sender and parameters[1]
will be the EventArgs
or class derived from EventArgs
.)
TracerEx
can trace any event that has up to 10 parameters, where each parameter is of a reference type, and is not passed as a ref
parameter or as an out
parameter.
There are two important aspects of the implementation of the Tracer
class:
- Getting the events that the target object can raise, and
- Hooking them by adding event handlers
Getting the events couldn't be easier. The Type
object for the target object, describing the target object's class, has a GetEvents
method. That method takes a set of flags that describe the events you're interested in, and it returns an array of System.Reflection.EventInfo
objects. Those objects (which inherit from System.Reflection.MethodInfo
) provide all that is necessary to hook an event: The event's name, and methods to add or remove event handler (a delegate
) to the event's list of handlers. (An EventInfo
contains lots of other information too, of course.)
Since we're interested in all of the public
events, static
or instance, declared by the target object's class or any of its superclasses, we call GetEvents
like this:
EventInfo[] events = m_target.GetType().GetEvents( BindingFlags.Instance |
BindingFlags.Static |
BindingFlags.Public |
BindingFlags.FlattenHierarchy );
A second call to GetEvents
'without' the flag BindingFlags.Instance
will return an array of only the static
events. (It is an interesting detail that a call to Type.FindMembers
specifying MemberTypes.Event
and BindingFlags.Static
returns 'all' events, static
'and' instance, instead of just the static
events. At least, this is the case in .NET 2.0. So, to get static
events only, use GetEvents
rather than FindMembers
.)
Now, to actually hook the event, one only needs to create a delegate
—the event handler—and call AddEventHandler
on the EventInfo
for the event. But what to use for the delegate
? The event itself, as raised by the target object, will only have the sender
and EventArgs e
parameters, as usual. But from that information, there is no way for the user to get the name of the event. (Unless the user wants to walk the stack.) So we want to pass the event name along with the event args. We'll need an object to hold that name.
Therefore, the Tracer
class has a private
class declared within it, EventProxy
. The purpose of EventProxy
is to hold the event name, and to have a method to use as an event handler for an arbitrary event. To hook an event of the target object, we create a new instance of EventProxy
, create a delegate
to its handler method, and attach that delegate
as the event handler of the target object's event.
Here's how that works, where this
is the Tracer
instance, m_target
is the target object whose events we're hooking, and eventInfo
is the EventInfo
for the event we're hooking:
EventProxy proxy = new EventProxy( this, m_target, eventInfo.Name );
MethodInfo handler = typeof( EventProxy ).GetMethod( "OnEvent" );
Delegate d = Delegate.CreateDelegate( eventInfo.EventHandlerType,
proxy,
handler );
eventInfo.AddEventHandler( m_target, d );
Note that when creating the delegate
, I'm passing in the event handler type of the event I'm hooking, eventInfo.EventHandlerType
, not the signature of EventProxy.OnEvent
. In order to add the delegate
to the event, it must be of the event's handler's type, which is why it is specified as the first argument to Delegate.CreateDelegate
, but that type might not match the signature of the EventProxy.OnEvent
which is the method the delegate
is for. Why does this work? Well, before .NET 2.0, it didn't work. Before .NET 2.0 when you made a delegate
from a method, the delegate
's signature and the method's signature had to match exactly. This was somewhat painful. For example, Martin Carolan created a class to do event tracing on arbitrary objects that worked in .NET 1.1 (see this CodeProject article). He had to generate code for each event he hooked, generating a method with a signature exactly matching the event.
In .NET 2.0, the CLR changed so that methods are matched to signatures with looser rules called contravariance, which allows a "more general" method to be made into a delegate
with a "more specific" signature. Because of this change, EventProxy.OnEvent
's signature...
public void OnEvent( object sender, EventArgs e );
... will match any event that implements the .NET Framework's standard design pattern for events.
In the actual implementation, the delegate
to EventProxy.OnEvent
is also saved in a dictionary keyed by the event's name, so that it can be used to unhook the event via the EventInfo.RemoveEventHandler
method.
I implemented class
TracerEx
to handle more kinds of events than just those that match the .NET Framework event design pattern. Contravariance is pretty nice when matching methods to
delegate
s, but it doesn't give you arbitrary wildcards. The number of parameters of the method and
delegate
must be the same, none of the
delegate
parameters can be value types, and none of the
delegate
parameters can be passed as
ref
parameters or
out
parameters. (These are limitations defined by the CLR.) To handle events like that, you need to generate code on the fly—and for two different approaches see articles and code by
Martin Carolan and
"Death_Child". I wanted to avoid that so I was willing to accept some restrictions on the kinds of events that
TracerEx
could handle. I implemented 11 event handlers, for events with zero to 10 parameters, where all of the event handlers accept parameters of type
object
:
public void OnEvent0( ) { ... }
public void OnEvent1( object p1 ) { ... }
public void OnEvent2( object p1, object p2 ) { ... }
public void OnEvent3( object p1, object p2, object p3 ) { ... }
...
public void OnEvent10( object p1, object p2, object p3, object p4 ,
object p5, object p6, object p7, object p8, object p9, object p10 ) { ... }
Then, I choose which of these event handlers to pass to Delegate.CreateDelegate
based on the number of parameters in the event's delegate
type.
Both Tracer
and TracerEx
need to check each of the target's events to figure out if they are traceable or not. Tracer
has a method called IsGoodNetFrameworkEvent
that gets the event's delegate
type from the event's EventInfo
, then gets the delegate
's Invoke
method's MethodInfo
. From that, it can check that the delegate
has a return type of void
, that it takes exactly two parameters, and that the two parameters are of the proper types. TracerEx
has a method called IsTraceableEvent
which gets the event delegate
's type Invoke
method's MethodInfo
and then checks each parameter to make sure it is not a value type and not passed as ref
or out
. See the code for the details.
The only other implementation detail that seems worth mentioning is that the Tracer
class derives from IDisposable
. (Also, of course, the TracerEx
class.) It isn't strictly necessary since Tracer
instances don't own unmanaged resources, but it does provide a reasonable way for the class to remember to unhook any events and disconnect itself from the target object.
The class Tracer
runs 250 lines of code (for a generous interpretation of "line of code" that includes braces standing on lines by themselves). But the key aspects are centered on just a few APIs provided by .NET Reflection: System.GetType
, Type.GetEvents
, Type.GetMethod
, EventInfo.AddEventHandler
, EventInfo.RemoveEventHandler
(and also System.CreateDelegate
, which strictly speaking is not part of Reflection).
namespace EventTracer
{
public sealed class Tracer : IDisposable
{
public delegate void OnEventHandler( object sender,
object target,
string eventName,
EventArgs e );
public Tracer( object target, OnEventHandler handler );
public Type TheType { get; }
public object TheTarget { get; }
public ReadOnlyCollection<string> EventNames { get; }
public bool IsValidEvent( string eventName );
public ReadOnlyCollection<string> UntraceableEventNames { get; }
public int EventsHookedCount { get; }
public bool IsHookedEvent( string eventName );
public void HookEvent( string eventName );
public void HookAllEvents( );
public void UnhookEvent( string eventName );
public void UnhookAllEvents();
public void Dispose();
}
}
This class is very useful, even though it is so simple. I've tested it on .NET 2.0 and .NET 3.0. Try it yourself—and be sure to let me know if you find any problems. In fact, let me know if you like or dislike this article—it's my first for CodeProject. (I appreciate the comments I've already received.) I thank Giorgi Dalakishvili for pointing out the article by Martin Carolan, which I was not aware of, and "Death_Child" for reminding me that the Tracer
only traces events matching the .NET Framework design pattern.
- 14-07-2008
- Added a second class that traces non-standard events
- Discussion of .NET 1.1 vs. .NET 2.0
delegate
signature matching
- Discussion of order in which events fire
- Reference to prior article
- 06-07-2008