One shot events using Lambda in C#

前端 未结 8 1625
無奈伤痛
無奈伤痛 2021-01-31 07:55

I find myself doing this sort of thing quite often:-

 EventHandler eh = null;  //can\'t assign lambda directly since it uses eh
 eh = (s, args) =>
 {
     //s         


        
相关标签:
8条回答
  • 2021-01-31 08:09

    Personally, I just create a specialized extension method for whatever type has the event I'm dealing with.

    Here's a basic version of something I am using right now:

    namespace MyLibrary
    {
        public static class FrameworkElementExtensions
        {
            public static void HandleWhenLoaded(this FrameworkElement el, RoutedEventHandler handler)
            {
                RoutedEventHandler wrapperHandler = null;
                wrapperHandler = delegate
                {
                    el.Loaded -= wrapperHandler;
    
                    handler(el, null);
                };
                el.Loaded += wrapperHandler;
            }
        }
    }
    

    The reason I think this is the best solution is because you often don't need to just handle the event one time. You also often need to check if the event has already passed... For instance, here is another version of the above extension method that uses an attached property to check if the element is already loaded, in which case it just calls the given handler right away:

    namespace MyLibraryOrApplication
    {
        public static class FrameworkElementExtensions
        {
            public static void HandleWhenLoaded(this FrameworkElement el, RoutedEventHandler handler)
            {
                if ((bool)el.GetValue(View.IsLoadedProperty))
                {
                    // el already loaded, call the handler now.
                    handler(el, null);
                    return;
                }
                // el not loaded yet. Attach a wrapper handler that can be removed upon execution.
                RoutedEventHandler wrapperHandler = null;
                wrapperHandler = delegate
                {
                    el.Loaded -= wrapperHandler;
                    el.SetValue(View.IsLoadedProperty, true);
    
                    handler(el, null);
                };
                el.Loaded += wrapperHandler;
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-31 08:17

    You can use reflection:

    public static class Listener {
    
      public static void ListenOnce(this object eventSource, string eventName, EventHandler handler) {
        var eventInfo = eventSource.GetType().GetEvent(eventName);
        EventHandler internalHandler = null;
        internalHandler = (src, args) => {
          eventInfo.RemoveEventHandler(eventSource, internalHandler);
          handler(src, args);
        };
        eventInfo.AddEventHandler(eventSource, internalHandler);
      }
    
      public static void ListenOnce<TEventArgs>(this object eventSource, string eventName, EventHandler<TEventArgs> handler) where TEventArgs : EventArgs {
        var eventInfo = eventSource.GetType().GetEvent(eventName);
        EventHandler<TEventArgs> internalHandler = null;
        internalHandler = (src, args) => {
          eventInfo.RemoveEventHandler(eventSource, internalHandler);
          handler(src, args);
        };
        eventInfo.AddEventHandler(eventSource, internalHandler);
      }
    
    }
    

    Use it like so:

    variableOfSomeType.ListenOnce("SomeEvent", 
      (s, args) => Console.WriteLine("I should print only once!"));
    
    variableOfSomeType.ListenOnce<InterestingEventArgs>("SomeOtherEvent", 
      (s, args) => Console.WriteLine("I should print only once!"));
    
    0 讨论(0)
  • 2021-01-31 08:17

    Another user encountered a very similar problem, and I believe the solution in that thread applies here.

    In particular, what you have is not an instance of the publish/subscribe pattern, its a message queue. Its easy enough to create your own message queue using a Queue{EventHandler}, where you dequeue events as you invoke them.

    So instead of hooking on to an event handler, your "one-shot" events should expose a method allowing clients to add an function to the message queue.

    0 讨论(0)
  • 2021-01-31 08:19

    You could attache a permanent event handler to the event. The event handler then invokes "one shot event handlers" that are added to an internal queue:

    OneShotHandlerQueue<EventArgs> queue = new OneShotHandlerQueue<EventArgs>();
    
    Test test = new Test();
    
    // attach permanent event handler
    test.Done += queue.Handle;
    
    // add a "one shot" event handler
    queue.Add((sender, e) => Console.WriteLine(e));
    test.Start();
    
    // add another "one shot" event handler
    queue.Add((sender, e) => Console.WriteLine(e));
    test.Start();
    

    Code:

    class OneShotHandlerQueue<TEventArgs> where TEventArgs : EventArgs {
        private ConcurrentQueue<EventHandler<TEventArgs>> queue;
        public OneShotHandlerQueue() {
            this.queue = new ConcurrentQueue<EventHandler<TEventArgs>>();
        }
        public void Handle(object sender, TEventArgs e) {
            EventHandler<TEventArgs> handler;
            if (this.queue.TryDequeue(out handler) && (handler != null))
                handler(sender, e);
        }
        public void Add(EventHandler<TEventArgs> handler) {
            this.queue.Enqueue(handler);
        }
    }
    

    Test class:

    class Test {
        public event EventHandler Done;
        public void Start() {
            this.OnDone(new EventArgs());
        }
        protected virtual void OnDone(EventArgs e) {
            EventHandler handler = this.Done;
            if (handler != null)
                handler(this, e);
        }
    }
    
    0 讨论(0)
  • 2021-01-31 08:19

    If you can use the Reactive Extensions for .NET, you can simplify this.

    You can make an Observable from an event, and only listen for the first element using .Take(1), to do your small snippet of code. This turns this entire process into a couple of lines of code.


    Edit: In order to demonstrate, I've made a full sample program (I'll paste below).

    I moved the observable creation and subscription into a method (HandleOneShot). This lets you do what you're attempting with a single method call. For demonstrating, I made a class with two properties that implements INotifyPropertyChanged, and am listening for the first property changed event, writing to the console when it occurs.

    This takes your code, and changes it to:

    HandleOneShot<SomeEventArgs>(variableOfSomeType, "SomeEvent",  e => { 
                        // Small snippet of code here
                    }); 
    

    Notice that all of the subscription/unsubscription happens automatically for you behind the scenes. There's no need to handle putting in the subscription manually - just Subscribe to the Observable, and Rx takes care of this for you.

    When run, this code prints:

    Setup...
    Setting first property...
     **** Prop2 Changed! /new val
    Setting second property...
    Setting first property again.
    Press ENTER to continue...
    

    You only get a single, one shot trigger of your event.

    namespace ConsoleApplication1
    {
        using System;
        using System.ComponentModel;
        using System.Linq;
    
        class Test : INotifyPropertyChanged
        {
            private string prop2;
            private string prop;
            public string Prop
            {
                get {
                    return prop;
                }
                set
                {
                    if (prop != value)
                    {
                        prop = value;
                        if (PropertyChanged!=null)
                            PropertyChanged(this, new PropertyChangedEventArgs("Prop"));
                    }
                }
            }
    
            public string Prop2
            {
                get
                {
                    return prop2;
                }
                set
                {
                    if (prop2 != value)
                    {
                        prop2 = value;
                        if (PropertyChanged != null)
                            PropertyChanged(this, new PropertyChangedEventArgs("Prop2"));
                    }
                }
            }
    
            public event PropertyChangedEventHandler PropertyChanged;
        }
    
    
        class Program
        {
            static void HandleOneShot<TEventArgs>(object target, string eventName, Action<TEventArgs> action)  where TEventArgs : EventArgs
            {
                var obsEvent = Observable.FromEvent<TEventArgs>(target, eventName).Take(1);
                obsEvent.Subscribe(a => action(a.EventArgs));
            }
    
            static void Main(string[] args)
            {
                Test test = new Test();
    
                Console.WriteLine("Setup...");
                HandleOneShot<PropertyChangedEventArgs>(
                    test, 
                    "PropertyChanged", 
                    e =>
                        {
                            Console.WriteLine(" **** {0} Changed! {1}/{2}!", e.PropertyName, test.Prop, test.Prop2);
                        });
    
                Console.WriteLine("Setting first property...");
                test.Prop2 = "new value";
                Console.WriteLine("Setting second property...");
                test.Prop = "second value";
                Console.WriteLine("Setting first property again...");
                test.Prop2 = "other value";
    
                Console.WriteLine("Press ENTER to continue...");
                Console.ReadLine();
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-31 08:26

    Why not do use the delegate stack built into the event? Something like...

        private void OnCheckedIn(object sender, Session e)
        {
            EventHandler<Session> nextInLine = null; 
            lock (_syncLock)
            {
                if (SessionCheckedIn != null)
                {
                    nextInLine = (EventHandler<Session>)SessionCheckedIn.GetInvocationList()[0];
                    SessionCheckedIn -= nextInLine;
                }
            }
    
            if ( nextInLine != null )
            {
                nextInLine(this, e);
            }
        }
    
    0 讨论(0)
提交回复
热议问题