Luke Puplett Luke Puplett - 1 month ago 19
C# Question

RunAsync - How do I await the completion of work on the UI thread?

When awaiting

Dispatcher.RunAsync
the continuation occurs when the work is scheduled, not when the work has completed. How can I await the work completing?

Edit

My original question assumed the premature continuation was caused by the design of the API, so here's the real question.

When awaiting
Dispatcher.RunAsync
using an asynchronous delegate, using
await
within the delegate's code, the continuation occurs when the
await
is encountered, not when the work has completed. How can I await the work completing?

Edit 2

One reason you may need to dispatch work that's already on the UI thread is to workaround subtle timing and layout issues. It's quite common for values of sizes and positions of elements in the visual tree to be in flux and scheduling work for a later iteration of the UI can help.

Answer

I found the following suggestion on a Microsoft github repository: How to await a UI task sent from a background thread.

Setup

Define this extension method for the CoreDispatcher:

using System;
using System.Threading.Tasks;
using Windows.UI.Core;

public static class DispatcherTaskExtensions
{
    public static async Task<T> RunTaskAsync<T>(this CoreDispatcher dispatcher, 
        Func<Task<T>> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal)
    {
        var taskCompletionSource = new TaskCompletionSource<T>();
        await dispatcher.RunAsync(priority, async () =>
        {
            try
            {
                taskCompletionSource.SetResult(await func());
            }
            catch (Exception ex)
            {
                taskCompletionSource.SetException(ex);
            }
        });
        return await taskCompletionSource.Task;
    }

    // There is no TaskCompletionSource<void> so we use a bool that we throw away.
    public static async Task RunTaskAsync(this CoreDispatcher dispatcher,
        Func<Task> func, CoreDispatcherPriority priority = CoreDispatcherPriority.Normal) => 
        await RunTaskAsync(dispatcher, async () => { await func(); return false; }, priority);
}

Once you do that, all you need to do is use the new RunTaskAsync method to have your background task await on the UI work.

Usage example

Let's pretend that this is the method that needs to run in the UI thread. Pay attention to the debug statements, which will help follow the flow:

public static async Task<string> ShowMessageAsync()
{
    // Set up a MessageDialog
    var popup = new Windows.UI.Popups.MessageDialog("Question", "Please pick a button to continue");
    popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 1"));
    popup.Commands.Add(new Windows.UI.Popups.UICommand("Button 2"));
    popup.CancelCommandIndex = 0;

    // About to show the dialog
    Debug.WriteLine("Waiting for user choice...");
    var command = await popup.ShowAsync();

    // Dialog has been dismissed by the user
    Debug.WriteLine("User has made a choice. Returning result.");
    return command.Label;
}

To await that from your background thread, this is how you would use RunTaskAsync:

// Background thread calls this method
public async void Object_Callback()
{
    Debug.WriteLine("Object_Callback() has been called.");

    // Do the UI work, and await for it to complete before continuing execution
    var buttonLabel = await Dispatcher.RunTaskAsync(ShowMessageAsync);

    Debug.WriteLine($"Object_Callback() is running again. User clicked {buttonLabel}.");
}

The output then looks like this:

Object_Callback() has been called.

Waiting for user choice...

User has made a choice. Returning result.

Object_Callback() is running again. User clicked Button 1.

Comments