2014-04-25 42 views
6

如何在F#中編寫異步WPF(或Windows窗體)事件處理程序?具體來說,是否有任何接近C#5的異步並等待的編碼模式?用於WPF的F#異步事件處理程序類似於C#的異步並等待

這是一個完整的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不能被修改。

+0

這篇文章應該是有幫助http://tomasp.net/blog/csharp-fsharp- async-intro.aspx/ –

回答

6

的第一步是使incrementSlowly異步的。這是在你的C#代碼,這可能不是一個好主意,實際上同步 - 在現實的情況下,這可能是與網絡進行通信,所以很多時候這實際上是異步的:

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

現在,你可以做按鈕點擊處理程序也是異步的。我們將使用Async.StartImmediate後來,以確保我們可以訪問UI元素開始,所以我們不必擔心dispatechers或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元素。

+0

如何在保持'incrementSlowly'同步的同時將它在新線程中運行,如原始C#代碼中那樣?我知道這不是F#中應該做的事,但我很好奇;) –

+0

您可以像C#中那樣做同樣的事情,但是使用'Async.AwaitTask'(然後使用'let!'結果來獲得值)。這將實現與C#代碼相同的行爲。 –

+0

在我的真實項目中,我無法訪問緩慢方法的源代碼。基於'Async.AwaitTask'的方法可能會更好。 – Wally

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