如何使用Observable.FromEvent而不是FromEventPattern并避免使用字符串文字事件名称

cki*_*tel 32 .net winforms system.reactive

我正在WinForms中学习Rx,并拥有以下代码:

// 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));
});
Run Code Online (Sandbox Code Playgroud)

这可以完美地工作/运行,在KeyPress事件中流,按键推送组,然后跟踪每个键被按下的次数,并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));
});
Run Code Online (Sandbox Code Playgroud)

所以,唯一的变化是换出Observable.FromEventPatternObservable.FromEvent(和在道路SelectLINQ查询来获取KeyChar).其余的,包括Subscribe方法是相同的.但是,在第二个解决方案的运行时,我得到:

mscorlib.dll中出现未处理的"System.ArgumentException"类型异常

附加信息:无法绑定到目标方法,因为其签名或安全透明性与委托类型的签名或安全透明度不兼容.

导致此运行时异常的原因是什么?我应该如何避免它?

  • GUI:WinForms
  • Rx&Rx-WinForms版本:2.1.30214.0(通过Nuget)
  • 目标框架:4.5

Jam*_*rld 87

摘要

要做的第一点是您实际上不需要使用Observable.FromEvent来避免字符串文字引用.这个版本FromEventPattern将工作:

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

如果你想做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);
Run Code Online (Sandbox Code Playgroud)

为什么?这是因为FromEvent运算符可以使用任何事件委托类型.

这里的第一个参数是将事件连接到Rx订户的转换函数.它接受观察者(an Action<T>)的OnNext处理程序,并返回与将调用该OnNext处理程序的基础事件委托兼容的处理程序.然后,可以将此生成的处理程序订阅到该事件.

我从不喜欢这个函数的官方MSDN文档,所以这里有一个扩展的解释,逐步介绍这个函数的用法.

Observable.FromEvent的Lowdown

以下细分了FromEvent存在的原因及其工作原理:

回顾.NET事件订阅的工作原理

考虑.NET事件的工作原理.这些是作为代理链实现的.标准事件委托遵循模式delegate void FooHandler(object sender, EventArgs eventArgs),但实际上事件可以使用任何委托类型(甚至那些具有返回类型!).我们通过将适当的委托传递给特殊函数来订阅事件,该特殊函数将其添加到委托链(通常通过+ =运算符),或者如果尚未订阅处理程序,则委托成为链的根.这就是我们在举起活动时必须进行空检查的原因.

引发事件时(通常)调用委托链,以便依次调用链中的每个委托.要取消订阅.NET事件,委托将被传递到一个特殊的函数(通常通过 - =运算符),以便可以从委托链中删除它(链被遍历,直到找到匹配的引用,并且该链接是从链中删除).

让我们创建一个简单但非标准的.NET事件实现.在这里,我使用不太常见的添加/删除语法来公开底层委托链,并使我们能够记录订阅和取消订阅.我们的非标准事件具有一个委托,其参数为整数和字符串,而不是通常object senderEventArgs子类:

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

回顾Rx订阅的工作原理

现在考虑Observable流如何工作.订阅到可观察到的是通过调用形成Subscribe方法并传递一个实现的对象IObserver<T>接口,其具有OnNext,OnCompletedOnError方法,通过可观察到的调用来处理事件.此外,该Subscribe方法返回一个IDisposable句柄,可以取消订阅.

更典型的是,我们使用过载的便利扩展方法Subscribe.这些扩展接受委托处理程序符合OnXXX签名和透明的创建一个AnonymousObservable<T>OnXXX方法会调用这些处理程序.

桥接.NET和Rx事件

那么我们如何创建一个桥接器来将.NET事件扩展到Rx可观察流?调用Observable.FromEvent的结果是创建一个IObservable,其Subscribe方法就像一个工厂,它将创建这个桥.

.NET事件模式没有已完成或错误事件的表示.只有一个事件被提出.换句话说,我们必须将映射到Rx的事件的三个​​方面桥接如下:

  1. 订阅,例如对IObservable<T>.Subscribe(SomeIObserver<T>)地图的调用fooInstance.BarEvent += barHandlerInstance.
  2. 调用,例如调用barHandlerInstance(int x, string y)地图SomeObserver.OnNext(T arg)
  3. 取消订阅,例如假设我们将调用的返回IDisposable处理程序保存Subscribe到一个名为的变量中subscription,然后调用subscription.Dispose()map fooInstance.BarEvent -= barHandlerInstance.

请注意,只有调用的行为才会Subscribe创建订阅.因此,Observable.FromEvent调用返回一个工厂,支持从底层事件订阅,调用和取消订阅.此时,没有事件订阅发生.只有在调用时才Subscribe会有Observer和它的OnNext处理程序.因此,FromEvent调用必须接受它可以用来在适当的时间实现三个桥接操作的工厂方法.

FromEvent类型参数

所以现在让我们考虑FromEvent上述事件的正确实现.

回想一下,OnNext处理程序只接受一个参数..NET事件处理程序可以包含任意数量的参数.因此,我们的第一个决定是选择单个类型来表示目标可观察流中的事件调用.

实际上,这可以是您希望在目标可观察流中出现的任何类型.转换函数(稍后讨论)的工作是提供将事件调用转换为OnNext调用的逻辑 - 并且有足够的自由来决定如何发生这种情况.

在这里,我们将int x, string yBarEvent调用的参数映射到描述这两个值的格式化字符串.换句话说,我们将调用fooInstance.RaiseBar(1, "a")to导致调用someObserver.OnNext("X:1 Y:a").

这个例子应该是一个非常常见的混淆源:FromEvent代表什么类型的参数?这里第一种类型BarHandler源.NET事件委托类型,第二种类型是目标OnNext处理程序的参数类型.因为第二种类型通常是EventArgs子类,所以通常认为它必须是.NET事件委托的必要部分 - 很多人都错过了它的相关性实际上是由于OnNext处理程序.所以我们FromEvent调用的第一部分看起来像这样:

 var observableBar = Observable.FromEvent<BarHandler, string>(
Run Code Online (Sandbox Code Playgroud)

转换函数

现在让我们考虑第一个参数FromEvent,即所谓的转换函数.(注意,有些重载会FromEvent忽略转换函数 - 稍后会详细介绍.)

由于类型推断,lambda语法可以被截断很多,所以这是一个开头的长版本:

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

因此,此转换函数是一个工厂函数,在调用时会创建与底层.NET事件兼容的处理程序.工厂函数接受OnNext委托.应该由返回的处理程序调用此委托,以响应使用基础.NET事件参数调用的处理程序函数.将调用委托,其结果是将.NET事件参数转换为OnNext参数类型的实例.因此,从上面的示例中我们可以看到工厂函数将使用onNextHandler类型Action<string>调用 - 必须使用字符串值调用它以响应每个.NET事件调用.工厂函数BarHandler为.NET事件创建一个类型的委托处理程序,它通过调用onNextHandler带有从相应事件调用的参数创建的格式化字符串来处理事件调用.

通过一些类型推断,我们可以将上面的代码折叠为以下等效代码:

onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)
Run Code Online (Sandbox Code Playgroud)

因此,转换函数在提供创建适当事件处理程序的函数时满足某些事件订阅逻辑,并且还完成将.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);
Run Code Online (Sandbox Code Playgroud)

由我们来决定如何获取我们将要桥接的事件 - 因此我们提供了添加和删除处理函数,这些函数期望提供创建的转换处理程序.该事件通常通过闭包捕获,如上例中我们关闭foo实例.

现在我们拥有FromEventobservable的所有部分来完全实现订阅,调用和取消订阅.

还有一件事...

最后一块胶水值得一提.Rx优化了对.NET事件的订阅.实际上,对于可观察量的任何给定数量的订阅者,只对基础.NET事件进行一次订阅.然后通过该Publish机制将其多播到Rx订户.这就像是Publish().RefCount()附加到了observable一样.

使用上面定义的委托和类考虑以下示例:

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();
}
Run Code Online (Sandbox Code Playgroud)

这产生了以下输出,证明只进行了一次订阅:

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
Run Code Online (Sandbox Code Playgroud)

我帮助解决了这个复杂功能如何工作的任何挥之不去的困惑!

  • +1精彩的解释,为我节省了一些时间 (2认同)