Как использовать Observable.FromEvent вместо FromEventPattern и избегайте имен событий строкового литерала

Я изучаю свой путь вокруг Rx в WinForms и имею следующий код:

// Create an observable from key presses, grouped by the key pressed
var groupedKeyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, "KeyPress")
                                  .Select(k => k.EventArgs.KeyChar)
                                  .GroupBy(k => k);

// Increment key counter and update user's display
groupedKeyPresses.Subscribe(keyPressGroup =>
{
    var numPresses = 0;
    keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses));
});

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

тем не менее, я не поклонник FromEventPattern подпись, из-за ссылки строкового литерала на событие. Поэтому я решил попробовать FromEvent вместо.

// Create an observable from key presses, grouped by the key pressed
var groupedKeyPresses = Observable.FromEvent<KeyPressEventHandler, KeyPressEventArgs>(h => this.KeyPress += h, h => this.KeyPress -= h)
                                  .Select(k => k.KeyChar)
                                  .GroupBy(k => k);

// Increment key counter and update user's display
groupedKeyPresses.Subscribe(keyPressGroup =>
{
    var numPresses = 0;
    keyPressGroup.Subscribe(key => UpdateKeyPressStats(key, ++numPresses));
});

Итак, единственным изменением была замена Observable.FromEventPattern С Observable.FromEvent (и путь в Select LINQ запрос, чтобы получить KeyChar). Остальные, включая Subscribe методы идентичны. Однако во время выполнения со вторым решением я получаю:

необработанное исключение типа System.ArgumentException в библиотеку mscorlib.dll файлы

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

что вызывает это исключение во время выполнения и как его избежать?

  • GUI: WinForms
  • RX & Rx-WinForms версия: 2.1.30214.0 (через Nuget)
  • Целевой Платформы: 4.5

1 ответов


резюме

первое, что нужно сделать, это то, что вам на самом деле не нужно использовать Observable.FromEvent чтобы избежать ссылки на строковый литерал. Эта версия FromEventPattern совместимость:

var groupedKeyPresses =
    Observable.FromEventPattern<KeyPressEventHandler, KeyPressEventArgs>(
        h => KeyPress += h,
        h => KeyPress -= h)
        .Select(k => k.EventArgs.KeyChar)
        .GroupBy(k => k);

если вы хотите сделать FromEvent РАБОТА, Вы можете сделать это так:

var groupedKeyPresses =
    Observable.FromEvent<KeyPressEventHandler, KeyPressEventArgs>(
        handler =>
        {
            KeyPressEventHandler kpeHandler = (sender, e) => handler(e);
            return kpeHandler;
        }, 
        h => KeyPress += h,
        h => KeyPress -= h)
        .Select(k => k.KeyChar)
        .GroupBy(k => k);

почему? Это потому что FromEvent оператор существует для работы с любым типом делегата события.

первый параметр здесь - функция преобразования, которая соединяет событие с Rx абонент. Он принимает обработчик OnNext наблюдателя (Action<T>) и возвращает обработчик, совместимый с базовым делегатом события, который вызовет этот обработчик OnNext. Затем этот сгенерированный обработчик может быть подписан на событие.

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

The Lowdown on Заметный.FromEvent

следующее ломается почему и как это работает:

обзор того, как работают подписки на события .NET

рассмотрим, как работают события .NET. Они реализуются в виде цепочек делегатов. Делегаты стандартных событий следуют шаблону delegate void FooHandler(object sender, EventArgs eventArgs), но на самом деле события могут работать с любой тип делегата (даже с типом возврата!). Мы подписываемся на событие, передавая соответствующий делегат в a специальная функция, которая добавляет его в цепочку делегатов (обычно через оператор+=), или если обработчики еще не подписаны, делегат становится корнем цепочки. Вот почему мы должны выполнить проверку null при вызове события.

при возникновении события (обычно) вызывается цепочка делегатов, так что каждый делегат в цепочке вызывается по очереди. Чтобы отказаться от подписки на событие .NET, делегат передается в специальную функцию (обычно через оператор -=), чтобы он мог быть удалено из цепочки делегатов (цепочка идет до тех пор, пока не будет найдена соответствующая ссылка, и эта ссылка будет удалена из цепочки).

давайте создадим простую, но нестандартную реализацию событий .NET. Здесь я использую менее распространенный добавить/удалить синтаксис, чтобы предоставить базовую цепочку делегатов и позволить нам регистрировать подписку и отмену подписки. Наше нестандартное событие имеет делегат с параметрами целого числа и строки, а не обычный object sender и EventArgs подкласс:

public delegate void BarHandler(int x, string y);

public class Foo
{  
    private BarHandler delegateChain;

    public event BarHandler BarEvent
    {
        add
        {
            delegateChain += value;                
            Console.WriteLine("Event handler added");
        }
        remove
        {
            delegateChain -= value;
            Console.WriteLine("Event handler removed");
        }
    }

    public void RaiseBar(int x, string y)
    {
        var temp = delegateChain;
        if(temp != null)
        {
            delegateChain(x, y);
        }
    }
}

обзор того, как работают подписки Rx

теперь рассмотрим, как работают наблюдаемые потоки. Подписка на observable формируется путем вызова Subscribe метод и передача объекта, реализующего IObserver<T> интерфейс, который имеет OnNext, OnCompleted и OnError методы, вызываемые наблюдаемым для обработки событий. Кроме того,Subscribe метод возвращает IDisposable ручка которую можно расположить к отписаться.

более типично, мы используем методы расширения удобства которые перегружают Subscribe. Эти расширения принимают обработчики делегатов, соответствующие OnXXX подписи и прозрачно создать AnonymousObservable<T> чей OnXXX методы будут вызывать эти обработчики.

связывание событий .NET и Rx

Итак, как мы можем создать мост для расширения событий .NET в наблюдаемые потоки Rx? Результат вызова Observable.FromEvent должен создать Чей интерфейс IObservable Subscribe метод действует как фабрика, которая создаст этот мост.

шаблон событий .NET не имеет представления завершенных событий или событий ошибок. Только о событии. Другими словами, мы должны только соединить три аспекта события, которые сопоставляются с Rx следующим образом:

  1. подписка e.g вызов IObservable<T>.Subscribe(SomeIObserver<T>) карты fooInstance.BarEvent += barHandlerInstance.
  2. ссылка например, вызов barHandlerInstance(int x, string y) карты для SomeObserver.OnNext(T arg)
  3. отписку например, предполагая, что мы сохраняем возвращенные IDisposable обработчик из нашего Subscribe вызов переменной с именем subscription вызов subscription.Dispose() карты fooInstance.BarEvent -= barHandlerInstance.

обратите внимание, что это всего лишь акт вызова Subscribe это создает подписку. Так что Observable.FromEvent вызов возвращает фабрику, поддерживающую подписку, вызов и отмену подписки на базовое событие. На данный момент нет происходит подписка на мероприятия. Только в точке вызова Subscribe будет ли наблюдатель доступен вместе с ним OnNext обработчик. Следовательно,FromEvent вызов должен принимать заводские методы, которые он может использовать для реализации трех действий моста в соответствующее время.

Аргументы Типа FromEvent

Итак, теперь давайте рассмотрим правильное применение FromEvent для вышеуказанного события.

Напомним, что OnNext обработчики принимают только один аргумент. Обработчики событий .NET могут иметь любой количество параметров. Поэтому наше первое решение-выбрать один тип для представления вызовов событий в целевом наблюдаемом потоке.

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

здесь мы будем отображать int x, string y аргументы вызова BarEvent в форматированную строку, описывающую оба значения. Другими словами, мы вызовемfooInstance.RaiseBar(1, "a") чтобы привести к вызову someObserver.OnNext("X:1 Y:a").

этот пример должен положить конец очень распространенному источнику путаницы: что делают параметры типа FromEvent представляете? Вот первый тип BarHandler и тип делегата исходного события .NET, второй тип составляет в цель OnNext тип аргумента обработчика. потому что этот второй тип часто является EventArgs подкласс часто предполагается, что это должна быть какая - то необходимая часть делегата события .NET-многие люди упускают тот факт, что его актуальность действительно связана с OnNext обработчик. Итак, первая часть нашего FromEvent вызов выглядит так:

 var observableBar = Observable.FromEvent<BarHandler, string>(

Функция Преобразования

теперь давайте рассмотрим первый аргумент FromEvent, так называемая функция преобразования. (Отмечать, некоторые перегрузки FromEvent опустите функцию преобразования-подробнее об этом позже.)

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

(Action<string> onNextHandler) =>
{
    BarHandler barHandler = (int x, string y) =>
    {
        onNextHandler("X:" + x + " Y:" + y);
    };
    return barHandler;
}

таким образом, эта функция преобразования является функции фабрики при вызове создает обработчик, совместимый с базовым событием .NET. Заводская функция принимает OnNext делегат. Этот делегат должен вызываться возвращаемый обработчик в ответ на вызов функции обработчика с базовыми аргументами события .NET. Делегат будет вызван в результате преобразования аргументов события .NET в экземпляр OnNext тип параметра. Таким образом, из приведенного выше примера мы видим, что функция factory будет вызываться с помощью onNextHandler типа Action<string> - он должен вызываться со строковым значением в ответ на каждый вызов события .NET. Функция factory создает обработчик делегата типа BarHandler для события .NET, которое обрабатывает вызовы событий, вызывая onNextHandler с форматированной строкой, созданной из аргументов соответствующего вызова события.

с немного вывода типа, мы можем свернуть приведенный выше код в следующий эквивалентный код:

onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)

функция преобразования поэтому выполняет некоторые логики подписки на события в предоставлении функции для создания соответствующего обработчика событий, а также работа по сопоставлению вызова события .NET с Rx OnNext вызов обработчика.

Как упоминалось ранее, есть перегрузки FromEvent без функции преобразования. Это происходит потому, что это не требуется, если делегат события уже совместим с сигнатурой метода, необходимой для OnNext.

добавить/удалить обработчики

остальные два аргумента-addHandler и removeHandler, которые отвечают за подписку и отмена подписки созданного обработчика делегата на фактическое событие .NET-при условии, что у нас есть экземпляр Foo под названием foo затем FromEvent вызов выглядит так:

var observableBar = Observable.FromEvent<BarHandler, string>(
    onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y),
    h => foo.BarEvent += h,
    h => foo.BarEvent -= h);

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

теперь у нас есть все части для FromEvent observable для полной реализации подписки, вызова и отмены подписки.

еще одна вещь...

есть еще один последний кусок клея, чтобы упомянуть. Rx оптимизирует подписки на событие .NET. На самом деле для любого заданного числа подписчиков наблюдаемого только одна подписка делается на базовое событие .NET. Это затем многоадресная рассылка абонентам Rx через Publish механизм. Это как если бы Publish().RefCount() было добавлено к наблюдаемому.

рассмотрим следующий пример, используя делегат и класс, определенный выше:

public static void Main()
{
    var foo = new Foo();

    var observableBar = Observable.FromEvent<BarHandler, string>(
        onNextHandler => (int x, string y)
            => onNextHandler("X:" + x + " Y:" + y),
    h => foo.BarEvent += h,
    h => foo.BarEvent -= h);

    var xs = observableBar.Subscribe(x => Console.WriteLine("xs: " + x));
    foo.RaiseBar(1, "First");    
    var ys = observableBar.Subscribe(x => Console.WriteLine("ys: " + x));
    foo.RaiseBar(1, "Second");
    xs.Dispose();
    foo.RaiseBar(1, "Third");    
    ys.Dispose();
}

это производит следующий вывод, демонстрируя только одну подписку составляет:

Event handler added
xs: X:1 Y:First
xs: X:1 Y:Second
ys: X:1 Y:Second
ys: X:1 Y:Third
Event handler removed

я помогаю это помогает проясняет любую затяжную путаницу в том, как эта сложная функция работает!