WPF的F#异步事件处理程序类似于C#的异步和等待

时间:2014-04-25 00:06:07

标签: .net wpf asynchronous f#

如何在F#中编写异步WPF(或Windows窗体)事件处理程序?具体来说,是否有任何编码模式接近C#5的异步并等待?

这是一个完整的C#WPF应用程序:

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

class Program
{
    static int IncrementSlowly(int previous)
    {
        Thread.Sleep(3000);
        if (previous == 2) throw new Exception("Oops!");
        return previous + 1;
    }

    static async void btn_Click(object sender, RoutedEventArgs e)
    {
        var btn = sender as Button;
        btn.IsEnabled = false;
        try
        {
            var prev = (int)btn.Content;
            btn.Content = await Task.Run(() => IncrementSlowly(prev));
        }
        catch (Exception ex)
        {
            btn.Content = ex.Message;
        }
        finally
        {
            btn.IsEnabled = true;
        }
    }

    [STAThread]
    static void Main(string[] args)
    {
        var btn = new Button() { Content = 0 };
        var win = new Window() { Content = btn };
        btn.Click += btn_Click;
        new Application().Run(win);
    }
}

我无法弄清楚使用F#的等价物。我使用异步工作流和异步方法的组合进行了多次尝试。它真的很快就真的很乱。我希望有一个简单的方法,我只是忽略。

这是我的起点,它在btn.Content <- incrementSlowly prev锁定了用户界面。我接下来该怎么办?

open System
open System.Threading
open System.Threading.Tasks
open System.Windows
open System.Windows.Controls

let incrementSlowly previous = 
    Thread.Sleep(3000)
    if previous = 2 then failwith "Oops!"
    previous + 1

let btn_Click (sender : obj) e = 
    let btn = sender :?> Button
    btn.IsEnabled <- false
    try 
        try 
            let prev = btn.Content :?> int
            btn.Content <- incrementSlowly prev
        with ex -> btn.Content <- ex.Message
    finally
        btn.IsEnabled <- true

[<EntryPoint>][<STAThread>]
let main _ = 
    let btn = new Button(Content = 0)
    let win = new Window(Content = btn)
    btn.Click.AddHandler(RoutedEventHandler(btn_Click))
    Application().Run(win)

顺便说一下,假设incrementSlowly无法修改。

2 个答案:

答案 0 :(得分:6)

第一步是使incrementSlowly异步。这在C#代码中实际上是同步的,这可能不是一个好主意 - 在现实的情况下,这可能是与网络通信,所以通常这实际上可以是异步的:

let incrementSlowly previous = async {
  do! Async.Sleep(3000)
  if previous = 2 then failwith "Oops!"
  return previous + 1 }

现在,您可以使按钮单击处理程序也异步。我们稍后会使用Async.StartImmediate启动它以确保我们可以访问UI元素,因此我们现在不必担心调度程序或UI线程:

let btn_Click (sender : obj) e = async {
  let btn = sender :?> Button
  btn.IsEnabled <- false
  try 
    try 
      let prev = btn.Content :?> int
      let! next = incrementSlowly prev
      btn.Content <- next
    with ex -> btn.Content <- ex.Message
  finally
    btn.IsEnabled <- true }

最后一步是更改活动注册。这样的事情可以解决问题:

btn.Click.Add(RoutedEventHandler(fun sender e ->
  btn_Click sender e |> Async.StartImmediate)

关键是Async.StartImmediate启动异步工作流程。当我们在UI线程上调用它时,它确保所有实际工作都在UI线程上完成(除非你明确地将其卸载到后台),因此访问代码中的UI元素是安全的。

答案 1 :(得分:0)

Tomas正确地指出,如果你可以将慢速方法转换为异步,那么let!Async.StartImmedate可以很好地工作。这是首选。

但是,一些慢速方法没有异步对应方法。在这种情况下,托马斯对Async.AwaitTask的建议也有效。为了完整起见,我提到了另一种方法,即使用Async.SwitchToContext手动管理编组。

Async.AwaitTask新任务

let btn_Click (sender : obj) e = 
  let btn = sender :?> Button
  btn.IsEnabled <- false
  async {
    try 
      try 
        let prev = btn.Content :?> int
        let! next = Task.Run(fun () -> incrementSlowly prev) |> Async.AwaitTask
        btn.Content <- next
      with ex -> btn.Content <- ex.Message
    finally
      btn.IsEnabled <- true
  }
  |> Async.StartImmediate

手动管理线程上下文

let btn_Click (sender : obj) e = 
  let btn = sender :?> Button
  btn.IsEnabled <- false
  let prev = btn.Content :?> int
  let uiContext = SynchronizationContext.Current
  async {
    try
      try
        let next = incrementSlowly prev
        do! Async.SwitchToContext uiContext
        btn.Content <- next
      with ex ->
        do! Async.SwitchToContext uiContext
        btn.Content <- ex.Message
    finally
      btn.IsEnabled <- true
  }
  |> Async.Start