Question

Le compilateur s'étouffe généralement lorsqu'un événement n'apparaît pas à côté d'un += ou un -=, donc je ne sais pas si cela est possible.

Je souhaite pouvoir identifier un événement à l'aide d'une arborescence d'expressions afin de pouvoir créer un observateur d'événements pour un test.La syntaxe ressemblerait à ceci :

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

Mes questions sont doubles :

  1. Le compilateur va-t-il s'étouffer ?Et si oui, des suggestions pour éviter cela ?
  2. Comment puis-je analyser l'objet Expression du constructeur afin de l'attacher au MyEventToWatch événement de target?
Était-ce utile?

La solution

Modifier: Comme Sec l'a souligné, mon implémentation est plutôt défectueuse dans la mesure où elle ne peut être utilisée qu'à partir de la classe qui déclare l'événement :) Au lieu de "x => x.MyEvent" en renvoyant l'événement, il renvoyait le champ de support, qui n'est accessible qu'à la classe.

Puisque les expressions ne peuvent pas contenir d'instructions d'affectation, une expression modifiée comme "( x, h ) => x.MyEvent += h" ne peut pas être utilisé pour récupérer l'événement, il faudra donc utiliser la réflexion à la place.Une implémentation correcte nécessiterait d'utiliser la réflexion pour récupérer le EventInfo pour l'événement (qui, malheureusement, ne sera pas fortement typé).

Sinon, les seules mises à jour à effectuer sont de stocker les données réfléchies. EventInfo, et utilisez le AddEventHandler/RemoveEventHandler méthodes pour enregistrer l'auditeur (au lieu du manuel Delegate Combine/Remove appels et ensembles de champs).Le reste de l’implémentation ne devrait pas avoir besoin d’être modifié.Bonne chance :)


Note: Il s'agit d'un code de qualité démonstration qui fait plusieurs hypothèses sur le format de l'accesseur.La vérification appropriée des erreurs, la gestion des événements statiques, etc., sont laissées en exercice au lecteur ;)

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'utilisation est légèrement différente de celle suggérée, afin de tirer parti de l'inférence de type :

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

Autres conseils

Moi aussi, je voulais faire ça, et j'ai trouvé une méthode plutôt sympa qui fait quelque chose comme l'idée de l'Empereur XLII.Cependant, il n'utilise pas d'arbres d'expression, comme mentionné, cela ne peut pas être fait car les arbres d'expression ne permettent pas l'utilisation de += ou -=.

Nous pouvons cependant utiliser une astuce intéressante en utilisant le proxy .NET Remoting (ou tout autre proxy tel que LinFu ou Castle DP) pour intercepter un appel au gestionnaire Ajouter/Supprimer sur un objet proxy de très courte durée.Le rôle de cet objet proxy est simplement de faire appeler une méthode et de permettre l'interception de ses appels de méthode, auquel cas nous pouvons connaître le nom de l'événement.

Cela semble bizarre mais voici le code (qui d'ailleurs ne fonctionne QUE si vous avez un MarshalByRefObject ou une interface pour l'objet proxy)

Supposons que nous ayons l'interface et la classe suivantes

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

Nous pouvons alors avoir une classe très simple qui attend un Action<T> délégué qui recevra une instance de T.

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

L'astuce consiste à transmettre l'objet mandaté au Action<T> délégué fourni.

Où nous avons ce qui suit CustomProxy<T> code, qui intercepte l'appel à += et -= sur l'objet mandaté

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

Et puis il ne nous reste plus qu'à l'utiliser comme suit

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

En faisant cela, je verrai cette sortie :

Event to watch = Changed
Event to watch = Changed

Un événement .NET n'est pas réellement un objet, c'est un point de terminaison représenté par deux fonctions : une pour ajouter et une pour supprimer un gestionnaire.C'est pourquoi le compilateur ne vous permettra pas de faire autre chose que += (qui représente l'ajout) ou -= (qui représente la suppression).

La seule façon de faire référence à un événement à des fins de métaprogrammation est de l'appeler System.Reflection.EventInfo, et la réflexion est probablement le meilleur moyen (sinon le seul) d'en obtenir un.

MODIFIER:L'Empereur XLII a écrit du code magnifique qui devrait fonctionner pour vos propres événements, à condition que vous les ayez déclarés simplement en C#

public event DelegateType EventName;

C'est parce que C# crée deux choses pour vous à partir de cette déclaration :

  1. Un champ de délégué privé pour servir de stockage de support pour l'événement
  2. L'événement réel ainsi que le code d'implémentation qui utilise le délégué.

Idéalement, les deux portent le même nom.C'est pourquoi l'exemple de code fonctionnera pour vos propres événements.

Cependant, vous ne pouvez pas compter sur cela lorsque vous utilisez des événements implémentés par d'autres bibliothèques.En particulier, les événements dans Windows Forms et dans WPF ne disposent pas de leur propre stockage de sauvegarde, donc l'exemple de code ne fonctionnera pas pour eux.

Bien que l’empereur XLII ait déjà donné la réponse à cette question, j’ai pensé qu’il valait la peine de partager ma réécriture à ce sujet.Malheureusement, aucune possibilité d'obtenir l'événement via l'arbre d'expression, j'utilise le nom de l'événement.

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

Et l'utilisation est :

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top