c# - instrucciones - Identificar un evento a través de un árbol de expresiones Linq
instrucciones lambda c# (4)
El compilador generalmente se bloquea cuando un evento no aparece junto a +=
o a -=
, por lo que no estoy seguro de si esto es posible.
Quiero poder identificar un evento mediante el uso de un árbol de Expresión, por lo que puedo crear un observador de eventos para una prueba. La sintaxis se vería así:
using(var foo = new EventWatcher(target, x => x.MyEventToWatch) {
// act here
} // throws on Dispose() if MyEventToWatch hasn''t fired
Mis preguntas son dobles:
- ¿Se ahogará el compilador? Y si es así, ¿alguna sugerencia sobre cómo prevenir esto?
- ¿Cómo puedo analizar el objeto Expression del constructor para adjuntarlo al evento
MyEventToWatch
deltarget
?
Un evento .NET no es realmente 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) para obtener una.
EDITAR: Emperor XLII ha escrito un hermoso código que debería funcionar para sus propios eventos, siempre que los haya declarado desde C # simplemente como
public event DelegateType EventName;
Eso es porque C # crea dos cosas para ti a partir de esa declaración:
- Un campo de delegado privado para servir como almacenamiento de respaldo para el evento
- 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.
Editar: Como ha señalado Curt , mi implementación es bastante defectuosa, ya que solo se puede usar desde dentro de la clase que declara el evento :) En lugar de " x => x.MyEvent
" devolver el evento, devolvió el campo de respaldo , 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 usar para recuperar el evento, por lo que habría que usar la reflexión en su lugar. Una implementación correcta necesitaría usar la reflexión para recuperar EventInfo
para el evento (que, desafortunadamente, no estará fuertemente tipado).
De lo contrario, las únicas actualizaciones que se deben realizar son almacenar el EventInfo
reflejado y usar los métodos AddEventHandler
/ AddEventHandler
para registrar el oyente (en lugar del manual Delegate
Combine
/ Remove
calls y sets de campo). El resto de la implementación no debería ser cambiado. Buena suerte :)
Nota: Este es un código de calidad de demostración que hace varias suposiciones sobre el formato del acceso. La correcta comprobación de errores, el manejo de eventos estáticos, etc., se deja como un 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 de lo sugerido, para aprovechar la inferencia tipográfica:
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 );
}
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 el uso es:
using(EventWatcher.Create(o, "MyEvent")) {
o.RaiseEvent();
}
Yo también quería hacer esto, y he encontrado una forma bastante genial que hace algo así como 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 buen truco donde usamos .NET Remoting Proxy (o cualquier otro Proxy como LinFu o Castle DP) para interceptar una llamada al controlador Add / Remove 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 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 que un delegado de Action<T>
reciba una 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 es pasar el objeto por proxy al delegado Action<T>
provisto.
Donde tenemos el siguiente CustomProxy<T>
, quien 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é esta salida:
Event to watch = Changed
Event to watch = Changed