Identifizieren Sie ein Ereignis über einen Linq-Ausdrucksbaum

c# expression-trees linq

Frage

Der Compiler drosselt normalerweise, wenn ein Ereignis nicht neben einem += oder einem -= , also bin ich mir nicht sicher, ob das möglich ist.

Ich möchte ein Ereignis mithilfe eines Ausdrucksbaums identifizieren können, sodass ich einen Ereigniswatcher für einen Test erstellen kann. Die Syntax würde etwa so aussehen:

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

Meine Fragen sind zweifach:

  1. Wird der Compiler würgen? Und wenn ja, irgendwelche Vorschläge, wie dies verhindert werden kann?
  2. Wie kann ich das Expression-Objekt vom Konstruktor analysieren, um es an das MyEventToWatch Ereignis des target anzuhängen?

Akzeptierte Antwort

Bearbeiten: Wie Curt darauf hingewiesen hat, ist meine Implementierung x => x.MyEvent fehlerhaft, als sie nur innerhalb der Klasse verwendet werden kann, die das Ereignis deklariert :) Anstatt " x => x.MyEvent " das Ereignis zurückzugeben, gab es das Hintergrundfeld zurück , die nur von der Klasse zugreifbar ist.

Da Ausdrücke keine Zuweisungsanweisungen enthalten können, kann ein geänderter Ausdruck wie " ( x, h ) => x.MyEvent += h " nicht zum Abrufen des Ereignisses verwendet werden, sodass stattdessen eine Reflektion verwendet werden müsste. Eine korrekte Implementierung müsste Reflektion verwenden, um die EventInfo für das Ereignis EventInfo (das leider nicht stark typisiert wird).

Andernfalls müssen Sie nur die reflektierte EventInfo speichern und die AddEventHandler / RemoveEventHandler Methoden verwenden, um den Listener zu registrieren (anstelle der manuellen Delegate Combine / Remove Anrufen und RemoveEventHandler ). Der Rest der Implementierung sollte nicht geändert werden müssen. Viel Glück :)


Hinweis: Dies ist Code in Demonstrationsqualität, der verschiedene Annahmen über das Format des Accessors trifft. Korrekte Fehlerprüfung, Handhabung von statischen Ereignissen, etc. wird dem Leser als Übung überlassen;)

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

Die Verwendung unterscheidet sich geringfügig von der vorgeschlagenen, um die Vorteile der Typinferenz zu nutzen:

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

Beliebte Antwort

Ich wollte das auch tun, und ich habe mir einen ziemlich coolen Weg ausgedacht, der so etwas wie die Idee von Kaiser XLII macht. Es werden jedoch keine Ausdrucksbäume verwendet. Wie bereits erwähnt, kann dies nicht durchgeführt werden, da Ausdrucksbäume die Verwendung von += oder -= nicht zulassen.

Wir können jedoch einen netten Trick verwenden, bei dem wir .NET Remoting Proxy (oder einen anderen Proxy wie LinFu oder Castle DP) verwenden, um einen Aufruf des Add / Remove-Handlers auf einem sehr kurzlebigen Proxy-Objekt abzufangen. Die Rolle dieses Proxy-Objekts besteht darin, dass einfach eine Methode aufgerufen wird und seine Methodenaufrufe abgefangen werden können. An diesem Punkt können wir den Namen des Ereignisses herausfinden.

Das klingt seltsam, aber hier ist der Code (der übrigens nur funktioniert, wenn Sie ein MarshalByRefObject oder eine Schnittstelle für das Proxy-Objekt haben)

Angenommen, wir haben die folgende Schnittstelle und Klasse

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

Dann können wir eine sehr einfache Klasse haben, die einen Action<T> -Delegaten erwartet, der eine Instanz von T passiert.

Hier ist der Code

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

Der Trick besteht darin, das proxied-Objekt an den bereitgestellten Action<T> -Delegaten zu übergeben.

Wo wir den folgenden CustomProxy<T> -Code haben, der den Aufruf an += und -= auf dem Proxy-Objekt abfängt

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

Und dann müssen wir nur noch folgendes verwenden

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

Dadurch sehe ich diese Ausgabe:

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



Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum
Lizenziert unter: CC-BY-SA with attribution
Nicht verbunden mit Stack Overflow
Ist diese KB legal? Ja, lerne warum