2008-08-29 2 views
7

Компилятор обычно дросселирует, когда событие не отображается рядом с += или -=, поэтому я не уверен, что это возможно.Идентифицировать событие через дерево выражения Linq

Я хочу, чтобы иметь возможность идентифицировать событие, используя дерево выражений, поэтому я могу создать наблюдателя событий для теста. Синтаксис будет выглядеть следующим образом:

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

Мои вопросы носят двоякий характер:

    Будет
  1. подсос компилятор? И если да, то какие-либо предложения о том, как предотвратить это?
  2. Как я могу разобрать объект Expression из конструктора, чтобы прикрепить событие MyEventToWatch от target?

ответ

4

Edit: Как Curt отметил, моя реализация довольно изъяны в том, что она может быть использована только внутри класса, объявляющего событие :) Вместо «x => x.MyEvent» возвращающегося событие, оно возвращалось поддержку поле, которое доступно только классу.

Поскольку выражения не могут содержать операторы присваивания, для извлечения события не может использоваться измененное выражение типа «(x, h) => x.MyEvent += h», поэтому вместо этого необходимо использовать отражение. Правильная реализация должна будет использовать отражение для извлечения EventInfo для события (что, к сожалению, не будет строго типизировано).

В противном случае, только обновления, которые должны быть сделаны, хранят отраженную EventInfo и использовать AddEventHandler/RemoveEventHandler методы для регистрации слушателя (вместо ручного DelegateCombine/Remove звонки и наборы полей). Остальная часть реализации не должна изменяться. Удачи :)


Примечание: Это демонстрация качества кода, который делает несколько предположений о формате аксессору. Надлежащая проверка ошибок, обработка статических событий и т.д., остается в качестве упражнения для читателя;)

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

Использование немного отличается от предложенного для того, чтобы воспользоваться преимуществами умозаключений типа:

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

Событие .NET на самом деле не является объектом, это конечная точка, представленная двумя функциями: одна для добавления и одна для удаления обработчика. Вот почему компилятор не позволит вам делать что-либо другое, кроме + = (которое представляет добавление) или - = (которое представляет удаление).

Единственный способ обратиться к событию для целей метапрограммирования - это как System.Reflection.EventInfo, а отражение, вероятно, лучший способ (если не единственный способ) получить его.

EDIT: Император XLII написал некоторый красивый код, который должен работать для ваших собственных событий, при условии, вы объявили их от C# просто как

public event DelegateType EventName; 

Это потому, что C# создает две вещи для вас из этого заявления:

  1. частное поле делегатом служить в качестве основы хранения для события
  2. Реальное событие вместе с кодом реализации, использует делегата .

Удобно, оба они имеют одно и то же имя. Вот почему образец кода будет работать для ваших собственных событий.

Однако вы не можете полагаться на это, когда используете события, реализованные другими библиотеками. В частности, события в Windows Forms и WPF не имеют собственного резервного хранилища, поэтому примерный код для них не будет работать.

1

Хотя император XLII уже дал ответ на этот вопрос, я подумал, что стоит поделиться своим переписью. К сожалению, нет возможности получить событие через дерево выражений, я использую имя события.

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

А использование является:

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

Я тоже хотел это сделать, и я пришел с довольно прохладным способом, что делает что-то вроде императора XLII идеи. Он не использует деревья выражений, хотя, как уже упоминалось, это невозможно сделать, поскольку деревья выражений не позволяют использовать += или -=.

Однако мы можем использовать опрятный трюк, в котором мы используем прокси-сервер .NET Remoting Proxy (или любой другой прокси, такой как LinFu или Castle DP), чтобы перехватить вызов Add/Remove handler на очень коротком прокси-объекте. Роль этого прокси-объекта состоит в том, чтобы просто вызвать на него какой-то метод и разрешить перехватывать его вызовы методов, после чего мы можем узнать название события.

Это звучит странно, но вот код (который, кстати, работает только если у вас есть MarshalByRefObject или интерфейс для объекта прокси)

Предположим, мы имеем следующий интерфейс и класс

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

Тогда у нас может быть очень простой класс, который ожидает делегата Action<T>, который получит какой-то экземпляр T.

Вот код

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

Хитрость заключается в том, чтобы пройти через прокси-объект в Action<T> делегата.

Где мы имеем следующий CustomProxy<T> код, который перехватывает вызов += и -= на проксируемом объекте

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

И тогда мы все, что осталось, чтобы использовать эту функцию следующим образом

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

При этом я увижу этот выход:

Event to watch = Changed 
Event to watch = Changed 

 Смежные вопросы

  • Нет связанных вопросов^_^