Eenmalige gebeurtenissen met Lambda in C#

Ik merk dat ik dit soort dingen vaak doe:-

EventHandler eh = null;  //can't assign lambda directly since it uses eh
 eh = (s, args) =>
 {
     //small snippet of code here
     ((SomeType)s).SomeEvent -= eh;
 }
 variableOfSomeType.SomeEvent += eh;

Eigenlijk wil ik alleen een gebeurtenishandler koppelen om naar één opname van de gebeurtenis te luisteren, daarna wil ik niet langer verbonden blijven. Heel vaak is dat “codefragment” slechts één regel.

Mijn geest wordt een beetje gevoelloos, ik weet zeker dat er iets moet zijn dat ik kan doen, zodat ik al deze overhead niet hoef te herhalen. Houd er rekening mee dat EventHandlermogelijk EventHandler<T>is.

Enig idee hoe ik het herhalende deel van de code kan opruimen en het fragment gewoon in een Lambda kan laten?


Antwoord 1, autoriteit 100%

Je zou een permanente gebeurtenishandler aan de gebeurtenis kunnen koppelen. De gebeurtenis-handler roept dan “one shot event handlers” aan die worden toegevoegd aan een interne wachtrij:

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

Testles:

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

Antwoord 2, autoriteit 100%

Je kunt reflectie gebruiken:

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

Gebruik het als volgt:

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!"));

Antwoord 3, autoriteit 78%

Als je de Reactive Extensions voor .NETkunt gebruiken, kan dit vereenvoudigen.

Je kunt een Waarneembaar vanaf een gebeurtenis, en luister alleen naar het eerste element met .Take(1), om je kleine codefragment te maken. Dit verandert dit hele proces in een paar regels code.


Bewerken: om te demonstreren heb ik een volledig voorbeeldprogramma gemaakt (ik zal het hieronder plakken).

Ik heb de waarneembare creatie en het abonnement verplaatst naar een methode (HandleOneShot). Hiermee kunt u doen wat u probeert met een enkele methodeaanroep. Om te demonstreren heb ik een klasse gemaakt met twee eigenschappen die INotifyPropertyChanged implementeert, en luister naar de eersteeigenschap gewijzigde gebeurtenis, die naar de console schrijft wanneer deze zich voordoet.

Hiermee wordt uw code overgenomen en verandert deze in:

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

Merk op dat alle aan- en afmeldingen achter de schermen automatisch voor je gebeuren. U hoeft het abonnement niet handmatig in te voeren – abonneer u gewoon op de Observable en Rx regelt dit voor u.

Wanneer uitgevoerd, wordt deze code afgedrukt:

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

Je krijgt maar één keer een trigger voor je evenement.

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

Other episodes