2008-08-29 46 views
7

Trình biên dịch thường bị rung khi một sự kiện không xuất hiện bên cạnh += hoặc -=, vì vậy tôi không chắc liệu điều này có thể thực hiện được hay không.Xác định sự kiện qua cây biểu thức LINQ

Tôi muốn có thể xác định sự kiện bằng cách sử dụng cây Biểu thức, vì vậy tôi có thể tạo trình theo dõi sự kiện để kiểm tra. Cú pháp sẽ giống như thế này:

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

Câu hỏi của tôi là hai khía cạnh:

  1. Will trình biên dịch sặc? Và nếu có, bất kỳ đề xuất nào về cách ngăn chặn điều này?
  2. Làm cách nào để phân tích cú pháp đối tượng Biểu thức khỏi hàm tạo để đính kèm sự kiện MyEventToWatch của target?

Trả lời

4

Edit: Như Curt đã chỉ ra, thực hiện của tôi là khá sai lầm ở chỗ nó chỉ có thể được sử dụng từ bên trong lớp mà tuyên bố sự kiện này :) Thay vì "x => x.MyEvent" trở về sự kiện này, nó đã được trả lại sự ủng hộ trường chỉ có thể truy cập bởi lớp.

Vì biểu thức không thể chứa câu lệnh gán, biểu thức đã sửa đổi như "(x, h) => x.MyEvent += h" không thể được sử dụng để truy xuất sự kiện, do đó, việc phản chiếu sẽ cần được sử dụng thay thế. Việc triển khai chính xác sẽ cần sử dụng sự phản chiếu để truy xuất EventInfo cho sự kiện (trong đó, unfortunatley, sẽ không được đánh máy mạnh).

Nếu không, các bản cập nhật duy nhất mà cần phải được thực hiện là để lưu trữ các phản ánh EventInfo, và sử dụng AddEventHandler/RemoveEventHandler phương pháp để đăng ký listener (thay vì hướng dẫn DelegateCombine/Remove cuộc gọi và bộ lĩnh vực). Phần còn lại của việc triển khai không cần phải thay đổi. Chúc may mắn :)


Lưu ý: Đây là mã trình diễn chất lượng mà làm cho một số giả định về định dạng của các accessor. kiểm tra lỗi thích hợp, xử lý các sự kiện tĩnh, vv, là trái như một bài tập cho người đọc;)

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

Cách sử dụng hơi khác so với đề nghị, để tận dụng lợi thế của kiểu suy luận:

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

Sự kiện .NET không thực sự là đối tượng, đó là điểm cuối được biểu thị bằng hai hàm - một để thêm và một để loại bỏ trình xử lý. Đó là lý do tại sao trình biên dịch sẽ không cho phép bạn làm bất cứ điều gì khác hơn là + = (đại diện cho phần bổ sung) hoặc - = (đại diện cho loại bỏ).

Cách duy nhất để đề cập đến một sự kiện cho mục đích lập trình meta là System.Reflection.EventInfo, và phản ánh có lẽ là cách tốt nhất (nếu không phải là cách duy nhất) để có được ahold của một.

EDIT: Hoàng đế XLII đã viết một số mã đẹp mà nên làm việc cho các sự kiện của riêng bạn, miễn là bạn đã tuyên bố họ từ C# đơn giản là

public event DelegateType EventName; 

Đó là bởi vì C# tạo ra hai thứ cho bạn từ khai rằng:

  1. Một trường đại biểu riêng để phục vụ như sự ủng hộ lưu trữ cho trường hợp
  2. sự kiện thực tế cùng với mã thực hiện điều đó sử dụng của người được ủy quyền.

Thuận tiện, cả hai đều có cùng tên. Đó là lý do tại sao mã mẫu sẽ hoạt động cho các sự kiện của riêng bạn.

Tuy nhiên, bạn không thể dựa vào trường hợp này khi sử dụng các sự kiện do thư viện khác triển khai. Đặc biệt, các sự kiện trong Windows Forms và trong WPF không có bộ nhớ sao lưu riêng, vì vậy mã mẫu sẽ không hoạt động cho chúng.

1

Trong khi Hoàng đế XLII đã đưa ra câu trả lời cho điều này, tôi nghĩ rằng nó là giá trị trong khi chia sẻ viết lại của tôi về điều này. Đáng buồn thay, không có khả năng để có được sự kiện thông qua cây biểu hiện, tôi đang sử dụng tên của sự kiện.

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."); 
    } 
} 

Và sử dụng là:

using(EventWatcher.Create(o, "MyEvent")) { 
    o.RaiseEvent(); 
} 
3

tôi cũng muốn làm điều này, và tôi đã tìm ra một cách khá mát mẻ mà không một cái gì đó giống như Hoàng đế XLII ý tưởng. Nó không sử dụng cây biểu thức mặc dù, như đã đề cập này không thể được thực hiện như cây biểu thức không cho phép sử dụng += hoặc -=. Tuy nhiên, chúng tôi có thể sử dụng một thủ thuật gọn gàng khi chúng tôi sử dụng .NET Remoting Proxy (hoặc bất kỳ Proxy nào khác như LinFu hoặc Castle DP) để chặn một cuộc gọi đến Add/Remove handler trên một đối tượng proxy rất ngắn ngủi. Vai trò của đối tượng proxy này chỉ đơn giản là có một số phương thức được gọi trên nó, và để cho phép các cuộc gọi phương thức của nó bị chặn, tại thời điểm đó chúng ta có thể tìm ra tên của sự kiện.

này nghe có vẻ lạ nhưng đây là mã (mà bằng cách này chỉ hoạt động nếu bạn có một MarshalByRefObject hoặc một giao diện cho các đối tượng proxy)

Giả sử chúng ta có giao diện và lớp sau

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

Sau đó, chúng tôi có thể có một lớp học rất đơn giản mà mong đợi một đại biểu Action<T> sẽ được thông qua một số trường hợp của T.

Đây là mã

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

Bí quyết là để vượt qua các đối tượng proxy cho Action<T> đại biểu được cung cấp.

đâu chúng ta có CustomProxy<T> mã sau, người chặn cuộc gọi đến +=-= trên đối tượng proxy

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

Và sau đó tất cả chúng ta những gì còn lại là sử dụng này như sau

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

Làm như vậy, tôi sẽ thấy kết quả này:

Event to watch = Changed 
Event to watch = Changed 
Các vấn đề liên quan