16.1.3 创建简单的有响应的应用程序
我们将建立一个小的 Windows 窗体应用程序,来演示这种类型的事件处理的强大。图 16.1 显示这个应用程序的主窗体的,从图中可以看出,预期的行为应该是相当明显。
图 16.1 我们通过单击按钮,改变显示在标签上的号码。
实现这个应用程序的一种常规方法,是创建一个可变的字段(或在 F# 中的可变的引用单元),当单击其中的一个按钮时,调用事件处理程序。事件处理程序将测试单击了哪个按钮,并递增或递减可变的状态,再将其显示在标签上。有很多其他可能的办法,但在大多数情况下,我们可能必须显式声明一些可变的状态。
现在,我们如何使用我们上一节介绍的函数,实现相同的行为,无需显式依赖可变的状态?一个好的有关声明性编码,在许多情况下,这样的代码可以轻松地实现可视化。对事件来说,这是真的,图 16.2 展示了我们的解决方案。
图 16.2 用在示例应用程序中的事件处理管道;在左边的两个框表示源事件,淡方框表示使用处理函数创建的事件。
这个想法是,我们将取这些事件,将它们转换成携带一个整数值的事件。我们将使用辅助函数 always 来实现,它返回一个函数,忽略它的参数,并始终返回相同的值。我们将使用它来创建事件,它携带 +1 或 –1,取决于单击了哪个按钮 。然后,可以合并这两个事件,使用 Observable.scan 函数来汇总事件所携带的值。
生成用户界面所需的代码不是很有趣,所以,清单 16.4 只显示设置事件处理的部分。
Listing 16.4 Pipeline for handling events (F#)
let always x = (fun _ -> x)
let incEvent = btnUp.Click |> Observable.map (always 1)
let decEvent = btnDown.Click |> Observable.map (always -1)
Observable.merge incEvent decEvent
|> Observable.scan (+) 0
|> Observable.add (fun sum –>
lbl.Text <- sprintf "Count: %d" sum)
我们首先声明一个工具函数 always,返回一个函数,它忽略其参数,返回的值,与参数指定的一样。接下来,我们开始实现事件处理代码。为了使它更具可读性,我们不编码整个管道,作为一个表达式。相反,我们首先声明两个辅助值,表示事件。
IncEvent 和 decEvent 的类型都是 IObservable<int>,这意味着,它们表示事件携带的整数。由事件所携带的值,Increment 按钮总是产生 +1,另一个事件的值始终产生 –1。要生成这个值,我们将使用辅助函数 always,返回一个函数,忽略参数,始终返回相同的值。清单 16.4 中,被忽略的值是 Click 事件的 EventArgs 参数。
我们合并这些事件,创建一个事件,每当单击其中的一个按钮就会触发。该事件携带整数值,所以,可以使用 Observable.scan 来汇总这些值,起始值从 0 开始。我们将使用加号运算符汇总,所以,对于每次单击,函数将加 +1 或 -1。最后,我们使用 Observable.add 函数,指定一个处理程序,它显示当前点击的总和。
处理与 IEvent<'T> 类型的值一样的 .NET 事件的能力(这会使它用高阶函数处理它们与),是 F# 的一项特殊功能。F# 自动把 .NET 事件打包到这种类型中,所以,我们可以把事件作为标准的值来使用。在 C# 中,使用同样的概念是很棘手的,但也可能努力一下,它很好地演示了声明式编程风格和 LINQ 的强大。