Question

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?
Was it helpful?

Solution

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 );
}

OTHER TIPS

I too wanted to do this, and I have come up with a pretty cool way that does something like Emperor XLII idea. It doesn't use Expression trees though, as mentioned this can't be done as Expression trees do not allow the use of += or -=.

We can however use a neat trick where we use .NET Remoting Proxy (or any other Proxy such as LinFu or Castle DP) to intercept a call to Add/Remove handler on a very short lived proxy object. The role of this proxy object is to simply have some method called on it, and to allow its method calls to be intercepted, at which point we can find out the name of the event.

This sounds weird but here is the code (which by the way ONLY works if you have a MarshalByRefObject or an interface for the proxied object)

Assume we have the following interface and class

public interface ISomeClassWithEvent {
    event EventHandler<EventArgs> Changed;
}


public class SomeClassWithEvent : ISomeClassWithEvent {
    public event EventHandler<EventArgs> Changed;

    protected virtual void OnChanged(EventArgs e) {
        if (Changed != null)
            Changed(this, e);
    }
}

Then we can have a very simply class that expects an Action<T> delegate that will get passed some instance of T.

Here is the code

public class EventWatcher<T> {
    public void WatchEvent(Action<T> eventToWatch) {
        CustomProxy<T> proxy = new CustomProxy<T>(InvocationType.Event);
        T tester = (T) proxy.GetTransparentProxy();
        eventToWatch(tester);

        Console.WriteLine(string.Format("Event to watch = {0}", proxy.Invocations.First()));
    }
}

The trick is to pass the proxied object to the Action<T> delegate provided.

Where we have the following CustomProxy<T> code, who intercepts the call to += and -= on the proxied object

public enum InvocationType { Event }

public class CustomProxy<T> : RealProxy {
    private List<string> invocations = new List<string>();
    private InvocationType invocationType;

    public CustomProxy(InvocationType invocationType) : base(typeof(T)) {
        this.invocations = new List<string>();
        this.invocationType = invocationType;
    }

    public List<string> Invocations {
        get { 
            return invocations; 
        }
    }

    [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.Infrastructure)]
    [DebuggerStepThrough]
    public override IMessage Invoke(IMessage msg) {
        String methodName = (String) msg.Properties["__MethodName"];
        Type[] parameterTypes = (Type[]) msg.Properties["__MethodSignature"];
        MethodBase method = typeof(T).GetMethod(methodName, parameterTypes);

        switch (invocationType) {
            case InvocationType.Event:
                invocations.Add(ReplaceAddRemovePrefixes(method.Name));
                break;
            // You could deal with other cases here if needed
        }

        IMethodCallMessage message = msg as IMethodCallMessage;
        Object response = null;
        ReturnMessage responseMessage = new ReturnMessage(response, null, 0, null, message);
        return responseMessage;
    }

    private string ReplaceAddRemovePrefixes(string method) {
        if (method.Contains("add_"))
            return method.Replace("add_","");
        if (method.Contains("remove_"))
            return method.Replace("remove_","");
        return method;
    }
}

And then we all that's left is to use this as follows

class Program {
    static void Main(string[] args) {
        EventWatcher<ISomeClassWithEvent> eventWatcher = new EventWatcher<ISomeClassWithEvent>();
        eventWatcher.WatchEvent(x => x.Changed += null);
        eventWatcher.WatchEvent(x => x.Changed -= null);
        Console.ReadLine();
    }
}

Doing this I will see this output:

Event to watch = Changed
Event to watch = Changed

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.

While Emperor XLII already gave the answer for this, I thought it was worth while to share my rewrite of this. Sadly, no ability to get the Event via Expression Tree, I'm using the name of the Event.

public sealed class EventWatcher : IDisposable {
     private readonly object _target;
     private readonly EventInfo _eventInfo;
     private readonly Delegate _listener;
     private bool _eventWasRaised;

     public static EventWatcher Create<T>(T target, string eventName) {
         EventInfo eventInfo = typeof(T).GetEvent(eventName);
         if (eventInfo == null)
            throw new ArgumentException("Event was not found.", eventName);
         return new EventWatcher(target, eventInfo);
     }

     private EventWatcher(object target, EventInfo eventInfo) {
         _target = target;
         _eventInfo = event;
         _listener = CreateEventDelegateForType(_eventInfo.EventHandlerType);
         _eventInfo.AddEventHandler(_target, _listener);
     }

     // SetEventWasRaised()
     // CreateEventDelegateForType

     void IDisposable.Dispose() {
         _eventInfo.RemoveEventHandler(_target, _listener);
         if (!_eventWasRaised)
            throw new InvalidOperationException("event was not raised.");
     }
}

And usage is:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top