Domanda

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

Desidero essere in grado di identificare un evento utilizzando un albero delle espressioni, in modo da poter 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 due:

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

Soluzione

Modificare: COME Brusco ha sottolineato, la mia implementazione è piuttosto imperfetta in quanto può essere utilizzata solo all'interno della classe che dichiara l'evento :) Invece di "x => x.MyEvent" restituendo l'evento, stava restituendo il campo di supporto, a cui può accedere solo la classe.

Poiché le espressioni non possono contenere istruzioni di assegnazione, un'espressione modificata come "( x, h ) => x.MyEvent += h" non può essere utilizzato per recuperare l'evento, quindi sarebbe necessario utilizzare la riflessione.Un'implementazione corretta dovrebbe utilizzare la riflessione per recuperare il file EventInfo per l'evento (che, purtroppo, non sarà fortemente tipizzato).

Altrimenti, gli unici aggiornamenti che devono essere effettuati sono la memorizzazione del riflesso EventInfo, e utilizzare il file AddEventHandler/RemoveEventHandler metodi per registrare l'ascoltatore (invece del manuale Delegate Combine/Remove chiamate e insiemi di campi).Non dovrebbe essere necessario modificare il resto dell'implementazione.Buona fortuna :)


Nota: Si tratta di codice di qualità dimostrativa che fa diverse ipotesi sul formato della funzione di accesso.Il corretto controllo degli errori, la gestione degli eventi statici, ecc., sono lasciati 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, in modo da sfruttare l'inferenza del 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 );
}

Altri suggerimenti

Anch'io volevo farlo e ho escogitato un modo davvero interessante che realizza qualcosa come l'idea dell'Imperatore XLII.Tuttavia non utilizza gli alberi delle espressioni, come accennato in precedenza, ciò non può essere fatto poiché gli alberi delle espressioni 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 al gestore Aggiungi/Rimuovi su un oggetto proxy di brevissima durata.Il ruolo di questo oggetto proxy è semplicemente quello di richiamare qualche metodo e consentire l'intercettazione delle chiamate al metodo, a quel punto possiamo scoprire il nome dell'evento.

Sembra strano ma ecco il codice (che tra l'altro funziona SOLO se hai un file 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 an Action<T> delegato che verrà superato in qualche istanza 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 sta nel passare l'oggetto proxy al file Action<T> delegato fornito.

Dove abbiamo quanto segue CustomProxy<T> codice, a chi intercetta la chiamata += 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 ci resta è usarlo 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();
    }
}

Fatto questo vedrò questo output:

Event to watch = Changed
Event to watch = Changed

Un evento .NET non è in realtà un oggetto, è un endpoint rappresentato da due funzioni: una per l'aggiunta e l'altra per la rimozione di un gestore.Ecco perché il compilatore non ti permetterà di fare altro che += (che rappresenta l'aggiunta) o -= (che rappresenta la rimozione).

L'unico modo per fare riferimento a un evento per scopi di metaprogrammazione è come System.Reflection.EventInfo e la riflessione è probabilmente il modo migliore (se non l'unico modo) per ottenerne uno.

MODIFICARE:L'Imperatore XLII ha scritto del bellissimo codice che dovrebbe funzionare per i tuoi eventi, a condizione che tu li abbia dichiarati da C# semplicemente come

public event DelegateType EventName;

Questo perché C# crea due cose per te da quella dichiarazione:

  1. Un campo delegato privato per fungere da archiviazione di supporto per l'evento
  2. L'evento effettivo insieme al codice di implementazione che utilizza il delegato.

Convenientemente, entrambi hanno lo stesso nome.Ecco perché il codice di esempio funzionerà per i tuoi eventi.

Tuttavia, non si può fare affidamento su questo quando si utilizzano eventi implementati da altre librerie.In particolare, gli eventi in Windows Forms e in WPF non hanno un proprio spazio di archiviazione di backup, quindi il codice di esempio non funzionerà per loro.

Sebbene l'Imperatore XLII abbia già dato la risposta a questo, ho pensato che valesse la pena condividere la mia riscrittura.Purtroppo non è possibile ottenere l'evento tramite Expression Tree, sto utilizzando il nome dell'evento.

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

E l'utilizzo è:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top