Определить событие с помощью дерева выражений Linq
-
09-06-2019 - |
Вопрос
Компилятор обычно задыхается, когда событие не отображается рядом с +=
или -=
, поэтому я не уверен, возможно ли это.
Я хочу иметь возможность идентифицировать событие с помощью дерева выражений, чтобы я мог создать наблюдатель событий для теста. Синтаксис будет выглядеть примерно так:
using(var foo = new EventWatcher(target, x => x.MyEventToWatch) {
// act here
} // throws on Dispose() if MyEventToWatch hasn't fired
У меня двоякие вопросы:
<Ол>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();
}