Identificare un evento tramite un albero di espressione Linq

c# expression-trees linq

Domanda

Il compilatore di solito soffoca quando un evento non appare accanto a += o a -= , quindi non sono sicuro che sia possibile.

Voglio essere in grado di identificare un evento utilizzando un albero delle espressioni, così posso creare un osservatore di eventi per un test. La sintassi sarebbe simile a questa:

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

Le mie domande sono doppie:

  1. Il compilatore si strozzerà? E se sì, qualche suggerimento su come prevenirlo?
  2. Come posso analizzare l'oggetto Expression dal costruttore per collegarlo all'evento MyEventToWatch di target ?

Risposta accettata

Modifica: come ha sottolineato Curt , la mia implementazione è piuttosto errata in quanto può essere utilizzata solo all'interno della classe che dichiara l'evento :) Invece di " x => x.MyEvent " che restituisce l'evento, stava ritornando il campo di supporto , che è accessibile solo dalla classe.

Poiché le espressioni non possono contenere istruzioni di assegnazione, non è possibile utilizzare un'espressione modificata come " ( x, h ) => x.MyEvent += h " per recuperare l'evento, pertanto è necessario utilizzare la riflessione. Un'implementazione corretta dovrebbe utilizzare la reflection per recuperare EventInfo per l'evento (che, sfortunatamente, non sarà fortemente digitato).

In caso contrario, gli unici aggiornamenti che devono essere effettuati sono per archiviare l' EventInfo riflesso e utilizzare i metodi AddEventHandler / RemoveEventHandler per registrare il listener (anziché il AddEventHandler Delegate Combine / Remove chiamate e insiemi di campi). Non è necessario modificare il resto dell'implementazione. In bocca al lupo :)


Nota: questo è un codice di qualità dimostrativa che formula diverse ipotesi sul formato dell'accessorio. Un corretto controllo degli errori, la gestione di eventi statici, ecc., È lasciato come esercizio al lettore;)

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

L'utilizzo è leggermente diverso da quello suggerito, al fine di sfruttare l'inferenza di tipo:

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

Risposta popolare

Anch'io volevo fare questo, e ho escogitato un modo piuttosto interessante che fa qualcosa come l'idea dell'Imperatore XLII. Tuttavia, non usa gli alberi di espressione, come detto non può essere fatto perché gli alberi di espressione non consentono l'uso di += o -= .

Possiamo tuttavia utilizzare un trucco accurato in cui utilizziamo .NET Remoting Proxy (o qualsiasi altro Proxy come LinFu o Castle DP) per intercettare una chiamata a Aggiungi / Rimuovi gestore su un oggetto proxy molto breve. Il ruolo di questo oggetto proxy è di avere semplicemente un metodo chiamato su di esso, e di permettere che le sue chiamate al metodo vengano intercettate, a quel punto possiamo scoprire il nome dell'evento.

Questo suona strano ma qui è il codice (che tra l'altro funziona SOLO se hai un MarshalByRefObject o un'interfaccia per l'oggetto proxy)

Supponiamo di avere la seguente interfaccia e classe

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

Quindi possiamo avere una classe molto semplice che si aspetta un delegato di Action<T> che riceverà una qualche istanza di T

Ecco il codice

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

Il trucco è passare l'oggetto proxy al delegato Action<T> fornito.

Dove abbiamo il seguente CustomProxy<T> , che intercetta la chiamata a += e -= sull'oggetto proxy

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

E poi tutto ciò che rimane è usare questo come segue

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

Facendo questo vedrò questo risultato:

Event to watch = Changed
Event to watch = Changed


Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché
Autorizzato sotto: CC-BY-SA with attribution
Non affiliato con Stack Overflow
È legale questo KB? Sì, impara il perché