Linq 표현식 트리를 통해 이벤트 식별
-
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
?
해결책
편집하다: 처럼 무뚝뚝한 내 구현은 이벤트를 선언하는 클래스 내에서만 사용할 수 있다는 점에서 다소 결함이 있다고 지적했습니다. :) 대신 "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 );
}
다른 팁
나 역시 이것을 하고 싶었고, Emperor XLII 아이디어와 같은 것을 수행하는 매우 멋진 방법을 생각해냈습니다.그러나 언급한 대로 표현식 트리를 사용하지 않습니다. 표현식 트리에서는 다음을 사용할 수 없으므로 이 작업을 수행할 수 없습니다. +=
또는 -=
.
그러나 .NET Remoting Proxy(또는 LinFu 또는 Castle DP와 같은 다른 프록시)를 사용하여 매우 짧은 수명의 프록시 개체에서 추가/제거 처리기에 대한 호출을 가로채는 깔끔한 트릭을 사용할 수 있습니다.이 프록시 개체의 역할은 단순히 일부 메서드를 호출하고 해당 메서드 호출을 가로채는 것을 허용하는 것입니다. 이 시점에서 이벤트 이름을 찾을 수 있습니다.
이상하게 들리지만 여기에 코드가 있습니다(그런데 이 코드는 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
.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();
}