F# asynchronous event handlers for WPF similar to C#'s async and await F# asynchronous event handlers for WPF similar to C#'s async and await wpf wpf

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