2008-08-29 4 views
7

+= 또는 -= 옆에 이벤트가 나타나지 않으면 컴파일러는 일반적으로 문제를 일으키기 때문에 가능한지 확실하지 않습니다.Linq 익스프레션 트리를 통해 이벤트를 확인하십시오

식 트리를 사용하여 이벤트를 식별 할 수 있기 때문에 테스트를 위해 이벤트 감시자를 만들 수 있습니다. 구문은 다음과 같이 보일 것입니다 :

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

내 질문에 두 가지이다 :

  1. 윌 컴파일러 초크? 그렇다면이를 방지하는 방법에 대한 제안은 무엇입니까?
  2. targetMyEventToWatch 이벤트에 연결하기 위해 생성자에서 Expression 개체를 구문 분석하려면 어떻게해야합니까?

답변

4

편집 : Curt으로는 지적, 내 구현은 오히려에만 이벤트를 선언하는 클래스 내에서 사용할 수 있다는 :) 대신 "x => x.MyEvent"이벤트를 반환하는에 결함이, 그것은 지원을 반환했다 필드는 클래스에 의해서만 액세스 가능합니다.

표현식에 할당 문을 포함 할 수 없으므로 "(x, h) => x.MyEvent += h"과 같은 수정 된 표현식을 사용하여 이벤트를 검색 할 수 없으므로 반영을 대신 사용해야합니다. 올바른 구현은 리플렉션을 사용하여 이벤트에 대해 EventInfo을 검색해야합니다 (이는 유감스럽게도 강력하게 입력되지 않음).

그렇지 않으면, 할 필요가있는 경우에만 업데이트가 반영 EventInfo를 저장하고, (대신 수동 DelegateCombine/Remove 통화 및 필드 세트) 리스너를 등록 할 AddEventHandler/RemoveEventHandler 방법을 사용한다. 나머지 구현은 변경 될 필요가 없습니다. 행운을 빕니다 :)


참고 : 접근의 형식에 대한 몇 가지 가정을 이것은 데모 품질의 코드입니다.

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); 
} 
: 제안 타입 추론을 활용하기 위해,

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

사용은 약간 다릅니다) 등 정적 이벤트의 처리를 적절한 오류 검사는, 독자에게 연습 문제로 남겨

2

.NET 이벤트는 실제로는 오브젝트가 아니며, 추가 및 처리기 제거를위한 두 가지 기능으로 표현되는 엔드 포인트입니다. 그래서 컴파일러는 + = (더하기를 나타냄) 또는 - = (제거를 나타냄) 이외의 다른 일을시키지 않습니다.

메타 프로그래밍 목적으로 이벤트를 참조하는 유일한 방법은 System.Reflection.EventInfo로서, 리플렉션은 아마도 유일한 방법이 아닌 경우 최선의 방법 일 것입니다.

편집이 : 황제 XLII는, 자신의 이벤트에 대한 작업을해야 아름다운 코드를 작성하면 C 번호에서 그들을 선언 한 제공하고있다 단순히

C#을 그 선언에서 당신을 위해 두 가지를 만들기 때문에입니다
public event DelegateType EventName; 

:

  1. 전용 대리자 필드는 이벤트
  2. 실행 코드와 함께 실제 이벤트에 대한 보강으로 스토리지를 제공하는 것을 대리인의 을 사용합니다.

편리하게 둘 다 같은 이름입니다. 이것이 샘플 코드가 여러분 자신의 이벤트에서 작동하는 이유입니다.

그러나 다른 라이브러리에서 구현 한 이벤트를 사용할 때는이 방법을 사용해서는 안됩니다. 특히 Windows Forms 및 WPF의 이벤트에는 자체 백업 저장소가 없으므로 샘플 코드가 해당 작업에 적합하지 않습니다.

1

황제 XLII는 이미 이에 대한 답을 주었지만, 필자는 이것을 다시 쓰는 동안 가치가 있다고 생각했다. 슬프게도 Expression Tree를 통해 이벤트를 가져올 수있는 능력이 없습니다. 이벤트의 이름을 사용하고 있습니다.

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 처리기에 대한 호출을 차단할 수 있습니다. 이 프록시 객체의 역할은 단순히 메소드를 호출하고 메소드 호출을 가로 채기 위해 이벤트의 이름을 찾을 수있게하는 것입니다.

이 여기에 이상한 소리를하지만

우리는 다음과 같은 인터페이스와 클래스가 가정 (당신이 프록시 객체에 대한 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> 위임하는 프록시 객체를 전달할 수있는 코드이다.우리는 프록시 객체

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

을에 +=-=에 대한 호출을 차단 다음 CustomProxy<T> 코드를 가지고 그리고 남아있는 것을 우리 모두가

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