通过Linq表达式树识别事件

时间:2008-08-29 20:15:51

标签: c# linq expression-trees

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

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

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

我的问题有两个:

  1. 编译器是否会窒息?如果是的话,有关如何防止这种情况的任何建议吗?
  2. 如何从构造函数中解析Expression对象以附加MyEventToWatch的{​​{1}}事件?

4 个答案:

答案 0 :(得分:4)

编辑:正如Curt指出的那样,我的实现存在相当大的缺陷,因为它只能在声明事件的类中使用:)而不是“{{1 “返回事件,它返回了支持字段,该字段只能由类访问。

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

否则,唯一需要进行的更新是存储反映的EventInfo,并使用EventInfo / AddEventHandler方法注册侦听器(而不是手动{{} 1}} RemoveEventHandler / Delegate调用和字段集)。其余的实现不需要改变。祝你好运:)


注意:这是一个演示质量的代码,可以对访问者的格式做出一些假设。正确的错误检查,静态事件的处理等,留给读者练习;)

Combine

用法与建议略有不同,以便利用类型推断:

Remove

答案 1 :(得分:3)

我也想这样做,而且我想出了一个非常酷的方式,就像Emperor XLII的想法一样。它不使用表达式树,如上所述,由于表达式树不允许使用+=-=,因此无法执行此操作。

然而,我们可以使用一个巧妙的技巧,我们使用.NET Remoting Proxy(或任何其他代理,如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

答案 2 :(得分:2)

.NET事件实际上不是一个对象,它是一个由两个函数表示的端点 - 一个用于添加,另一个用于删除处理程序。这就是为什么编译器不会让你做除+ =(代表添加)或 - =(代表删除)以外的任何事情。

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

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

public event DelegateType EventName;

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

  1. 用作支持的私有代理字段 存储事件
  2. 实际的事件 使用的实现代码 代表。
  3. 方便地,这两者具有相同的名称。这就是示例代码适用于您自己的事件的原因。

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

答案 3 :(得分:1)

虽然皇帝XLII已经给出了答案,但我认为值得分享我对此的重写。可悲的是,没有能力通过表达式树获取事件,我正在使用事件的名称。

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