Identifier un événement via un arbre d'expression Linq

c# expression-trees linq

Question

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

Je souhaite pouvoir identifier un événement à l'aide d'un arbre d'expression 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 sur la façon de prévenir cela?
  2. Comment analyser l'objet Expression du constructeur afin de l'attacher à l'événement MyEventToWatch de la target ?

Réponse acceptée

Edit: Comme Curt l’ a souligné, mon implémentation est plutôt imparfaite dans le sens où elle ne peut être utilisée que dans la classe qui déclare l’événement :) Au lieu de " x => x.MyEvent " renvoyant l’événement, elle x => x.MyEvent le champ de sauvegarde , qui n'est accessible que par la classe.

Étant donné que les expressions ne peuvent pas contenir d'instructions d'affectation, une expression modifiée telle que " ( x, h ) => x.MyEvent += h " ne peut pas être utilisée pour extraire l'événement. Par conséquent, la réflexion doit être utilisée. Une implémentation correcte devrait utiliser la réflexion pour extraire EventInfo pour l'événement (qui, malheureusement, ne sera pas fortement typé).

Dans le cas contraire, les seules mises à jour à effectuer consistent à stocker le EventInfo réfléchi et à utiliser les méthodes AddEventHandler / RemoveEventHandler pour enregistrer l'écouteur (au lieu du manuel Delegate Combine / Remove appels et ensembles de champs). Le reste de la mise en œuvre ne devrait pas avoir besoin d'être changé. Bonne chance :)


Remarque: Il s'agit d'un code de qualité démonstration qui suppose plusieurs hypothèses sur le format de l'accesseur. La vérification correcte des erreurs, la gestion des événements statiques, etc., sont laissées à l’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:

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

Réponse populaire

Moi aussi, je voulais faire cela, et je suis parvenu à une manière plutôt cool qui fait quelque chose comme l’idée de l’empereur XLII. Il n'utilise cependant pas les arbres d'expression, comme indiqué, cela ne peut pas être fait car les arbres d'expression ne permettent pas l'utilisation de += ou -= .

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

Cela semble étrange, mais voici le code (qui, en fait, ne fonctionne que si vous avez un MarshalByRefObject ou une interface pour l'objet MarshalByRefObject par proxy)

Supposons que nous avons 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);
    }
}

Ensuite, nous pouvons avoir une classe très simple qui attend un délégué Action<T> qui sera passé à une instance de T

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

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

Où nous avons le code CustomProxy<T> suivant, qui intercepte l'appel à += et -= sur l'objet traité par proxy

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

Et puis nous avons tout ce qui reste est d'utiliser ceci comme suit

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

En faisant cela, je vais voir cette sortie:

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



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Est-ce KB légal? Oui, apprenez pourquoi