如何使用 Observable.FromEvent 代替 FromEventPattern 并避免使用字符串文字事件名称。

41

我正在学习如何在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));
});

这个程序运行完美,可以捕获每次按键事件并将它们按照按下的键分组,并跟踪每个键被按下的次数。随后会使用键和新的按键次数调用 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.FromEvent 代替了 Observable.FromEventPattern(以及在 Select LINQ 查询中获取 KeyChar 的路径)。其他部分,包括 Subscribe 方法都是相同的。 然而,在运行时使用第二个解决方案时,我得到了以下错误:

An unhandled exception of type 'System.ArgumentException' occurred in mscorlib.dll

Additional information: Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.

什么导致了这个运行时异常,我应该如何避免它?

  • GUI:WinForms
  • Rx和Rx-WinForms版本:2.1.30214.0(通过Nuget获取)
  • 目标框架:4.5
2个回答

113

摘要

首先要指出的是,您实际上不需要使用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订阅者。它接受观察者(Action<T>)的OnNext处理程序,并返回与底层事件委托兼容的处理程序,该处理程序将调用该OnNext处理程序。然后可以订阅生成的处理程序到事件中。
我从来不喜欢官方MSDN文档对此函数的解释,所以在这里,我将逐步详细解释如何使用该函数。

Observable.FromEvent的基本原理

以下内容将分解说明为什么FromEvent存在以及如何工作:

.NET事件订阅方式的回顾

考虑.NET事件的工作方式。这些事件被实现为委托链。标准事件委托遵循delegate void FooHandler(object sender, EventArgs eventArgs)的模式,但实际上事件可以使用任何委托类型(甚至带有返回类型的委托)。我们通过将适当的委托传递到特殊函数中(通常是通过+=运算符)将其添加到委托链中来订阅事件,或者如果尚未订阅任何处理程序,则该委托成为链的根。这就是为什么在引发事件时必须进行空值检查的原因。
当事件被触发时,(通常)委托链被调用,以便依次调用链中的每个委托。要取消订阅.NET事件,必须将委托传递到特殊函数中(通常是通过-=运算符),以便它可以从委托链中删除(链会遍历,直到找到匹配的引用,并从链中移除该链接)。
现在我们创建一个简单但非标准的.NET事件实现。这里我使用不太常见的add/remove语法公开了底层的委托链,并使我们能够记录订阅和取消订阅日志。我们的非标准事件具有具有整数和字符串参数的委托,而不是通常的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);
        }
    }
}

Rx订阅方式的回顾

现在考虑Observable流的工作原理。通过调用Subscribe方法并传递实现了IObserver<T>接口的对象来形成对Observable的订阅,这个接口具有OnNextOnCompletedOnError方法,由Observable调用它们来处理事件。此外,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. 取消订阅,例如假设我们将从Subscribe调用中保留返回的IDisposable句柄到名为subscription的变量中,那么对subscription.Dispose()的调用将映射到fooInstance.BarEvent -= barHandlerInstance

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

FromEvent类型参数

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

请注意,OnNext 处理程序仅接受单个参数。.NET 事件处理程序可以具有任意数量的参数。因此,我们的第一个决定是选择单个类型来表示目标可观察流中的事件调用。
实际上,这可以是您想要在目标可观察流中出现的任何类型。转换函数(稍后讨论)的工作是提供将事件调用转换为 OnNext 调用的逻辑 - 并且有足够的自由度来决定如何进行此操作。
在这里,我们将 BarEvent 调用的 int x, string y 参数映射到描述两个值的格式化字符串中。换句话说,我们将导致对 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的重载会省略转换函数 - 这个稍后再说。)

由于类型推断,lambda语法可以被缩短很多,因此这里先从一个长形式开始:

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

因此,这个转换函数是一个工厂函数,当调用时会创建与底层.NET事件兼容的处理程序。工厂函数接受一个OnNext委托。返回的处理程序应该在底层.NET事件参数被调用时调用该委托。委托将被调用以将.NET事件参数转换为OnNext参数类型的实例。因此,从上面的示例中我们可以看到,工厂函数将使用类型为Action<string>的onNextHandler进行调用 - 它必须在每个.NET事件调用的响应中使用字符串值进行调用。工厂函数创建了一个BarHandler类型的委托处理程序,用于处理通过调用onNextHandler并使用对应事件调用的参数创建的格式化字符串的事件调用。
通过一些类型推断,我们可以将上述代码折叠成以下等效代码:
onNextHandler => (int x, string y) => onNextHandler("X:" + x + " Y:" + y)

因此,转换函数在提供创建适当事件处理程序的功能方面实现了一些事件订阅逻辑,并且还执行将.NET事件调用映射到Rx OnNext处理程序调用所需的工作。
正如先前提到的,FromEvent有一些重载函数省略了转换函数。这是因为如果事件委托已经与OnNext所需的方法签名兼容,则不需要它。
剩下的两个参数是addHandler和removeHandler,它们负责将创建的委托处理程序订阅和取消订阅到实际的.NET事件中 - 假设我们有一个名为fooFoo实例,那么完成的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可观察对象完全实现订阅、调用和取消订阅所需的组件。

只剩最后一步...

还有一件重要的事情要提到。Rx优化了对.NET事件的订阅。实际上,对于任何给定数量的可观察者订阅者,只会对底层.NET事件进行单个订阅。然后通过Publish机制将其多播到Rx订阅者。就像向可观察对象附加了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

我希望这篇文章能够帮助你更好地理解这个复杂的函数是如何工作的!


1
同意,但是 FromEvent 方法的第一个版本有什么意义呢?它不需要用户的转换函数。似乎 RX 正试图提供此转换的默认实现(感谢 ILSpy)ReflectionUtils.CreateDelegate<TDelegate>(onNext, typeof(Action<TEventArgs>).GetMethod("Invoke"));),但它并没有创建一个好的委托。是这样吗?使用与 FromEventPattern 相同的签名的第一个版本可以解决这个问题。 - user195275
FromEvent 存在的目的是处理所有委托类型,包括那些不符合标准 .NET 事件模式的类型。如果该委托恰好是 Action<T>,则无需进行转换。然而,FromEventPattern 仅适用于传统事件委托。 - James World
1
+1 这对我理解转换函数的工作原理以及将其与 ComponentDispatcher.ThreadPreprocessMessage 事件配合使用非常有帮助。 - JMIII
当使用FromEvent(...)时,如果出现诸如System.Reflection.TargetInvocationExceptionSystem.Reflection.TargetParameterCountException等嵌入式异常,则应编写转换处理程序并返回与事件使用的相同组合((sender, args))的Observable,或者使用FromEventPattern - this.myself

0
为了避免使用字符串字面量事件名称,可以使用nameof运算符:
var keyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, nameof(KeyPress))
                        .Select(k => k.EventArgs.KeyChar)
                        .GroupBy(k => k);

不要像硬编码事件名称一样
var keyPresses = Observable.FromEventPattern<KeyPressEventArgs>(this, "KeyPress")
                        .Select(k => k.EventArgs.KeyChar)
                        .GroupBy(k => k);

请注意,nameof 运算符仅适用于 C# 6 或更高版本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接