2014-04-25 9 views
6

Come si codifica un gestore di eventi WPF asincrono (o Windows Form) in F #? In particolare, c'è qualche schema di codifica che approssima asincrono C# 5 e attendi?F # handler di eventi asincroni per WPF simili a asincroni di C# e attendi

Qui è una completa applicazione 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); 
    } 
} 

Sto avendo difficoltà a capire ciò che l'equivalente sarebbe utilizzando F #. Ho fatto diversi tentativi usando combinazioni di flussi di lavoro asincroni e metodi Asincroni. Diventa davvero complicato molto velocemente. Spero che ci sia un modo semplice che sto solo trascurando.

Ecco il mio punto di partenza, che blocca l'interfaccia utente a btn.Content <- incrementSlowly prev. Cosa faccio dopo?

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) 

A tal proposito, supporre che incrementSlowly non possa essere modificato.

+0

Questo articolo dovrebbe essere utile http://tomasp.net/blog/csharp-fsharp- async-intro.aspx/ –

risposta

6

Il primo passo è rendere incrementSlowly asincrono. Questo è in realtà sincrono nel codice C#, che probabilmente non è una buona idea - in uno scenario realistico, questo potrebbe essere in comunicazione con la rete, quindi molto spesso questo può effettivamente essere asincrona:

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

Ora, si può fare il gestore di clic del pulsante è anche asincrono. Inizieremo utilizzando Async.StartImmediate più tardi per fare in modo che siamo in grado di accedere agli elementi dell'interfaccia utente, in modo da non devono preoccuparsi di dispatechers o thread UI per ora:

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 } 

Il passo finale è quello di cambiare la registrazione dell'evento. Qualcosa del genere dovrebbe fare il trucco:

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

La cosa fondamentale è Async.StartImmediate che inizia il flusso di lavoro asincrono. Quando lo chiamiamo sul thread dell'interfaccia utente, assicura che tutto il lavoro effettivo venga eseguito sul thread dell'interfaccia utente (a meno che non lo scarichi esplicitamente in background) e quindi è sicuro accedere agli elementi dell'interfaccia utente nel codice.

+0

Come si fa a mantenere il comando "incrementSlowly" sincrono ed eseguendolo in un nuovo thread, come nel codice C# originale? So che non è così che vanno fatte le cose in F #, ma sono curioso;) –

+0

Si potrebbe fare la stessa cosa di C#, ma usare 'Async.AwaitTask' (e poi' let! 'Sul risultato per ottenere il valore).Ciò implementerebbe lo stesso comportamento del codice C#. –

+0

Sul mio progetto reale, non ho accesso all'origine per il metodo lento. L'approccio basato su 'Async.AwaitTask' potrebbe essere migliore. – Wally

0

Tomas sottolinea correttamente che se è possibile convertire il metodo lento in modo asincrono, quindi let! e Async.StartImmedate funzionano magnificamente. Questo è preferito.

Tuttavia, alcuni metodi lenti non hanno controparti asincrone. In tal caso, anche il suggerimento di Tomas su Async.AwaitTask funziona. Per completezza menziono un'altra alternativa, gestendo manualmente il marshalling con Async.SwitchToContext.

Async.AwaitTask un nuovo Task

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 

Gestisci manualmente contesto del thread

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