Определить событие с помощью дерева выражений Linq

StackOverflow https://stackoverflow.com/questions/35211

  •  09-06-2019
  •  | 
  •  

Вопрос

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

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

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

У меня двоякие вопросы:

<Ол>
  • Будет ли задыхаться компилятор? И если да, какие-либо предложения о том, как это предотвратить?
  • Как я могу разобрать объект Expression из конструктора, чтобы присоединить его к событию MyEventToWatch target?
  • Это было полезно?

    Решение

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

    Поскольку выражения не могут содержать операторы присваивания, измененное выражение типа " ( x, h ) => x.MyEvent += h " не может использоваться для извлечения события, поэтому вместо этого нужно будет использовать отражение. Для правильной реализации потребуется использовать отражение, чтобы получить 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 );
    }
    

    Другие советы

    Я тоже хотел сделать это, и я придумал довольно крутой способ, который делает что-то вроде идеи 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()));
        }
    }
    

    Хитрость заключается в передаче прокси-объекта предоставленному делегату 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
    

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

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

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

    public event DelegateType EventName;
    

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

    <Ол>
  • Поле частного делегата, служащее основой хранилище для события
  • Фактическое событие вместе с код реализации, который использует делегата.
  • Удобно, оба они имеют одинаковое имя. Вот почему пример кода будет работать для ваших собственных событий.

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

    Хотя Император 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();
    }
    
    Лицензировано под: CC-BY-SA с атрибуция
    Не связан с StackOverflow
    scroll top