Определить событие с помощью дерева выражений Linq - PullRequest
9 голосов
/ 30 августа 2008

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

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

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 ]

4 голосов
/ 01 сентября 2008

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

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

В противном случае единственные обновления, которые необходимо выполнить, - это сохранить отраженный EventInfo и использовать методы AddEventHandler / RemoveEventHandler для регистрации прослушивателя (вместо руководства Delegate Combine / 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 );
}
3 голосов
/ 18 июня 2012

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

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

Звучит странно, но вот код (который, кстати, работает ТОЛЬКО, если у вас есть 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
2 голосов
/ 30 августа 2008

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

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

РЕДАКТИРОВАТЬ: Emperor XLII написал красивый код, который должен работать для ваших собственных событий, при условии, что вы объявили их из C # просто как

public event DelegateType EventName;

Это потому, что C # создает для вас две вещи из этой декларации:

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

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

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

1 голос
/ 02 апреля 2011

Хотя Император 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();
}
...