Question

How does one code an asynchronous WPF (or Windows Forms) event handler in F#? Specifically, is there any coding pattern that approximates C# 5's async and await?

Here is a complete C# WPF app:

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);
    }
}

I am having trouble figuring out what the equivalent would be using F#. I have made several attempts using combinations of async workflows and Async methods. It just gets really messy real fast. I'm hoping there is an easy way that I'm just overlooking.

Here is my starting point, which locks up the UI at btn.Content <- incrementSlowly prev. What do I do next?

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)

By the way, assume that incrementSlowly cannot be modified.

Was it helpful?

Solution

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.

OTHER TIPS

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
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top