Run arbitrary code on the UI thread asynchronously

A gotcha and a solution for easily running async code on the UI thread from a background thread.

With the advent of the async and await keywords in .NET, it is super easy to run arbitrary code asynchronously on a background thread. Everybody is familiar with using Task.Run() to do something like this:

Debug.WriteLine("before Task.Run()");
await Task.Run(async () =>
{
    Debug.WriteLine("in Task.Run() - before long running process");
    await Task.Delay(1000);
    Debug.WriteLine("in Task.Run() - after long running process");
});
Debug.WriteLine("after Task.Run()");

The code is straight-forward. The calling code calls Task.Run() and waits for it to finish, including the await-ed code that is being run inside of the task. As expected, the output is:

before Task.Run()
in Task.Run() - before long running process
in Task.Run() - after long running process
after Task.Run()

So running async code on a background thread is dead easy. What about running async code on the UI thread? It is a common scenario - you have some background thread that reads data or whatever and you need to update some UI based on that operation. The usual tool to use in this case is the CoreDispatcher's RunAsync() method. If we rewrote our example above, it would look like:

Debug.WriteLine("before Dispatcher.RunAsync()");
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
    Debug.WriteLine("in Dispatcher.RunAsync() - before long running process");
    await Task.Delay(1000);
    Debug.WriteLine("in Dispatcher.RunAsync() - after long running process");
});
Debug.WriteLine("after Dispatcher.RunAsync()");

But if you run this code, you will notice something strange. The output is:

before Dispatcher.RunAsync()
in Dispatcher.RunAsync() - before long running process
after Dispatcher.RunAsync()
in Dispatcher.RunAsync() - after long running process

The outer await returned control to the method before the inner await was complete. Is it a bug in the framework? Nope. The difference is in the argument type passed to Task.Run() and CoreDispatcher.RunAsync(). In Task.Run(), the argument is of type Func<Task> so the resulting Task is also await-ed. But the argument passed to CoreDispatcher.RunAsync() is of type DispatchedHandler which is a delegate that returns void. Since it returns void, the inner code block cannot be await-ed and returns immediately, which returns control to the outer block which continues to execute.

So now that you know about this potential pitfall, what should you do about it? Well, the entire point of using the CoreDispatcher in the first place was to emulate Task.Run()'s ability to execute a block of code on another thread (specifically the UI thread). What we really need (and wanted all along) was something like UITask.Run() that takes the same Func<Task> argument and results in the same behavior but on the UI thread instead of a background thread.

So, I present to you the UITask class (full source code at the end of the article). If we rewrite our example:

Debug.WriteLine("before UITask.Run()");
await UITask.Run(async () =>
{
    Debug.WriteLine("in UITask.Run() - before long running process");
    await Task.Delay(1000);
    Debug.WriteLine("in UITask.Run() - after long running process");
});
Debug.WriteLine("after UITask.Run()");

Now we get the expected output:

before UITask.Run()
in UITask.Run() - before long running process
in UITask.Run() - after long running process
after UITask.Run()

Behind the scenes, the UITask class still relies on the CoreDispatcher to marshal the call back to the UI thread, but it wraps it in a TaskCompletionSource so we can wait until the inner code block is actually done. Note that because it still relies on the CoreDispatcher, you need to intialize UITask with an instance of the CoreDispatcher when you app starts up.

Full source for UITask:

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

namespace UITaskSample
{
    public static class UITask
    {
        static CoreDispatcher dispatcher;

        public static void Initialize(CoreDispatcher dispather)
        {
            UITask.dispatcher = dispather;
        }

        public static Task Run(Action action)
        {
            if (dispatcher == null)
                throw new InvalidOperationException("UITask must be initialized with a dispatcher before calling Run()");

            var tcs = new TaskCompletionSource<bool>();
            var ignore = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                try
                {
                    action();
                    tcs.TrySetResult(true);
                }
                catch (Exception ex)
                {
                    tcs.TrySetException(ex);
                }
            });
            return tcs.Task;
        }

        public static Task Run(Task task)
        {
            return Run(() => task);
        }

        public static Task Run(Func<Task> taskFunc)
        {
            if (dispatcher == null)
                throw new InvalidOperationException("UITask must be initialized with a dispatcher before calling Run()");

            var tcs = new TaskCompletionSource<bool>();
            var ignore = dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
            {
                try
                {
                    await taskFunc();
                    tcs.TrySetResult(true);
                }
                catch (Exception ex)
                {
                    tcs.TrySetException(ex);
                }
            });
            return tcs.Task;
        }
    }
}
 

@briandunnington


2018.03.25

The many uses of Azure Functions Proxies

Azure Functions Proxies are awesome - here are just a few ways to leverage them

View details »


2018.03.12

Build an Alexa skill using Azure Functions

Although AWS Lambdas are the default, it is dead simple to use Azure Functions for your Alexa skill as well

View details »


2018.02.28

Roll Your Own React

(on Roku, just for an added twist)

View details »


2018.02.06

Streaming live drone footage using Azure Media Services

Directly from your drone to the world, via the the cloud

View details »


2017.07.07

Redux + Roku = Redoku

Making Roku development less painful by making state changes more predictable

View details »


More Posts >