Frage

Der Compiler verschluckt sich normalerweise, wenn ein Ereignis nicht neben einem erscheint += oder ein -=, Daher bin ich mir nicht sicher, ob dies möglich ist.

Ich möchte in der Lage sein, ein Ereignis mithilfe einer Ausdrucksbaumstruktur zu identifizieren, damit ich einen Ereignisbeobachter 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 zweierlei:

  1. Wird der Compiler ersticken?Und wenn ja, gibt es Vorschläge, wie man das verhindern kann?
  2. Wie kann ich das Expression-Objekt vom Konstruktor aus analysieren, um es an das anzuhängen? MyEventToWatch Ereignis von target?
War es hilfreich?

Lösung

Bearbeiten: Als Curt hat darauf hingewiesen, dass meine Implementierung insofern ziemlich fehlerhaft ist, als sie nur innerhalb der Klasse verwendet werden kann, die das Ereignis deklariert :) Anstelle von „x => x.MyEvent„Beim Zurückgeben des Ereignisses wurde das Hintergrundfeld zurückgegeben, auf das nur die Klasse zugreifen kann.

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

Andernfalls besteht die einzige Aktualisierung, die vorgenommen werden muss, darin, die reflektierten Daten zu speichern EventInfo, und verwenden Sie die AddEventHandler/RemoveEventHandler Methoden zum Registrieren des Listeners (anstelle des Handbuchs). Delegate Combine/Remove Aufrufe und Feldsätze).Der Rest der Implementierung sollte nicht geändert werden müssen.Viel Glück :)


Notiz: Hierbei handelt es sich um Code in Demonstrationsqualität, der mehrere Annahmen über das Format des Accessors trifft.Die ordnungsgemäße Fehlerprüfung, der Umgang mit statischen Ereignissen usw. bleiben 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 Typinferenz zu nutzen:

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

Andere Tipps

Ich wollte das auch machen und habe mir eine ziemlich coole Möglichkeit ausgedacht, die so etwas wie die Idee von Kaiser XLII umsetzt.Es werden jedoch keine Ausdrucksbäume verwendet. Wie bereits erwähnt, ist dies nicht möglich, da Ausdrucksbäume die Verwendung von nicht zulassen += oder -=.

Wir können jedoch einen netten Trick anwenden, bei dem wir .NET Remoting Proxy (oder einen anderen Proxy wie LinFu oder Castle DP) verwenden, um einen Aufruf des Add/Remove-Handlers für ein sehr kurzlebiges Proxy-Objekt abzufangen.Die Rolle dieses Proxy-Objekts besteht einfach darin, eine Methode aufzurufen und das Abfangen seiner Methodenaufrufe zu ermöglichen. 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 einen haben MarshalByRefObject oder eine Schnittstelle für das Proxy-Objekt)

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 eine erwartet Action<T> Delegierter, dem eine Instanz von übergeben wird T.

Hier ist der Code

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

Der Trick besteht darin, das Proxy-Objekt an zu übergeben Action<T> Delegierter zur Verfügung gestellt.

Wo wir Folgendes haben CustomProxy<T> Code, der den Anruf abfängt += Und -= auf dem Proxy-Objekt

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

Und dann bleibt uns nur noch, dies wie folgt zu verwenden

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

Wenn ich das mache, sehe ich diese Ausgabe:

Event to watch = Changed
Event to watch = Changed

Ein .NET-Ereignis ist eigentlich kein Objekt, sondern ein Endpunkt, der durch zwei Funktionen dargestellt wird – eine zum Hinzufügen und eine zum Entfernen eines Handlers.Deshalb erlaubt Ihnen der Compiler nichts anderes als += (was das Hinzufügen darstellt) oder -= (was das Entfernen darstellt).

Die einzige Möglichkeit, auf ein Ereignis für Metaprogrammierungszwecke zu verweisen, ist als System.Reflection.EventInfo, und Reflexion ist wahrscheinlich der beste Weg (wenn nicht der einzige), um an eines zu gelangen.

BEARBEITEN:Emperor XLII hat einen schönen Code geschrieben, der für Ihre eigenen Ereignisse funktionieren sollte, vorausgesetzt, Sie haben sie in C# einfach als deklariert

public event DelegateType EventName;

Das liegt daran, dass C# aus dieser Deklaration zwei Dinge für Sie erstellt:

  1. Ein privates Delegierterfeld, das als Backing -Speicher für die Veranstaltung dient
  2. Das tatsächliche Ereignis zusammen mit Implementierungscode, der den Delegierten nutzt.

Praktischerweise haben beide den gleichen Namen.Aus diesem Grund funktioniert der Beispielcode auch für Ihre eigenen Veranstaltungen.

Sie können sich jedoch nicht darauf verlassen, dass dies der Fall ist, wenn Sie Ereignisse verwenden, die von anderen Bibliotheken implementiert wurden.Insbesondere verfügen die Ereignisse in Windows Forms und in WPF nicht über einen eigenen Sicherungsspeicher, sodass der Beispielcode für sie nicht funktioniert.

Während Kaiser XLII bereits die Antwort darauf gab, hielt ich es für lohnenswert, meine Neufassung davon mitzuteilen.Leider gibt es keine Möglichkeit, das Ereignis über den Ausdrucksbaum abzurufen. Ich verwende den Namen des Ereignisses.

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

Und die Verwendung ist:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top