سؤال

عادةً ما يختنق المترجم عندما لا يظهر حدث بجانب ملف += أو أ -=, ، لذلك لست متأكدًا مما إذا كان هذا ممكنًا.

أريد أن أكون قادرًا على تحديد حدث ما باستخدام شجرة التعبير، حتى أتمكن من إنشاء مراقب حدث للاختبار.سيبدو بناء الجملة كالتالي:

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

نصائح أخرى

أنا أيضًا أردت أن أفعل هذا، وقد توصلت إلى طريقة رائعة جدًا تفعل شيئًا مثل فكرة الإمبراطور 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، وربما يكون الانعكاس هو أفضل طريقة (إن لم تكن الطريقة الوحيدة) للحصول على حدث ما.

يحرر:لقد كتب الإمبراطور XLII بعض التعليمات البرمجية الجميلة التي يجب أن تعمل مع الأحداث الخاصة بك، بشرط أن تكون قد أعلنتها من C# ببساطة

public event DelegateType EventName;

وذلك لأن لغة C# تنشئ لك شيئين من هذا الإعلان:

  1. حقل مندوب خاص ليكون بمثابة تخزين دعم لهذا الحدث
  2. الحدث الفعلي جنبا إلى جنب مع رمز التنفيذ الذي يستخدم المندوب.

ومن الملائم أن كلاهما لهما نفس الاسم.ولهذا السبب سيعمل نموذج التعليمات البرمجية للأحداث الخاصة بك.

ومع ذلك، لا يمكنك الاعتماد على هذا الأمر عند استخدام الأحداث التي تنفذها مكتبات أخرى.على وجه الخصوص، لا تحتوي الأحداث الموجودة في Windows Forms وWPF على مساحة تخزين احتياطية خاصة بها، لذلك لن يعمل نموذج التعليمات البرمجية معها.

في حين أن الإمبراطور الثاني والأربعين قد أعطى بالفعل الإجابة على هذا السؤال، فقد اعتقدت أنه من المفيد مشاركة إعادة كتابتي لهذا.للأسف، لا توجد إمكانية للحصول على الحدث عبر 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();
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top