9

+=イベントが aまたは a の横に表示されない場合、コンパイラは通常チョークする-=ため、これが可能かどうかはわかりません。

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

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

4 に答える 4

4

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

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

それ以外の場合、行う必要がある唯一の更新は、反映された を保存し、 /メソッドをEventInfo使用してリスナーを登録することです (手動の/呼び出しとフィールド セットの代わりに)。残りの実装を変更する必要はありません。幸運を :)AddEventHandlerRemoveEventHandlerDelegate CombineRemove


注:これは、アクセサーの形式についていくつかの仮定を行うデモンストレーション用のコードです。適切なエラー チェック、静的イベントの処理などは、読者の課題として残されています ;)

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

私もこれをやりたかったのですが、皇帝 42 世のアイデアのようなことを行うかなりクールな方法を思いつきました。ただし、前述のように、式ツリーはまたはの使用を許可していないため、式ツリーは使用しませ+=-=

ただし、.NET Remoting Proxy (または LinFu や Castle DP などの他のプロキシ) を使用して、有効期間が非常に短いプロキシ オブジェクトで 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>提供されたデリゲートに渡すことです。

次のコードがある場合、誰がプロキシされたオブジェクトへ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 イベントは実際にはオブジェクトではなく、2 つの関数 (ハンドラーの追加用と削除用) で表されるエンドポイントです。そのため、コンパイラは += (追加を表す) または -= (削除を表す) 以外の操作を許可しません。

メタプログラミングの目的でイベントを参照する唯一の方法は、System.Reflection.EventInfo を使用することです。リフレクションは、イベントを取得するためのおそらく最良の方法 (唯一の方法ではないにしても) です。

編集: 皇帝 XLII は、C# から単に次のように宣言した場合、独自のイベントで機能するはずの美しいコードを記述しました。

public event DelegateType EventName;

これは、C# がその宣言から 2 つのものを作成するためです。

  1. イベントのバッキング ストレージとして機能するプライベート デリゲート フィールド
  2. 実際のイベントと、デリゲートを利用する実装コード。

便利なことに、これらは両方とも同じ名前です。これが、サンプル コードが独自のイベントで機能する理由です。

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

于 2008-08-30T18:54:49.783 に答える