Asynchronous UI development in WinRT, Silverlight, Windows Phone & WPF with async/await keywords of C# 5.0

This is a reblog of my article from http://labs.vectorform.com

C# 5.0 comes with the new async/await keywords that make asynchronous code easier to write, read and maintain. This is very nice if you have properly declared methods that support this pattern and Windows Runtime (or WinRT – the API for Windows 8 Metro Style Apps) comes with a lot of these methods for long running tasks or ones of nondeterministic duration – especially in I/O or web calls. It is however completely lacking in support for asynchronous UI development, even though one of the main goals of these new keywords was support for responsive UI.

Interactive coding often introduces the need to start an operation, like an animation or a dialog box and then wait for an event before switching the state of the UI, for example: running another animation or removing a dialog. Reading this article you will learn how to do it better using the upcoming features of C# 5.0, regardless of whether you develop in WinRT, Silverlight, WPF, Windows Forms or even Web Forms. The source code that comes with this article contains a library you can use to cut the amount of code you need to write by half!

UI development of today and the past

If you develop applications using .NET Framework  you probably have to deal with a lot of events, event handlers, event subscriptions, state machines, flag fields, helper types, etc. If you want to sequence a series of state transitions that depend on events you have no choice but to break up the logic of your code into separate methods and event handlers, subsequently your code quickly gets a bit ugly and hard to follow.

For example, if you want to run some code after an animation completes you would have to add an event handler to the “Completed” event first, then start the animation in one method and finally execute the rest of the code from the event handler a bit like here:

public void RunAnimationThenDoSomething()
{
    storyboard.Begin();
}

private void storyboard_Completed(object sender, object e)
{
    DoSomething();
}

It seems fairly straightforward unless you have to remove the event handlers or have five other similar actions in a sequence – timer delays, waiting for button clicks, conditional blocks etc.

You can keep the code a bit more compact if you use lambda expressions, reactive extensions or iterator blocks, but they also make it more complicated. How about wrapping all the plumbing in #region blocks and collapsing them to leave only the logic visible in sequence? That would kind of work, but no. 🙂

In come async and await keywords…

How about getting rid of half of the event handlers, event subscriptions etc.? Let’s take a look at what you can do today if you develop for WinRT/Windows 8 or if you install the Async CTP or what you will be able to do in the near future.

WinRT does not help you much in terms of support awaitables in the UI classes, although you can wait for web requests, basic dialogs or just use Task.Delay() to delay execution:

await new HttpClient().GetAsync(uri);
await new MessageDialog("Done!").ShowAsync();
await Task.Delay(1000);

How about a single method call to wait for your UI to load, one to wait for a second, one to run and wait for a storyboard to complete, one to wait for a button click and even check which button was clicked?

public async void InteractWithUser()
{
    await this.WaitForLoaded();
    await showMessageStoryboard.BeginAsync();
    await Task.Delay(1000);
    await hideMessageStoryboard.BeginAsync();
    var button = await buttons.WaitForClickAsync();
    if (button == OKButton)
        Disk.Format("C:");
    else
        InteractWithUser();
}

The concept isn’t new – it seems like Daniel Earwicker in his C# 5.0 async/await and GUI events article mentioned having a “button as a task”. This here though is the implementation of the concept.

Awaitable extension methods

To add the new functionality to existing classes I am using extension methods. To make them awaitable, I am creating a task from a TaskCompletionSource that completes when the expected event occurs. I created a helper class based on Stephen Cleary’s post on MSDN forums, but modified to work with WinRT. Here are the extensions and helper code I came up with:

public static class StoryboardExtensions
{
    /// <summary>
    /// Begins a storyboard and waits for it to complete.
    /// </summary>
    public static async Task BeginAsync(this Storyboard storyboard)
    {
        await EventAsync.FromEvent(
            eh => storyboard.Completed += eh,
            eh => storyboard.Completed -= eh,
            storyboard.Begin);
    }
}

public static class ButtonExtensions
{
    /// <summary>
    /// Waits for the button Click event.
    /// </summary>
    public static async Task<RoutedEventArgs> WaitForClickAsync(this ButtonBase button)
    {
        return await EventAsync.FromRoutedEvent(
            eh => button.Click += eh,
            eh => button.Click -= eh);
    }
}

// Based on: http://social.msdn.microsoft.com/Forums/sk/async/thread/30f3339c-5e04-4aa8-9a09-9be72d9d9a1b
public static class EventAsync
{
    /// <summary>
    /// Creates a <see cref="System.Threading.Tasks.Task"/>
    /// that waits for an event to occur.
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// await EventAsync.FromEvent(
    ///     eh => storyboard.Completed += eh,
    ///     eh => storyboard.Completed -= eh,
    ///     storyboard.Begin);
    /// ]]>
    /// </example>
    /// <param name="addEventHandler">
    /// The action that subscribes to the event.
    /// </param>
    /// <param name="removeEventHandler">
    /// The action that unsubscribes from the event when it first occurs.
    /// </param>
    /// <param name="beginAction">
    /// The action to call after subscribing to the event.
    /// </param>
    /// <returns>
    /// The <see cref="System.Threading.Tasks.Task"/> that
    /// completes when the event registered in
    /// <paramref name="addEventHandler"/> occurs.
    /// </returns>
    public static Task<object> FromEvent(
        Action<EventHandler> addEventHandler,
        Action<EventHandler> removeEventHandler,
        Action beginAction = null)
    {
        return new EventHandlerTaskSource(
            addEventHandler,
            removeEventHandler,
            beginAction).Task;
    }

    /// <summary>
    /// Creates a <see cref="System.Threading.Tasks.Task"/>
    /// that waits for an event to occur.
    /// </summary>
    /// <example>
    /// <![CDATA[
    /// await EventAsync.FromEvent(
    ///     eh => button.Click += eh,
    ///     eh => button.Click -= eh);
    /// ]]>
    /// </example>
    /// <param name="addEventHandler">
    /// The action that subscribes to the event.
    /// </param>
    /// <param name="removeEventHandler">
    /// The action that unsubscribes from the event when it first occurs.
    /// </param>
    /// <param name="beginAction">
    /// The action to call after subscribing to the event.
    /// </param>
    /// <returns>
    /// The <see cref="System.Threading.Tasks.Task"/> that
    /// completes when the event registered in
    /// <paramref name="addEventHandler"/> occurs.
    /// </returns>
    public static Task<RoutedEventArgs> FromRoutedEvent(
        Action<RoutedEventHandler> addEventHandler,
        Action<RoutedEventHandler> removeEventHandler,
        Action beginAction = null)
    {
        return new RoutedEventHandlerTaskSource(
            addEventHandler,
            removeEventHandler,
            beginAction).Task;
    }

    private sealed class EventHandlerTaskSource
    {
        private readonly TaskCompletionSource<object> tcs;
        private readonly Action<EventHandler> removeEventHandler;

        public EventHandlerTaskSource(
            Action<EventHandler> addEventHandler,
            Action<EventHandler> removeEventHandler,
            Action beginAction = null)
        {
            if (addEventHandler == null)
            {
                throw new ArgumentNullException("addEventHandler");
            }

            if (removeEventHandler == null)
            {
                throw new ArgumentNullException("removeEventHandler");
            }

            this.tcs = new TaskCompletionSource<object>();
            this.removeEventHandler = removeEventHandler;
            addEventHandler.Invoke(EventCompleted);

            if (beginAction != null)
            {
                beginAction.Invoke();
            }
        }

        /// <summary>
        /// Returns a task that waits for the event to occur.
        /// </summary>
        public Task<object> Task
        {
            get { return tcs.Task; }
        }

        private void EventCompleted(object sender, object args)
        {
            this.removeEventHandler.Invoke(EventCompleted);
            this.tcs.SetResult(args);
        }
    }

    private sealed class RoutedEventHandlerTaskSource
    {
        private readonly TaskCompletionSource<RoutedEventArgs> tcs;
        private readonly Action<RoutedEventHandler> removeEventHandler;

        public RoutedEventHandlerTaskSource(
            Action<RoutedEventHandler> addEventHandler,
            Action<RoutedEventHandler> removeEventHandler,
            Action beginAction = null)
        {
            if (addEventHandler == null)
            {
                throw new ArgumentNullException("addEventHandler");
            }

            if (removeEventHandler == null)
            {
                throw new ArgumentNullException("removeEventHandler");
            }

            this.tcs = new TaskCompletionSource<RoutedEventArgs>();
            this.removeEventHandler = removeEventHandler;
            addEventHandler.Invoke(EventCompleted);

            if (beginAction != null)
            {
                beginAction.Invoke();
            }
        }

        /// <summary>
        /// Returns a task that waits for the routed to occur.
        /// </summary>
        public Task<RoutedEventArgs> Task
        {
            get { return tcs.Task; }
        }

        private void EventCompleted(object sender, RoutedEventArgs args)
        {
            this.removeEventHandler.Invoke(EventCompleted);
            this.tcs.SetResult(args);
        }
    }
}

How do I use it today?

The source code is available on CodePlex. It comes with a library that works on Windows 8 Developer Preview and a sample project that has a custom dialog class for WinRT implemented, both using the classic event driven approach as well as using the extension methods for comparison. It also has versions of the library and samples for Silverlight 4.0, Windows Phone 7.1 (Mango) and WPF – all of which require Visual Studio 2010 with Async CTP 3 installed.

Advertisements
Tagged , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: