13

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.

Luke Puplett
  • 42,091
  • 47
  • 181
  • 266
  • This is not exactly right. The continuation occurs when the lambda returns. And when that lambda is actually `async void`, that's before it actually completes. But awaiting `RunAsync()` works just fine for synchronous lambdas. – svick Oct 02 '13 at 12:04
  • @svick I also thought that might be the case but I couldn't find any documentation as to when `RunAsync` is supposed to return. – Luke Puplett Oct 02 '13 at 14:43

4 Answers4

19

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.

Community
  • 1
  • 1
Mike
  • 2,220
  • 1
  • 18
  • 32
10

Your question is assuming that you want to schedule (and wait for) work on a UI thread from a background thread.

You'll usually find your code is much cleaner and easier to understand (and it will definitely be more portable) if you have the UI be the "master" and the background threads be the "slaves".

So, instead of having a background thread await some operation for the UI thread to do (using the awkward and unportable Dispatcher.RunAsync), you'll have the UI thread await some operation for the background thread to do (using the portable, made-for-async Task.Run).

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • The point is to await the dispatched work, irrespective of whether that work was invoked from a background thread or the UI thread. Often a background (or UI) thread needs to ensure something is done on the UI thread, and then do some other thing when its complete. The point of my solution is to orchestrate this within the async-await pattern and not resort to events or wait handles. – Luke Puplett Oct 02 '13 at 14:49
  • 1
    @LukePuplett: My point is that this is indicative of a more brittle design. At one time I "often" had to do this, but once I inverted that logic, my design became much cleaner. Allowing the UI thread to drive the logic embraces `async`/`await` *more* naturally than your solution and does not use events or wait handles at all. – Stephen Cleary Oct 02 '13 at 14:56
  • 2
    I think you're trying to answer a question about apples with an answer about oranges. There's a very specific problem here: knowing when this UI work is complete. This UI work is inside a custom UI control, and so it has to orchestrate its movements. Truth be told, it's all happening on the UI thread inside a control. Its just a way to use async-await to orchestrate a series of work that needs to be done. Your point about driving background tasks from the UI thread is correct in general, and how I work, but its not applicable here. It won't solve this problem. – Luke Puplett Oct 02 '13 at 15:04
  • @LukePuplett Sometimes, the solution to a very specific problem is to do something completely different, also known as [the XY problem](http://meta.stackexchange.com/q/66377). – svick Oct 02 '13 at 15:15
  • @LukePuplett Also, if it's all happening on the UI thread, what do you need `RunAsync()` for? – svick Oct 02 '13 at 15:16
  • @svick If I take out the `RunAsync` then the call will return with the `Task` only when its complete. I need to return a `Task` ASAP so the caller can await it. I can't easily use a proper `Task` since there's a lot of stuff that I'm interacting with that only allows access from the UI thread, which means coordinating with `RunAsync` again (no synchronous `Run`). I've effectively turned some UI work into a `Task`, which as well as being awaitable, is also nicer than dealing with events and callbacks. I think for this problem (all caused by the `Delay`), this is the simplest solution. – Luke Puplett Oct 02 '13 at 16:05
  • @LukePuplett “the call will return with the `Task` only when its complete” That's not how async methods work. It wouldn't make any sense, why would such methods return `Task` in the first place? – svick Oct 02 '13 at 17:11
  • @LukePuplett: I also don't see the need for `RunAsync` here, since your calling code is running on the UI thread; a simple `async Task` method should suffice. This does feel a bit like an "XY" conversation; if you give us a higher-level problem we may find a better higher-level solution. When I stated that I *used* to think this way, I'm speaking from years of `async` experience and ~50 projects. E.g. my progress reporting [used to look like this](http://blog.stephencleary.com/2010/06/reporting-progress-from-tasks.html) but now [looks like this](http://tinyurl.com/pgpht35). – Stephen Cleary Oct 02 '13 at 18:04
  • @svick For a method to be awaitable it must return a Task. If the method was to called only on the foreground thread then.. actually see my edit below. – Luke Puplett Oct 03 '13 at 09:22
  • 1
    @LukePuplett No, it doesn't. See the beginning of [*await anything;*](http://blogs.msdn.com/b/pfxteam/archive/2011/01/13/10115642.aspx). – svick Oct 03 '13 at 10:13
  • @svick Err, I think @StephenCleary is right here (and I thought you were, too), `async Task` is correct for a UI-only awaitable method. Btw, I posted this on SO to demonstrate a particular technique (using TCS). I prefer the QA format to my blog since it will engage other programmers in conversation. I didn't expect the approach to be questioned. – Luke Puplett Oct 03 '13 at 10:20
  • @svick - If you're alluding to the `TaskAwaiter` and extension method as another way to do this, then that's really cool +1 – Luke Puplett Oct 03 '13 at 10:23
5

You can wrap the call to RunAsync in your own asynchronous method that can be awaited and control the completion of the task and thus the continuation of awaiting callers yourself.

Since async-await is centred on the Task type, you must orchestrate the work using this type. However, usually a Task schedules itself to run on a threadpool thread and so it cannot be used to schedule UI work.

However, the TaskCompletionSource type was invented to act as a kind of puppeteer to an unscheduled Task. In other words, a TaskCompletionSource can create a dummy Task that is not scheduled to do anything, but via methods on the TaskCompletionSource can appear to be running and completing like a normal job.

See this example.

public Task PlayDemoAsync()
{
    var completionSource = new TaskCompletionSource<bool>();
    this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, async () =>
    {
        try
        {
            foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
            {
                // For each subsequent stroke plot, we need to start a new figure.
                //
                if (this.Sketch.DrawingPoints.Any())
                    this.Sketch.StartNewFigure(ppc.First().Position);

                foreach (var point in ppc)
                {
                    await Task.Delay(100);

                    this.Sketch.DrawingPoints.Add(point.Position);
                }
            }

            completionSource.SetResult(true);
        }
        catch (Exception e)
        {
            completionSource.SetException(e);
        }
    });

    return (Task)completionSource.Task;
}

Note: the main work being done on the UI thread is just some lines being drawn on screen every 100ms.

A TaskCompletionSource is created as the puppet master. Look near the end and you'll see that it has a Task property that is returned to the caller. Returning Task satisfies the compilers needs and makes the method awaitable and asynchronous.

However, the Task is just a puppet, a proxy for the actual work going on in the UI thread.

See how in that main UI delegate I use the TaskCompletionSource.SetResult method to force a result into the Task (since returned to the caller) and communicate that work has finished.

If there's an error, I use SetException to 'pull another string' and make it appear that an exception has bubbled-up in the puppet Task.

The async-await subsystem knows no different and so it works as you'd expect.

Edit

As prompted by svick, if the method was designed to be callable only from the UI thread, then this would suffice:

    /// <summary>
    /// Begins a demonstration drawing of the asterism.
    /// </summary>
    public async Task PlayDemoAsync()
    {
        if (this.Sketch != null)
        {
            foreach (var ppc in this.Plots.Select(p => this.TransformPlot(p, this.RenderSize)))
            {
                // For each subsequent stroke plot, we need to start a new figure.
                //
                if (this.Sketch.DrawingPoints.Any())
                    this.Sketch.StartNewFigure(ppc.First().Position);

                foreach (var point in ppc)
                {
                    await Task.Delay(100);

                    this.Sketch.DrawingPoints.Add(point.Position);
                }
            }
        }
    }
Luke Puplett
  • 42,091
  • 47
  • 181
  • 266
1

A nice way to work the clean way @StephenCleary suggests even if you have to start from a worker thread for some reason, is to use a simple helper object. With the object below you can write code like this:

    await DispatchToUIThread.Awaiter;
    // Now you're running on the UI thread, so this code is safe:
    this.textBox.Text = text;

In your App.OnLaunched you have to initialize the object:

    DispatchToUIThread.Initialize(rootFrame.Dispatcher);

The theory behind the code below you can find at await anything;

public class DispatchToUIThread : INotifyCompletion
{
    private readonly CoreDispatcher dispatcher;

    public static DispatchToUIThread Awaiter { get; private set; }

    private DispatchToUIThread(CoreDispatcher dispatcher)
    {
        this.dispatcher = dispatcher;
    }

    [CLSCompliant(false)]
    public static void Initialize(CoreDispatcher dispatcher)
    {
        if (dispatcher == null) throw new ArgumentNullException("dispatcher");
        Awaiter = new DispatchToUIThread(dispatcher);
    }

    public DispatchToUIThread GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get { return this.dispatcher.HasThreadAccess; }
    }

    public async void OnCompleted(Action continuation)
    {
        if (continuation == null) throw new ArgumentNullException("continuation");
        await this.dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => continuation());
    }

    public void GetResult() { }
}
Marcel Wolterbeek
  • 3,367
  • 36
  • 48
  • In the original preview of `async`-`await`, there was a method to do this. [But it was removed, because doing it this way is considered a bad practice.](http://stackoverflow.com/a/15364646/41071) – svick Mar 19 '14 at 14:06
  • @svick it may be considered bad practise, but sometimes (like when you want to do UI-related stuff in the SplashScreen.Dismissed event) you don't have a choice. In the example I mentioned, I discovered that the SplashScreen.Dismissed event does NOT run on the UI thread, meaning that the stuff you expect to do in the associated handler is off-limits--unless you use this class to force it to run the remainder on the UI thread. – Tom Lint Oct 11 '14 at 18:59