Один выстрел событий с использованием лямбда в C # - PullRequest
43 голосов
/ 28 января 2010

Я часто делаю такие вещи: -

 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;

По сути, я только хочу прикрепить обработчик события, чтобы прослушать один выстрел из события, я больше не хочу оставаться на связи после этого. Довольно часто этот фрагмент кода представляет собой одну строку.

Мой ум немного оцепенел, я уверен, что я должен что-то сделать, поэтому мне не нужно повторять все эти накладные расходы Имейте в виду, что EventHandler вполне может быть EventHandler<T>.

Есть идеи, как мне привести в порядок повторяющуюся часть кода и просто оставить фрагмент в лямбде?

Ответы [ 8 ]

8 голосов
/ 28 января 2010

Вы можете использовать отражение:

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

}

Используйте это так:

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!"));
7 голосов
/ 28 января 2010

Вы можете прикрепить к событию постоянный обработчик события.Затем обработчик события вызывает «обработчики события одного выстрела», которые добавляются во внутреннюю очередь:

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

Код:

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

Класс теста:

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);
    }
}
6 голосов
/ 28 января 2010

Если вы можете использовать Реактивные расширения для .NET , вы можете упростить это.

Вы можете сделать Наблюдаемым из события и прослушивать только первый элемент, используя .Take(1), чтобы сделать свой небольшой фрагмент кода. Это превращает весь этот процесс в пару строк кода.


Редактировать: Для демонстрации я создал полный пример программы (я вставлю ниже).

Я переместил наблюдаемое создание и подписку в метод (HandleOneShot). Это позволяет вам делать то, что вы пытаетесь, с помощью одного вызова метода. Для демонстрации я создал класс с двумя свойствами, который реализует INotifyPropertyChanged, и слушаю событие first измененное свойство, записывая в консоль, когда это происходит.

Это берет ваш код и изменяет его на:

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

Обратите внимание, что вся подписка / отмена подписки происходит автоматически за кулисами. Нет необходимости вручную вводить подписку - просто подпишитесь на Observable, и Rx позаботится об этом за вас.

При запуске этот код печатает:

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

Вы получаете только один, триггерный выстрел вашего события.

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();
        }
    }
}
2 голосов
/ 28 января 2010

Другой пользователь столкнулся с очень похожей проблемой , и я считаю, что решение в этой теме применимо здесь.

В частности, у вас есть , а не экземпляр шаблона публикации / подписки, его очередь сообщений. Достаточно просто создать собственную очередь сообщений, используя Queue{EventHandler}, где вы удаляете события по очереди при их вызове.

Таким образом, вместо того, чтобы подключаться к обработчику событий, ваши "одноразовые" события должны предоставлять метод, позволяющий клиентам добавлять функцию в очередь сообщений.

1 голос
/ 28 января 2010

это работает? Если так, то я говорю пойти на это. Для одноразового мероприятия, которое выглядит довольно элегантно.

Что мне нравится ...

  • Если s - сборщик мусора, то и обработчик событий.
  • Код отсоединения находится рядом с приложенным кодом, что позволяет легко увидеть, что вы делаете.

Возможно, вы сможете обобщить это, но я не совсем уверен, как это сделать, потому что я не могу получить указатель на событие.

0 голосов
/ 10 марта 2017

Почему бы не использовать встроенный в событие стек делегатов? Что-то вроде ...

    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 голосов
/ 20 ноября 2012

Вы, вероятно, хотите работать с новыми идиомами async / await. Обычно, когда мне нужно выполнить одноразовый обработчик события, как вы описали, мне действительно нужно что-то вроде:

await variableOfSomeSort.SomeMethodAsync();
//small snippet of code here
0 голосов
/ 28 октября 2010

Лично я просто создаю специальный метод расширения для любого типа, в котором есть событие, с которым я имею дело.

Вот базовая версия того, что я сейчас использую:

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

Причина, по которой я считаю это лучшим решением, заключается в том, что вам часто не нужно просто обрабатывать событие один раз. Вам также часто нужно проверять, прошло ли уже событие ... Например, вот еще одна версия вышеуказанного метода расширения, который использует присоединенное свойство для проверки, загружен ли уже элемент, и в этом случае он просто вызывает данный обработчик. сразу:

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;
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...