views:

485

answers:

2

The compiler usually chokes when an event doesn't appear beside a += or a -=, so I'm not sure if this is possible.

I want to be able to identify an event by using an Expression tree, so I can create an event watcher for a test. The syntax would look something like this:

using(var foo = new EventWatcher(target, x => x.MyEventToWatch)
{
  // act here
} // throws on Dispose() if MyEventToWatch hasn't fired

My questions are twofold:

  1. Will the compiler choke? And if so, any suggestions on how to prevent this?
  2. How can I parse the Expression object from the constructor in order to attach to the MyEventToWatch event of target?
+2  A: 

A .NET event isn't actually an object, it's an endpoint represented by two functions -- one for adding and one for removing a handler. That's why the compiler won't let you do anything other than += (which represents the add) or -= (which represents the remove).

The only way to refer to an event for metaprogramming purposes is as a System.Reflection.EventInfo, and reflection is probably the best way (if not the only way) to get ahold of one.

EDIT: Emperor XLII has written some beautiful code which should work for your own events, provided you've declared them from C# simply as

public event DelegateType EventName;

That's because C# creates two things for you from that declaration:

  1. A private delegate field to serve as the backing storage for the event
  2. The actual event along with implementation code that makes use of the delegate.

Conveniently, both of these have the same name. That's why the sample code will work for your own events.

However, you can't rely on this to be the case when using events implemented by other libraries. In particular, the events in Windows Forms and in WPF don't have their own backing storage, so the sample code will not work for them.

Curt Hagenlocher
+2  A: 

Edit: As Curt has pointed out, my implementation is rather flawed in that it can only be used from within the class that declares the event :) Instead of "x => x.MyEvent" returning the event, it was returning the backing field, which is only accessble by the class.

Since expressions cannot contain assignment statements, a modified expression like "( x, h ) => x.MyEvent += h" cannot be used to retrieve the event, so reflection would need to be used instead. A correct implementation would need to use reflection to retrieve the EventInfo for the event (which, unfortunatley, will not be strongly typed).

Otherwise, the only updates that need to be made are to store the reflected EventInfo, and use the AddEventHandler/RemoveEventHandler methods to register the listener (instead of the manual Delegate Combine/Remove calls and field sets). The rest of the implementation should not need to be changed. Good luck :)


Note: This is demonstration-quality code that makes several assumptions about the format of the accessor. Proper error checking, handling of static events, etc, is left as an exercise to the reader ;)

public sealed class EventWatcher : IDisposable {
  private readonly object target_;
  private readonly string eventName_;
  private readonly FieldInfo eventField_;
  private readonly Delegate listener_;
  private bool eventWasRaised_;

  public static EventWatcher Create<T>( T target, Expression<Func<T,Delegate>> accessor ) {
    return new EventWatcher( target, accessor );
  }

  private EventWatcher( object target, LambdaExpression accessor ) {
    this.target_ = target;

    // Retrieve event definition from expression.
    var eventAccessor = accessor.Body as MemberExpression;
    this.eventField_ = eventAccessor.Member as FieldInfo;
    this.eventName_ = this.eventField_.Name;

    // Create our event listener and add it to the declaring object's event field.
    this.listener_ = CreateEventListenerDelegate( this.eventField_.FieldType );
    var currentEventList = this.eventField_.GetValue( this.target_ ) as Delegate;
    var newEventList = Delegate.Combine( currentEventList, this.listener_ );
    this.eventField_.SetValue( this.target_, newEventList );
  }

  public void SetEventWasRaised( ) {
    this.eventWasRaised_ = true;
  }

  private Delegate CreateEventListenerDelegate( Type eventType ) {
    // Create the event listener's body, setting the 'eventWasRaised_' field.
    var setMethod = typeof( EventWatcher ).GetMethod( "SetEventWasRaised" );
    var body = Expression.Call( Expression.Constant( this ), setMethod );

    // Get the event delegate's parameters from its 'Invoke' method.
    var invokeMethod = eventType.GetMethod( "Invoke" );
    var parameters = invokeMethod.GetParameters( )
        .Select( ( p ) => Expression.Parameter( p.ParameterType, p.Name ) );

    // Create the listener.
    var listener = Expression.Lambda( eventType, body, parameters );
    return listener.Compile( );
  }

  void IDisposable.Dispose( ) {
    // Remove the event listener.
    var currentEventList = this.eventField_.GetValue( this.target_ ) as Delegate;
    var newEventList = Delegate.Remove( currentEventList, this.listener_ );
    this.eventField_.SetValue( this.target_, newEventList );

    // Ensure event was raised.
    if( !this.eventWasRaised_ )
      throw new InvalidOperationException( "Event was not raised: " + this.eventName_ );
  }
}

Usage is slightly different from that suggested, in order to take advantage of type inference:

try {
  using( EventWatcher.Create( o, x => x.MyEvent ) ) {
    //o.RaiseEvent( );  // Uncomment for test to succeed.
  }
  Console.WriteLine( "Event raised successfully" );
}
catch( InvalidOperationException ex ) {
  Console.WriteLine( ex.Message );
}
Emperor XLII