Identifique um evento por meio de uma árvore de expressão Linq
-
09-06-2019 - |
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:
- O compilador irá engasgar?E se sim, alguma sugestão sobre como evitar isso?
- Como posso analisar o objeto Expression do construtor para anexar ao
MyEventToWatch
evento detarget
?
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:
- Um campo de delegado particular para servir como armazenamento de apoio para o evento
- 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();
}