How to execute async code on DispatcherQueue (WinUI) without using async void?

335 views Asked by At

I currently have some code to help execute async code on the main thread using DispatcherQueue in a WinUI 3 application using .Net 7.

// Note that this is a simplified version of the original code, for brevity.
public static async Task Execute(this DispatcherQueue dispatcher, Func<Task> function)
{
  if (dispatcher.HasThreadAccess)
  {
    await function();
  }
  else
  {
    var taskCompletionSource = new TaskCompletionSource<bool>();
    dispatcher.TryEnqueue(ExecuteUI);
    await taskCompletionSource.Task;
  }

  static async void ExecuteUI()
  {
    try
    {
      await function();
      taskCompletionSource.SetResult(true);
    }
    catch (Exception e)
    {
      taskCompletionSource.SetException(e);
    }
  }
}

What would be the best way to implement this feature without using async void?


Edit:

I understand that the DispatcherQueue.TryEnqueue overloads take Action or Func parameters that are not returning Task. My question in about about bridging that sync-to-async gap with a solution that complies with this rule.

Rule #4. Never define async void methods. Make the methods return Task instead.

  • Exceptions thrown from async void methods always crash the process.
  • Callers don't even have the option to await the result.
  • Exceptions can't be reported to telemetry by the caller.
  • It's impossible for your VS package to responsibly block in Package.Close till your async work is done when it was kicked off this way.
  • Be cautious: async delegate or async () => become async void methods when passed to a method that accepts Action delegates. Only pass async delegates to methods that accept Func<Task> or Func<Task<T>> parameters.
2

There are 2 answers

5
mm8 On

Install the CommunityToolkit.WinUI NuGet package and use one of the overloads of the EnqueueAsync method that accept a Func<Task> or Func<Task<T>>.

The source code for these methods are available on GitHub.

0
Stephen Cleary On

My question in about about bridging that sync-to-async gap with a solution that complies with this rule.

The referenced rule is specifically for developing Visual Studio extensions. To quote from "Do I need to follow these rules?":

All code that runs in Visual Studio itself should follow these rules.

Any other GUI app that invokes asynchronous code that it must occasionally block the UI thread on is also recommended to follow these rules.

For context, Visual Studio extensions should never use async void because there's Task-returning equivalents for everything in that system.

The general-purpose recommendation for non-Visual Studio code is avoid async void, which is much less strict. Specifically, async void is the proper solution for event handlers or logically equivalent code.

It might be stretching a bit, but you could look at DispatcherQueue.TryEnqueue as a kind of manual event firing (from another thread). So I don't see anything wrong with async void here.

I understand that the DispatcherQueue.TryEnqueue overloads take Action or Func parameters that are not returning Task.

This is the API you're given, so it's what you have to work with.

There's only two real solutions: use async void or block (on the UI thread). Given the two, async void is far superior.