题
当事件没有出现在事件旁边时,编译器通常会卡住。 +=
或一个 -=
, ,所以我不确定这是否可能。
我希望能够使用表达式树来识别事件,这样我就可以创建一个事件观察器来进行测试。语法看起来像这样:
using(var foo = new EventWatcher(target, x => x.MyEventToWatch) {
// act here
} // throws on Dispose() if MyEventToWatch hasn't fired
我的问题有两个:
- 编译器会卡住吗?如果是这样,关于如何防止这种情况有什么建议吗?
- 如何从构造函数中解析 Expression 对象以便附加到
MyEventToWatch
的事件target
?
解决方案
编辑: 作为 柯特 已经指出,我的实现相当有缺陷,因为它只能在声明事件的类中使用:)而不是“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 );
}
其他提示
我也想这样做,并且我想出了一个非常酷的方法,可以实现类似皇帝四十二的想法。但它不使用表达式树,正如前面提到的,这是无法完成的,因为表达式树不允许使用 +=
或者 -=
.
然而,我们可以使用一个巧妙的技巧,即使用 .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
.NET 事件实际上并不是一个对象,它是由两个函数表示的端点——一个用于添加处理程序,另一个用于删除处理程序。这就是为什么编译器不允许您执行 +=(代表添加)或 -=(代表删除)以外的任何操作。
出于元编程目的引用事件的唯一方法是作为 System.Reflection.EventInfo,而反射可能是获取事件的最佳方法(如果不是唯一方法)。
编辑:皇帝四十二编写了一些漂亮的代码,这些代码应该适用于您自己的事件,只要您从 C# 中将它们简单地声明为
public event DelegateType EventName;
这是因为 C# 通过该声明为您创建了两件事:
- 一个私人代表领域,作为活动的后备存储
- 实际事件以及使用代表的实施代码。
方便的是,这两者具有相同的名称。这就是示例代码适用于您自己的事件的原因。
但是,在使用其他库实现的事件时,您不能依赖这种情况。特别是,Windows 窗体和 WPF 中的事件没有自己的后备存储,因此示例代码不适用于它们。
虽然四十二皇帝已经给出了答案,但我认为值得分享我对此的重写。遗憾的是,无法通过表达式树获取事件,我正在使用事件的名称。
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();
}