2008-08-29 7 views
7

+=または-=の横にイベントが表示されないときは、通常コンパイラーが突発するので、これが可能かどうかはわかりません。Linqの式ツリーを介してイベントを識別する

エクスプレッションツリーを使用してイベントを識別できるようにするため、テスト用のイベントウォッチャーを作成できます。構文は次のようになります。

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

私の質問2つあります:

  1. ウィルコンパイラチョーク?もしそうなら、これを防ぐ方法に関する提案はありますか?
  2. MyEventToWatchイベントに接続するために、コンストラクタからExpressionオブジェクトを解析するにはどうすればよいですか?target

答えて

4

編集:Curtとしてはそれがイベントを宣言するクラス内からのみを使用することができるという私の実装はかなり欠陥がある、指摘している:)代わりに「x => x.MyEvent」イベントを返すので、それは裏を返していましたフィールドはクラスによってのみアクセス可能です。

式に代入文を含めることはできないため、 "(x, h) => x.MyEvent += h"のような変更された式を使用してイベントを取得することはできません。代わりにリフレクションを使用する必要があります。正しい実装では、イベントのためにリフレクションを使用してEventInfoを取得する必要があります(残念ながら、厳密に型指定されません)。

は、そうでなければ、行う必要がある唯一の更新が反映EventInfoを格納し、(代わりに手動DelegateCombine/Removeコールとフィールドセットの)リスナーを登録するAddEventHandler/RemoveEventHandler方法を使用することです。残りの実装は変更する必要はありません。幸運:)


注:これは、アクセサのフォーマットについて、いくつかの仮定を行うデモンストレーション品質なコードです。適切なエラーチェック、静的なイベントなどの取り扱い、読者への課題として残されている;)

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

.NETイベントは実際にはオブジェクトではなく、追加するイベントとハンドラを削除するイベントという2つの関数で表されるエンドポイントです。だからこそ、コンパイラは+ =(addを表す)または - =(これはremoveを表す)以外のことをさせません。

メタプログラミングの目的でイベントを参照する唯一の方法は、System.Reflection.EventInfoとしてあり、おそらくリフレクションが(おそらく唯一の方法ではありませんが)1つを取得するための最良の方法です。

EDIT:天皇XLIIは、独自のイベントのために働く必要がありますいくつかの美しいコードを書いていますが、C#はその宣言からあなたのために二つのものを作成するためだそれだけで

public event DelegateType EventName; 

として、あなたは、C#からそれらを宣言した提供しました:イベント

  • のバッキング ストレージとして機能する

    1. プライベートデリゲートフィールドその 実装コードと一緒に実際のイベントデリゲートの を使用します。

    便宜上、両方とも同じ名前です。そのため、サンプルコードは独自のイベントで動作します。

    しかし、他のライブラリによって実装されたイベントを使用する場合は、これに頼ることはできません。特に、WindowsフォームとWPFのイベントには独自のバッキングストレージがないため、サンプルコードはそれらのためには機能しません。

  • 1

    皇帝XLIIはすでにこの答えを出していましたが、私はこれを書き直すのは価値があると思いました。残念ながら、Expression Treeを介してイベントを取得する機能はありません。イベントの名前を使用しています。

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

    私もこれをやってみたかった、と私は天皇XLIIのアイデアのような何かをかなりクールな方法が出ています。これは表現木を使用しませんが、表現木は+=または-=の使用を許可していないため、これはできません。

    ただし、.NET Remoting Proxy(またはLinFuやCastle DPなどの他のProxy)を使用して、非常に短い存続プロキシオブジェクトのAdd/Removeハンドラの呼び出しを傍受することができます。このプロキシオブジェクトの役割は、単に呼び出されたメソッドを持つことと、そのメソッド呼び出しをインターセプトできるようにすることです。この時点で、イベントの名前を知ることができます。

    これは奇妙に聞こえるが、ここで(あなたが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>デリゲートにプロキシオブジェクトを渡すことにあるコードです。私たちは、プロキシになるオブジェクト

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

    を次のようにこれを使用することで、次のCustomProxy<T>のコードを、持っている

    これを行うと、次の出力が表示されます:

    Event to watch = Changed 
    Event to watch = Changed 
    
    関連する問題