How to await asynchronous tasks in the constructor in C#

For original inspiration see this excellent link by Stephen Cleary:

http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html

Step 1: Create a new WPF application

asyncwpf1

Step 2: Define your example service

In this example, a service to count the number of bytes in a web page:

MyStaticService.cs

using System;
using System.Net.Http;
using System.Threading.Tasks;

namespace AsyncWpf
{
    public static class MyStaticService
    {
        public static async Task<int> CountBytesInUrlAsync(string url)
        {
            // Artificial delay to show responsiveness.
            await Task.Delay(TimeSpan.FromSeconds(3)).ConfigureAwait(false);

            // Download the actual data and count it.
            using (var client = new HttpClient())
            {
                var data = await client.GetByteArrayAsync(url).ConfigureAwait(false);
                return data.Length;
            }
        }
    }
}

Step 3: Define a type for obtaining the results or errors

This method takes a task representing the asynchronous operation, and asynchronously waits for it to complete.

This method has an empty catch clause – so as not to propagate exceptions directly back to the main UI loop, but rather capture the exceptions and set the properties so that the error handling is done via data binding.

When completed, the type raises PropertyChanged notifications for all appropriate properties.

NotifyTaskCompletion.cs

using System;
using System.ComponentModel;
using System.Threading.Tasks;

namespace AsyncWpf
{
    public sealed class NotifyTaskCompletion<TResult> : INotifyPropertyChanged
    {
        public NotifyTaskCompletion(Task<TResult> task)
        {
            Task = task;
            if (!task.IsCompleted)
            {
                var _ = WatchTaskAsync(task);
            }
        }
        private async Task WatchTaskAsync(Task task)
        {
            try
            {
                await task;
            }
            catch
            {
            }
            var propertyChanged = PropertyChanged;
            if (propertyChanged == null)
                return;
            propertyChanged(this, new PropertyChangedEventArgs("Status"));
            propertyChanged(this, new PropertyChangedEventArgs("IsCompleted"));
            propertyChanged(this, new PropertyChangedEventArgs("IsNotCompleted"));
            if (task.IsCanceled)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsCanceled"));
            }
            else if (task.IsFaulted)
            {
                propertyChanged(this, new PropertyChangedEventArgs("IsFaulted"));
                propertyChanged(this, new PropertyChangedEventArgs("Exception"));
                propertyChanged(this,
                  new PropertyChangedEventArgs("InnerException"));
                propertyChanged(this, new PropertyChangedEventArgs("ErrorMessage"));
            }
            else
            {
                propertyChanged(this,
                  new PropertyChangedEventArgs("IsSuccessfullyCompleted"));
                propertyChanged(this, new PropertyChangedEventArgs("Result"));
            }
        }
        public Task<TResult> Task { get; private set; }
        public TResult Result
        {
            get
            {
                return (Task.Status == TaskStatus.RanToCompletion) ?
                    Task.Result : default(TResult);
            }
        }
        public TaskStatus Status { get { return Task.Status; } }
        public bool IsCompleted { get { return Task.IsCompleted; } }
        public bool IsNotCompleted { get { return !Task.IsCompleted; } }
        public bool IsSuccessfullyCompleted
        {
            get
            {
                return Task.Status ==
                    TaskStatus.RanToCompletion;
            }
        }
        public bool IsCanceled { get { return Task.IsCanceled; } }
        public bool IsFaulted { get { return Task.IsFaulted; } }
        public AggregateException Exception { get { return Task.Exception; } }
        public Exception InnerException
        {
            get
            {
                return (Exception == null) ?
                    null : Exception.InnerException;
            }
        }
        public string ErrorMessage
        {
            get
            {
                return (InnerException == null) ?
                    null : InnerException.Message;
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Step 4: Create the viewmodel

MainWindowViewModel.cs

using System;
using System.Threading.Tasks;

namespace AsyncWpf
{
    public class MainWindowViewModel
    {
        public MainWindowViewModel()
        {
            UrlByteCount = new NotifyTaskCompletion<int>(
              MyStaticService.CountBytesInUrlAsync("http://www.example.com"));
        }
        
        public NotifyTaskCompletion<int> UrlByteCount { get; private set; }
    }
}

Step 5: Update the data binding

Use data bindings to show the busy indicators, results or error details.

Be sure to add your data context:

MainWindow.xaml

<Window x:Class="AsyncWpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:AsyncWpf"
        Title="MainWindow" Height="350" Width="525">

    <Window.Resources>
        <BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
    </Window.Resources>

    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>

    <Grid>
        <Label Content="Loading..." Visibility="{Binding UrlByteCount.IsNotCompleted, Converter={StaticResource BooleanToVisibilityConverter}}"/>
        <Label Content="{Binding UrlByteCount.Result}" Visibility="{Binding UrlByteCount.IsSuccessfullyCompleted, Converter={StaticResource BooleanToVisibilityConverter}}"/>
        <Label Content="{Binding UrlByteCount.ErrorMessage}" Background="Red" Visibility="{Binding UrlByteCount.IsFaulted, Converter={StaticResource BooleanToVisibilityConverter}}"/>
    </Grid>
</Window>

When running, observe how the label is set to “Loading…” while the asyc task is still completing:

asyncwpf2

And when the asynchronous task of counting the number of bytes in the website is completed, the label is set to the result value:

asyncwpf3


Leave a Reply