문제

컴파일러는 일반적으로 이벤트가 옆에 나타나지 않으면 질식합니다. += 또는 -=, 이것이 가능한지 확실하지 않습니다.

표현식 트리를 사용하여 이벤트를 식별하여 테스트용 이벤트 감시자를 만들 수 있기를 원합니다.구문은 다음과 같습니다.

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

내 질문은 두 가지입니다.

  1. 컴파일러가 질식할까요?그렇다면 이를 방지하는 방법에 대한 제안 사항이 있습니까?
  2. 에 연결하기 위해 생성자에서 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#이 해당 선언에서 다음 두 가지를 생성하기 때문입니다.

  1. 이벤트의 지원 스토리지 역할을하는 개인 대의원 분야
  2. 대의원을 사용하는 구현 코드와 함께 실제 이벤트.

편리하게도 둘 다 이름이 같습니다.이것이 바로 샘플 코드가 귀하의 이벤트에 작동하는 이유입니다.

그러나 다른 라이브러리에서 구현된 이벤트를 사용하는 경우에는 이에 의존할 수 없습니다.특히 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