2008-08-29 20 views
7

El compilador generalmente se bloquea cuando un evento no aparece junto a += o -=, por lo que no estoy seguro de si esto es posible.Identifique un evento a través de un árbol de expresiones Linq

Quiero ser capaz de identificar un evento mediante el uso de un árbol de Expresión, por lo que puedo crear un vigilante 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 dos:

    Will
  1. el estrangulador compilador? Y si es así, ¿alguna sugerencia sobre cómo prevenir esto?
  2. ¿Cómo puedo analizar el objeto Expression del constructor para adjuntarlo al evento MyEventToWatch de target?

Respuesta

4

Editar: Como Curt ha señalado, mi aplicación es bastante defectuoso, ya que sólo se puede utilizar desde dentro de la clase que declara el evento :) En lugar de "x => x.MyEvent" devolver el caso, regresaba el respaldo campo, que solo es accesible por la clase.

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

De lo contrario, los únicos cambios que deben hacerse son para almacenar el EventInfo reflejada, y el uso de los AddEventHandler/RemoveEventHandler métodos para registrar el detector (en lugar del manual DelegateCombine/Remove llamadas y conjuntos de campos). El resto de la implementación no debería ser cambiado. Buena suerte :)


Nota: Este es el código calidad demostración de que hace varias suposiciones sobre el formato del descriptor de acceso. la comprobación de errores, 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_); 
    } 
} 

uso es ligeramente diferente a la sugerida, con el fin de tomar ventaja de 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); 
} 
2

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 el complemento) o - = (que representa la eliminación).

La única forma de referirse a un evento para fines de metaprogramación es como System.Reflection.EventInfo, y la reflexión es probablemente la mejor manera (si no la única) de obtener una.

EDIT: Emperor XLII ha escrito algo de código preciosa que debe trabajar para sus propios eventos, siempre y cuando se les ha declarado desde C# simplemente como

public event DelegateType EventName; 

Eso es debido a que C# crea dos cosas para que de esa declaración:

  1. Un campo delegado privada para servir como el respaldo de almacenamiento para el evento
  2. el evento real, junto con el código de implementación que hace uso 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 en WPF no tienen su propio almacenamiento de respaldo, por lo que el código de ejemplo no funcionará para ellos.

1

Mientras que el Emperador XLII ya dio la respuesta para esto, pensé que valía la pena compartir mi reescritura de esto. Lamentablemente, no hay posibilidad de obtener el evento a través de Expression Tree, 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 su uso es:

using(EventWatcher.Create(o, "MyEvent")) { 
    o.RaiseEvent(); 
} 
3

demasiado lo que quería hacer esto, y han llegado con una forma muy bueno que hace algo como idea 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 utilizar un buen truco donde utilizamos .NET Remoting Proxy (o cualquier otro Proxy como LinFu o Castle DP) para interceptar una llamada al controlador Agregar/Quitar en un objeto proxy de muy corta duración. La función de este objeto proxy es simplemente tener un método invocado y permitir que se intercepten sus llamadas a métodos, en cuyo caso podemos encontrar el nombre del evento.

Esto suena extraño, pero aquí está el código (que por cierto sólo funciona si tiene una interfaz MarshalByRefObject o de los objetos proxy)

Supongamos que tenemos la siguiente interfaz y la 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 un delegado Action<T> que se aprobará en alguna instancia de T.

Este es 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 los objetos proxy al delegado Action<T> proporcionado.

donde tenemos la CustomProxy<T> siguiente código, que 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 entonces todo lo que queda es usar esto como sigue

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é esta salida:

Event to watch = Changed 
Event to watch = Changed 
Cuestiones relacionadas