Pregunta

El compilador normalmente se atasca cuando un evento no aparece al lado de un += o un -=, así que no estoy seguro de si esto es posible.

Quiero poder identificar un evento mediante el uso de un árbol de expresión, para poder crear un observador de eventos para una prueba.La sintaxis sería algo como esto:

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

Mis preguntas son dobles:

  1. ¿Se ahogará el compilador?Y si es así, ¿alguna sugerencia sobre cómo prevenirlo?
  2. ¿Cómo puedo analizar el objeto Expresión del constructor para adjuntarlo al MyEventToWatch evento de target?
¿Fue útil?

Solución

Editar: Como Brusco Como ha señalado, mi implementación es bastante defectuosa porque solo se puede usar desde dentro de la clase que declara el evento :) En lugar de "x => x.MyEvent" Al devolver el evento, se devolvía el campo de respaldo, al que solo puede acceder la clase.

Dado que las expresiones no pueden contener declaraciones de asignación, una expresión modificada como "( x, h ) => x.MyEvent += h" no se puede utilizar para recuperar el evento, por lo que sería necesario utilizar la reflexión en su lugar.Una implementación correcta necesitaría utilizar la reflexión para recuperar el EventInfo para el evento (que, desafortunadamente, no estará fuertemente tipado).

De lo contrario, las únicas actualizaciones que deben realizarse son almacenar los datos reflejados. EventInfo, y utilizar el AddEventHandler/RemoveEventHandler métodos para registrar al oyente (en lugar del manual Delegate Combine/Remove llamadas y conjuntos de campo).No debería ser necesario cambiar el resto de la implementación.Buena suerte :)


Nota: Este es un código con calidad de demostración que hace varias suposiciones sobre el formato del descriptor de acceso.La verificación adecuada de errores, el manejo de eventos estáticos, etc., se deja como ejercicio para el lector;)

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

El uso es ligeramente diferente al sugerido para aprovechar la inferencia de tipos:

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

Otros consejos

Yo también quería hacer esto, y se me ocurrió una manera bastante interesante que hace algo parecido a la idea del Emperador XLII.Sin embargo, no utiliza árboles de expresión, como se mencionó, esto no se puede hacer ya que los árboles de expresión no permiten el uso de += o -=.

Sin embargo, podemos usar un truco ingenioso donde usamos .NET Remoting Proxy (o cualquier otro Proxy como LinFu o Castle DP) para interceptar una llamada a Agregar o quitar controlador en un objeto proxy de muy corta duración.La función de este objeto proxy es simplemente llamar a algún método y permitir que sus llamadas a métodos sean interceptadas, momento en el que podemos averiguar el nombre del evento.

Esto suena raro pero aquí está el código (que por cierto SÓLO funciona si tienes un MarshalByRefObject o una interfaz para el objeto proxy)

Supongamos que tenemos la siguiente interfaz y clase.

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

Entonces podemos tener una clase muy simple que espera una Action<T> delegado que pasará alguna instancia de T.

Aquí está el código

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

El truco consiste en pasar el objeto proxy al Action<T> delegado proporcionado.

Donde tenemos lo siguiente CustomProxy<T> código, quién intercepta la llamada a += y -= en el objeto 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;
    }
}

Y luego todo lo que queda es usar esto de la siguiente manera

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

Al hacer esto, veré este resultado:

Event to watch = Changed
Event to watch = Changed

Un evento .NET no es en realidad un objeto, es un punto final representado por dos funciones: una para agregar y otra para eliminar un controlador.Es por eso que el compilador no le permitirá hacer nada más que += (que representa la adición) o -= (que representa la eliminación).

La única forma de hacer referencia a un evento con fines de metaprogramación es como System.Reflection.EventInfo, y la reflexión es probablemente la mejor manera (si no la única) de conseguir uno.

EDITAR:Emperor XLII ha escrito un hermoso código que debería funcionar para tus propios eventos, siempre que los hayas declarado desde C# simplemente como

public event DelegateType EventName;

Esto se debe a que C# crea dos cosas a partir de esa declaración:

  1. Un campo de delegado privado para servir como respaldo Almacenamiento para el evento
  2. El evento real, junto con código de implementación que hace uso de del delegado.

Convenientemente, ambos tienen el mismo nombre.Es por eso que el código de muestra funcionará para sus propios eventos.

Sin embargo, no puede confiar en que este sea el caso cuando utilice eventos implementados por otras bibliotecas.En particular, los eventos en Windows Forms y WPF no tienen su propio almacenamiento de respaldo, por lo que el código de muestra no funcionará para ellos.

Si bien el Emperador XLII ya dio la respuesta a esto, pensé que valía la pena compartir mi reescritura de esto.Lamentablemente, no es posible obtener el evento a través del árbol de expresiones; estoy usando el nombre del 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.");
     }
}

Y el uso es:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top