F# asynchronous event handlers for WPF similar to C#'s async and await
The first step is to make incrementSlowly
asynchronous. This is actually synchronous in your C# code, which is probably not a good idea - in a realistic scenario, this could be communicating with network, so very often this can actually be asynchronous:
let incrementSlowly previous = async { do! Async.Sleep(3000) if previous = 2 then failwith "Oops!" return previous + 1 }
Now, you can make the button click handler also asynchronous. We'll start it using Async.StartImmediate
later to make sure that we can access UI elements, so we do not have to worry about dispatechers or UI threads for now:
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 }
The final step is to change the event registration. Something like this should do the trick:
btn.Click.Add(RoutedEventHandler(fun sender e -> btn_Click sender e |> Async.StartImmediate)
The key thing is Async.StartImmediate
which starts the asynchronous workflow. When we call this on the UI thread, it ensures that all the actual work is done on the UI thread (unless you offload it explicitly to background) and so it is safe to access UI elements in your code.
Tomas correctly points out that if you can convert the slow method to be asynchronous, then let!
and Async.StartImmedate
work beautifully. That is preferred.
However, some slow methods do not have asynchronous counterparts. In that case, Tomas's suggestion of Async.AwaitTask
works too. For completeness I mention another alternative, manually managing the marshalling with Async.SwitchToContext
.
Async.AwaitTask a new 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
Manually manage thread context
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