New async APIs for WinForms in .NET 9

Posted: (EET/GMT+2)

 

WinForms development has traditionally required careful programming when mixing asynchronous code with the UI thread. Luckily with .NET 9, Microsoft introduced several new async APIs that make this easier.

WinForms user interface controls must generally be accessed from the UI thread. If some work runs on another thread, older code often uses Control.Invoke:

this.Invoke(() =>
{
    statusLabel.Text = "Done";
});

The problem is that the Invoke method blocks the calling thread while waiting for the UI thread to execute the delegate.

With .NET 9, you can use new InvokeAsync instead:

await statusLabel.InvokeAsync(() =>
{
    statusLabel.Text = "Done";
});

The delegate is queued to the WinForms UI thread and the returned task can be awaited. This fits much better with modern async C# code.

There are also overloads that can return a value:

int itemCount = await comboBox.InvokeAsync(
        () => comboBox.Items.Count);

Asynchronous callbacks are supported too, which is nice:

await statusLabel.InvokeAsync(async cancellationToken =>
{
    statusLabel.Text = "Loading...";

    // do some work here...
    await Task.Delay(1000, cancellationToken);

    statusLabel.Text = "Done";
});

.NET 9 also introduced experimental async APIs for showing forms and dialogs. For example:

Form detailsForm = new DetailsForm();
await detailsForm.ShowAsync();

And similarly for a modal dialog:

DialogResult result = await detailsForm.ShowDialogAsync();
if (result == DialogResult.OK)
{
    SaveChanges();
}

There is also an experimental async API for TaskDialog:

TaskDialogPage page = new TaskDialogPage()
    {
        Heading = "Processing",
        Text = "Please wait..."
    };

TaskDialogButton button = await TaskDialog.ShowDialogAsync(page);

These APIs are especially useful in applications that already contain asynchronous work, multiple forms, dependency injection, or long-running background operations.

For ordinary WinForms applications, the biggest practical improvement is probably Control.InvokeAsync.

This method removes some of the awkwardness around updating controls from asynchronous or background code and makes the code look more like the rest of modern C#.

However, keep in mind that async code does not mean that WinForms controls suddenly become thread-safe. UI access still needs to happen on the UI thread. InvokeAsync simply gives us a cleaner way to handle that work there. In Win32 API programming terms, the older Control.Invoke sends a message to the message queue, whereas Control.InvokeAsync uses a post instead. A send operation blocks, while posting doesn't.

Also note that in .NET 9, Form.ShowAsync, Form.ShowDialogAsync, and TaskDialog.ShowDialogAsync are experimental APIs. Check their current status before depending on them heavily in production code.

There's also a good post from Microsoft about this. Hope this helps!