Pergunta

O compilador geralmente engasga quando um evento não aparece ao lado de um += ou um -=, então não tenho certeza se isso é possível.

Quero poder identificar um evento usando uma árvore de expressões, para poder criar um observador de eventos para um teste.A sintaxe seria mais ou menos assim:

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

Minhas perguntas são duplas:

  1. O compilador irá engasgar?E se sim, alguma sugestão sobre como evitar isso?
  2. Como posso analisar o objeto Expression do construtor para anexar ao MyEventToWatch evento de target?
Foi útil?

Solução

Editar: Como Curto apontou, minha implementação é bastante falha porque só pode ser usada dentro da classe que declara o evento :) Em vez de "x => x.MyEvent" ao retornar o evento, estava retornando o backing field, que só é acessível pela classe.

Como as expressões não podem conter instruções de atribuição, uma expressão modificada como "( x, h ) => x.MyEvent += h" não pode ser usado para recuperar o evento, portanto, a reflexão precisaria ser usada.Uma implementação correta precisaria usar reflexão para recuperar o EventInfo para o evento (que, infelizmente, não será fortemente digitado).

Caso contrário, as únicas atualizações que precisam ser feitas são armazenar o refletido EventInfo, e use o AddEventHandler/RemoveEventHandler métodos para registrar o ouvinte (em vez do manual Delegate Combine/Remove chamadas e conjuntos de campos).O resto da implementação não deverá precisar ser alterado.Boa sorte :)


Observação: Este é um código com qualidade de demonstração que faz diversas suposições sobre o formato do acessador.A verificação adequada de erros, o tratamento de eventos estáticos, etc., são deixados como um exercício para o leitor;)

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

O uso é um pouco diferente do sugerido, para aproveitar a inferência de tipo:

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

Outras dicas

Eu também queria fazer isso e descobri uma maneira bem legal que faz algo parecido com a ideia do Imperador XLII.Porém, ele não usa árvores de expressão, como mencionado, isso não pode ser feito, pois as árvores de expressão não permitem o uso de += ou -=.

No entanto, podemos usar um truque interessante onde usamos o .NET Remoting Proxy (ou qualquer outro proxy como LinFu ou Castle DP) para interceptar uma chamada para o manipulador Adicionar/Remover em um objeto proxy de vida muito curta.A função deste objeto proxy é simplesmente ter algum método chamado nele e permitir que suas chamadas de método sejam interceptadas, momento em que podemos descobrir o nome do evento.

Isso parece estranho, mas aqui está o código (que, a propósito, SÓ funciona se você tiver um MarshalByRefObject ou uma interface para o objeto proxy)

Suponha que temos a seguinte interface e classe

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

Então podemos ter uma classe muito simples que espera um Action<T> delegado que será aprovado em alguma instância de T.

Aqui está o 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()));
    }
}

O truque é passar o objeto proxy para o Action<T> delegado fornecido.

Onde temos o seguinte CustomProxy<T> código, que intercepta a chamada para += e -= no 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;
    }
}

E então tudo o que resta é usar isso da seguinte maneira

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

Fazendo isso, verei esta saída:

Event to watch = Changed
Event to watch = Changed

Um evento .NET não é realmente um objeto, é um endpoint representado por duas funções – uma para adicionar e outra para remover um manipulador.É por isso que o compilador não permite fazer nada além de += (que representa a adição) ou -= (que representa a remoção).

A única maneira de se referir a um evento para fins de metaprogramação é como System.Reflection.EventInfo, e a reflexão é provavelmente a melhor maneira (se não a única) de obter um.

EDITAR:O Imperador XLII escreveu um belo código que deve funcionar para seus próprios eventos, desde que você os tenha declarado em C# simplesmente como

public event DelegateType EventName;

Isso ocorre porque o C# cria duas coisas para você a partir dessa declaração:

  1. Um campo de delegado particular para servir como armazenamento de apoio para o evento
  2. O evento real, juntamente com o código de implementação que utiliza o delegado.

Convenientemente, ambos têm o mesmo nome.É por isso que o código de exemplo funcionará para seus próprios eventos.

No entanto, você não pode confiar que isso aconteça ao usar eventos implementados por outras bibliotecas.Em particular, os eventos no Windows Forms e no WPF não possuem seu próprio armazenamento de apoio, portanto, o código de exemplo não funcionará para eles.

Embora o Imperador XLII já tenha dado a resposta para isso, achei que valia a pena compartilhar minha reescrita.Infelizmente, não é possível obter o Evento via Expression Tree, estou usando o nome do 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.");
     }
}

E o uso é:

using(EventWatcher.Create(o, "MyEvent")) {
    o.RaiseEvent();
}
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top