9

当事件没有出现在 a+=或 a旁边时,编译器通常会阻塞-=,所以我不确定这是否可能。

我希望能够通过使用表达式树来识别事件,因此我可以为测试创建事件观察器。语法看起来像这样:

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

我的问题是双重的:

  1. 编译器会窒息吗?如果是这样,关于如何防止这种情况的任何建议?
  2. 如何从构造函数解析 Expression 对象以附加到MyEventToWatch事件target
4

4 回答 4

4

编辑:正如Curt所指出的,我的实现存在相当大的缺陷,因为它只能在声明事件的类中使用 :) 它不是x => x.MyEvent返回事件,而是返回支持字段,该字段只能通过班上。

由于表达式不能包含赋值语句,修改后的表达式如“ ( x, h ) => x.MyEvent += h”不能用于检索事件,因此需要使用反射。正确的实现需要使用反射来检索EventInfo事件(不幸的是,它不会被强类型化)。

否则,唯一需要进行的更新是存储反射的EventInfo,并使用AddEventHandler/RemoveEventHandler方法注册监听器(而不是手动Delegate Combine/Remove调用和字段集)。实施的其余部分不需要更改。祝你好运 :)


注意:这是演示质量代码,它对访问器的格式做了几个假设。正确的错误检查、静态事件的处理等,留给读者作为练习;)

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

为了利用类型推断,用法与建议的略有不同:

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 );
}
于 2008-09-01T00:47:10.193 回答
3

我也想这样做,我想出了一个很酷的方法来做一些类似皇帝 XLII 的想法。但是它不使用表达式树,正如前面提到的,这不能完成,因为表达式树不允许使用+=or -=

然而,我们可以使用一个巧妙的技巧,使用 .NET 远程代理(或任何其他代理,如 LinFu 或 Castle DP)来拦截对生命周期很短的代理对象上的添加/删除处理程序的调用。这个代理对象的作用就是简单的在它上面调用一些方法,并允许它的方法调用被拦截,此时我们可以找出事件的名称。

这听起来很奇怪,但这里是代码(顺便说一句,只有在你有MarshalByRefObject代理对象的接口或接口时才有效)

假设我们有以下接口和类

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

然后我们可以有一个非常简单的类,它期望一个Action<T>委托将传递给T.

这是代码

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

诀窍是将代理对象传递给Action<T>提供的委托。

在我们有以下CustomProxy<T>代码的地方,谁拦截了+=-=代理对象的调用

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

然后我们剩下的就是按如下方式使用它

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

这样做我会看到这个输出:

Event to watch = Changed
Event to watch = Changed
于 2012-06-18T14:20:06.210 回答
2

.NET 事件实际上并不是一个对象,它是一个由两个函数表示的端点——一个用于添加处理程序,一个用于删除处理程序。这就是为什么编译器不会让您执行除 +=(表示添加)或 -=(表示删除)之外的任何操作。

为元编程目的引用事件的唯一方法是作为 System.Reflection.EventInfo,而反射可能是获取事件的最佳方法(如果不是唯一方法)。

编辑:Emperor XLII 编写了一些漂亮的代码,这些代码应该适用于您自己的事件,前提是您已将它们从 C# 简单地声明为

public event DelegateType EventName;

那是因为 C# 从该声明中为您创建了两件事:

  1. 用作事件后备存储的私有委托字段
  2. 实际事件以及使用委托的实现代码。

方便的是,它们都具有相同的名称。这就是示例代码适用于您自己的事件的原因。

但是,当使用其他库实现的事件时,您不能依赖这种情况。特别是,Windows 窗体和 WPF 中的事件没有自己的后备存储,因此示例代码不适用于它们。

于 2008-08-30T18:54:49.783 回答
1

虽然四十二皇帝已经给出了答案,但我认为值得分享我对此的重写。遗憾的是,无法通过表达式树获取事件,我使用的是事件的名称。

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

用法是:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
于 2011-04-02T04:02:28.920 回答