16.4.5 等待多个事件
到目前为止,所有 AwaitObservable 示例中,我们只是等待单个事件发生。矩形绘制应用程序首先等待 MouseDown 事件,然后,反复等待 MouseMove。如果想要等待有,既可能有 MouseMove 事件,也可能一些其他事件发生,又该如何呢?我们可能想要能够以某种方式,放弃绘制的新矩形。我们就来实现这种情况,使用按键作为我们的取消事件。
我们需要改变 drawingLoop 函数的返回类型。现在,当函数返回时,有两种可能性:或者用户已绘制了一个矩形(在这种情况下,调用函数需要知道它的详细信息),或者用户取消了绘制。对于选项类型,这是一个完美的匹配:返回类型是最初的 Async<int * int>,在更改后,它将变成 Async<option<int *int>>。当然,我们不需要显式更改返回类型:F# 编译器正常可以推断出来。
当用户点击了 Esc 键时,我们就会停止 drawingLoop,结果返回 None。为此,我们需要等待 MouseMove 或 KeyDown 事件,并处理第一个发生的。可以在清单 16.18 中,找到 drawingLoop 函数修改后的代码。
Listing 16.18 Drawing rectangle with cancellation using the Esc key (F#)
let rec drawingLoop(clr, from) = async {
let! args = Async.AwaitObservable(form.MouseMove, form.KeyDown)
match args with
| Choice1Of2(move) when
MouseButtons.Left =
(move.Button &&& MouseButtons.Left) –>
let! rects = state.AsyncGetRectangles()
redrawWindow(rects)
drawRectangle(clr, from, (move.X, move.Y))
return! drawingLoop(clr, from)
| Choice1Of2(move) –>
return Some(move.X, move.Y)
| Choice2Of2(key) when key.KeyCode = Keys.Escape –>
form.Invalidate();
return None
| _ -> return! drawingLoop(clr, from) }
在前面所有的示例中,我们都使用了 AwaitObservable 方法,只取一个事件作为参数的。然而,本书的源代码还实现了重载,可以指定多个事件,并等待指定事件中任何第一个发生的,忽略任何随后出现的。在我们的例子中,这意味着,调用将阻塞,直到移动鼠标,或者按下一个键;它将运行处理代码。如果这个处理以递归调用结束,那么,AwaitObservable 会再次调用,以等待下一个事件,但其他后序发生的将被忽略。为了更好地理解如何重载两个参数的运行,让我们看一下它的类型签名:
AwaitObservable : IObservable<'T> * IObservable<'U> -> Async<Choice<'T * 'U>>
当 AwaitObservable 返回时,我们需要知道哪一个事件发生了,它携带了什么参数。此外,由事件携带的值可以有不同于所有提供的事件。在这种情况下,这个方法不能简单地返回携带的参数,因此,要看一下返回值的类型。Choice 的类型是一个泛型的差别联合,可以表示几个选项中的一个。这个类型是通过类型参数的数量重载的。在清单 16.18 中,我们有两种不同的选择,所以,Args 值的类型是 Choice<MouseEventArgs, KeyEventArgs>。
当 MouseMove 事件首先发生时, 返回的值将使用差别联合构造函数 Choice1Of2,携带有关鼠标事件的信息;否则,将使用构造函数 Choice2Of2, 值的类型是 KeyEventArgs。如果想等待更多的事件,可以使用一个名字,比如 Choice1Of3 等。
适当地响应多个事件,是本身就适合模式匹配的任务。第一个分支,是当用户所一直按下鼠标左键并移动时,被调用。在这种情况下,我们更新窗口,然后继续绘制。如果鼠标按钮被释放,将调用下一个分支。这意味着,用户完成了绘制,因此,可以返回矩形的最后位置。
最后两个分支响应 KeyDown 事件。我们再次使用了 when 子句,这次要确定按下的键是否是 Esc。如果是,就取消绘制处理,结果返回 None;否则,忽略键盘事件,并继续等待其他事件。
已更改的 drawingLoop 函数,最后一步是调整 waitingLoop 函数,以便矩形实际绘制时,才发送 AddRectangle 消息。这是一个简单的改变,没有引入任何新的概念,因此,可以在本书的网站上找到完整的源代码。
这一节,我们首先讨论了如何使用邮箱处理器来存储应用程序的状态,在这种情况下,我们需要处理各种事件。到目前为止,在所有的示例中,我们都限于事件来自用户界面。邮箱处理器的一个重要特点,是它们还可在并发的情况下。在下一节中,我们将讨论这个主题。