Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Tracing Events Raised by Any C# Object

0.00/5 (No votes)
19 Jul 2008 1  
Describes a class to trace events raised by any C# object, via .NET Reflection
EventTracingViaReflection-screenshot.PNG

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

Introduction

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.

Usage of the Tracer Class

To use the Tracer class, you need three things:

  1. An object to trace
  2. A Tracer instance
  3. 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.

Order In Which Events Occur - Does It Really Trace ALL Events?

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

The Tracer Class Implementation

There are two important aspects of the implementation of the Tracer class:

  1. Getting the events that the target object can raise, and
  2. Hooking them by adding event handlers

Getting the Events that the Target Object Offers

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

Adding an Event Handler

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.

Handling Other Kinds of Events

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 delegates, 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.

Other Implementation Details

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

The Complete Tracer Class Interface

namespace EventTracer
{
    public sealed class Tracer : IDisposable
    {
        public delegate void OnEventHandler( object sender,
                                             object target,
                                             string eventName,
                                             EventArgs e );

        // Create an event tracer on a particular target object, using a
        // a particular delegate to handle the traced events.
        public Tracer( object target, OnEventHandler handler );

        // Property: The type of the target object.
        public Type TheType { get; }

        // Property: The target object.
        public object TheTarget { get; }

        // Property: A collection of all the 
        // traceable events raised by the target object.
        public ReadOnlyCollection<string> EventNames { get; }

        // Predicate: Returns true iff the parameter is a valid event name.
        public bool IsValidEvent( string eventName );

        // Property: A collection of all the untraceable events 
        // raised by the target object.
        // These are the events that don't conform to the .NET Framework's event design
        // pattern.
        public ReadOnlyCollection<string> UntraceableEventNames { get; }

        // Property: The number of events currently hooked.
        public int EventsHookedCount { get; }

        // Predicate: Returns true iff the parameter names a hooked event.
        public bool IsHookedEvent( string eventName );

        // Action: Hook the named event of the target object.  Your event
        // handler will be called whenever the target object raises the event.
        // (It is harmless to hook an event that is already hooked.)
        public void HookEvent( string eventName );

        // Action: Hook all events raised by the target object.
        public void HookAllEvents( );

        // Action: Unhook the named event.  Your event handler will no longer
        // be called when the target object raises the event.  (It is harmless
        // to unhook an event that is not hooked.)
        public void UnhookEvent( string eventName );

        // Action: Unhook all events.
        public void UnhookAllEvents();

        public void Dispose();
    }
}

Related Work

Ending Remarks

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.

Article Revision History

  • 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
    • Original article

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here