dotnet Cologne 2013: async/await

Friday, May 3, 2013 by Rainer Stropek

This year at dotnet Cologne I have proposed a 60 minutes live-coding talk about async/await. It was really accepted and I even got the large ballroom. Wow, live coding in front of a huge crowd of developers. This will be awesome. In this blog I post the sample that I am going to develop on stage (at least approximately; let's see if I have some ad hoc ideas on stage).

The Starting Point

We start from a very simple class simulating a sensor:

using System;
using System.Net;
using System.Threading;

namespace AsyncAwaitDemo
{
    public class SyncHeatSensor
    {
        /// <summary>
        /// Flag indicating whether the sensor is connected.
        /// </summary>
        private bool isConnected = false;

        public void Connect(IPAddress address)
        {
            if (address == null)
            {
                throw new ArgumentNullException("address");
            }

            if (this.isConnected)
            {
                throw new InvalidOperationException("Already connected");
            }

            // Simulate connect
            Thread.Sleep(3000);

            this.isConnected = true;
        }

        public void UploadFirmware(byte[] firmware)
        {
            if (firmware == null)
            {
                throw new ArgumentNullException("firmeware");
            }

            if (!this.isConnected)
            {
                throw new InvalidOperationException("Not connected");
            }

            for (var i = 0; i < 10; i++)
            {
                // Simulate uploading of a chunk of data
                Thread.Sleep(200);
            }
        }

        public bool TryDisconnect()
        {
            if (!this.isConnected)
            {
                return false;
            }

            // Simulate disconnect
            Thread.Sleep(500);

            this.isConnected = false;
            return true;
        }
    }
}

The user interface is very simple - just a button:

<Window x:Class="AsyncAwaitDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="3" />
        </Style>
    </Window.Resources>
    <StackPanel>
        <Button Command="{Binding Path=ConnectAndUpdateSync}">Connect to sensor and upload firmware</Button>
    </StackPanel>
</Window>

Of course the UI logic is implemented in a ViewModel class:

using System.Windows;

namespace AsyncAwaitDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel();
        }
    }
}
using System;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows;
using System.Windows.Input;

namespace AsyncAwaitDemo
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private SyncHeatSensor syncSensor = new SyncHeatSensor();

        public MainWindowViewModel()
        {
            this.InternalConnectAndUpdateSync = new DelegateCommand(
                this.ConnectSync,
                () => true);
        }

        private void ConnectSync()
        {
            var address = Dns.GetHostAddresses("localhost");
            this.syncSensor.Connect(address.FirstOrDefault());
            this.syncSensor.UploadFirmware(new byte[] { 0, 1, 2 });
            this.syncSensor.TryDisconnect();
            MessageBox.Show("Successfully updated");
        }

        private DelegateCommand InternalConnectAndUpdateSync;
        public ICommand ConnectAndUpdateSync
        {
            get
            {
                return this.InternalConnectAndUpdateSync;
            }
        }

        public void RaisePropertyChanged([CallerMemberName]string propertyName = null)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

The unit test for the synchronous version is also very basic. However, it will be enough to demonstrate the basic idea of async unit tests later.

using AsyncAwaitDemo;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace AsyncUnitTest
{
    [TestClass]
    public class TestAsyncSensor
    {
        [TestMethod]
        public void TestConnectDisconnect()
        {
            var sensor = new SyncHeatSensor();
            sensor.Connect(Dns.GetHostAddresses("localhost").First());
            Assert.IsTrue(sensor.TryDisconnect());
        }
    }
}

Moving to an Async API

Here is the async version of the sensor class using best-practices for async APIs:

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

namespace AsyncAwaitDemo
{
    public class AsyncHeatSensor
    {
        private bool isConnected = false;
        private object workInProgressLockObject = new object();

        public Task ConnectAsync(IPAddress address)
        {
            // Note that parameters are checked before the task is scheduled.
            if (address == null)
            {
                throw new ArgumentNullException("address");
            }

            return Task.Run(() =>
                {
                    // Note that method calls are serialized using this lock statement.
                    // If you want to specify a lock timeout, use Monitor.TryEnter(...)
                    // instead of lock(...).
                    lock (this.workInProgressLockObject)
                    {
                        if (this.isConnected)
                        {
                            throw new InvalidOperationException("Already connected");
                        }

                        // Simulate connect
                        Thread.Sleep(3000);

                        this.isConnected = true;
                    }
                });
        }

        public Task UploadFirmwareAsync(byte[] firmware, CancellationToken ct, IProgress<int> progress)
        {
            if (firmware == null)
            {
                throw new ArgumentNullException("firmeware");
            }

            return Task.Run(() =>
                {
                    lock (this.workInProgressLockObject)
                    {
                        if (!this.isConnected)
                        {
                            throw new InvalidOperationException("Not connected");
                        }

                        // Simulate upload in chunks.
                        for (var i = 1; i <= 10; i++)
                        {
                            // Note that we throw an exception if cancellation has been requested.
                            ct.ThrowIfCancellationRequested();

                            // Simulate uploading of a chunk of data
                            Thread.Sleep(200);

                            // Report progress
                            progress.Report(i * 10);
                        }
                    }
                }, ct);
        }

        public Task<bool> TryDisconnectAsync()
        {
            return Task.Run(() =>
            {
                lock (this.workInProgressLockObject)
                {
                    if (!this.isConnected)
                    {
                        return false;
                    }

                    // Simulate disconnect
                    Thread.Sleep(500);

                    this.isConnected = false;
                    return true;
                }
            });
        }
    }
}

In the ViewModel we can use async/await to make the code more readable:

using System;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows;
using System.Windows.Input;

namespace AsyncAwaitDemo
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private SyncHeatSensor syncSensor = new SyncHeatSensor();
        private AsyncHeatSensor asyncSensor = new AsyncHeatSensor();

        private Action<string> stateNavigator;
        private CancellationTokenSource cts;

        public MainWindowViewModel(Action<string> stateNavigator)
        {
            this.stateNavigator = stateNavigator;

            this.InternalConnectAndUpdateSync = new DelegateCommand(
                this.ConnectSync,
                () => !this.IsUpdating);

            this.InternalConnectAndUpdateAsync = new DelegateCommand(
                this.ConnectAsync,
                () => !this.IsUpdating);
            this.InternalCancelConnectAndUpdateAsync = new DelegateCommand(
                () => { if (this.cts != null) this.cts.Cancel(); },
                () => this.IsUpdating);
        }

        private void ConnectSync()
        {
            var address = Dns.GetHostAddresses("localhost");
            this.syncSensor.Connect(address.FirstOrDefault());
            this.syncSensor.UploadFirmware(new byte[] { 0, 1, 2 });
            this.syncSensor.TryDisconnect();
            MessageBox.Show("Successfully updated");
        }

        private async void ConnectAsync()
        {
            this.IsUpdating = true;
            this.cts = new CancellationTokenSource();
            this.stateNavigator("Updating");
            var ip = await Dns.GetHostAddressesAsync("localhost");
            await this.asyncSensor.ConnectAsync(ip.FirstOrDefault());
            var success = false;
            try
            {
                await this.asyncSensor.UploadFirmwareAsync(
                    new byte[] { 0, 1, 2 }, 
                    this.cts.Token, 
                    new Progress<int>(p => this.Progress = p));
                success = true;
            }
            catch (OperationCanceledException)
            {
            }

            await this.asyncSensor.TryDisconnectAsync();
            this.stateNavigator(success ? "Updated" : "Cancelled");
            this.IsUpdating = false;
            if (success)
            {
                MessageBox.Show("Successfully updated");
            }
        }

        private DelegateCommand InternalConnectAndUpdateSync;
        public ICommand ConnectAndUpdateSync
        {
            get
            {
                return this.InternalConnectAndUpdateSync;
            }
        }

        private DelegateCommand InternalConnectAndUpdateAsync;
        public ICommand ConnectAndUpdateAsync
        {
            get
            {
                return this.InternalConnectAndUpdateAsync;
            }
        }

        private DelegateCommand InternalCancelConnectAndUpdateAsync;
        public ICommand CancelConnectAndUpdateAsync
        {
            get
            {
                return this.InternalCancelConnectAndUpdateAsync;
            }
        }

        private bool IsUpdatingValue;
        public bool IsUpdating
        {
            get
            {
                return this.IsUpdatingValue;
            }

            set
            {
                if (this.IsUpdatingValue != value)
                {
                    this.IsUpdatingValue = value;
                    this.RaisePropertyChanged();
                    this.InternalConnectAndUpdateAsync.RaiseCanExecuteChanged();
                    this.InternalCancelConnectAndUpdateAsync.RaiseCanExecuteChanged();
                }
            }
        }

        private int ProgressValue;
        public int Progress
        {
            get
            {
                return this.ProgressValue;
            }

            set
            {
                if (this.ProgressValue != value)
                {
                    this.ProgressValue = value;
                    this.RaisePropertyChanged();
                }
            }
        }

        public void RaisePropertyChanged([CallerMemberName]string propertyName = null)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

In the UI I use visual states:

<Window x:Class="AsyncAwaitDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <VisualStateManager.VisualStateGroups>
        <VisualStateGroup Name="ConnectingStates">
            <VisualState Name="Initial">
            </VisualState>
            <VisualState Name="Updating">
                <Storyboard>
                    <ColorAnimationUsingKeyFrames Storyboard.TargetName="Indicator"
                                                  Storyboard.TargetProperty="Color"
                                                  RepeatBehavior="Forever" >
                        <DiscreteColorKeyFrame Value="Green" KeyTime="00:00:00.5" />
                        <DiscreteColorKeyFrame Value="Red" KeyTime="00:00:01.0" />
                    </ColorAnimationUsingKeyFrames>
                    <ObjectAnimationUsingKeyFrames Storyboard.TargetName="CancelButton"
                                                   Storyboard.TargetProperty="Visibility">
                        <DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" KeyTime="00:00:00" />
                    </ObjectAnimationUsingKeyFrames>
                </Storyboard>
            </VisualState>
            <VisualState Name="Cancelled">
                <Storyboard>
                    <ColorAnimation Storyboard.TargetName="Indicator"
                                    Storyboard.TargetProperty="Color"
                                    To="Red"
                                    Duration="0" />
                </Storyboard>
            </VisualState>
            <VisualState Name="Updated">
                <Storyboard>
                    <ColorAnimation Storyboard.TargetName="Indicator"
                                    Storyboard.TargetProperty="Color"
                                    To="Green"
                                    Duration="0" />
                </Storyboard>
            </VisualState>
        </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="Margin" Value="3" />
        </Style>
    </Window.Resources>
    <StackPanel>
        <Button Command="{Binding Path=ConnectAndUpdateSync}">Connect to sensor and upload firmware</Button>

        <Grid Margin="0, 20, 0, 0">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="*" />
            </Grid.ColumnDefinitions>
            <Ellipse Name="ConnectionIndicator" Width="50" Height="50">
                <Ellipse.Fill>
                    <SolidColorBrush Color="Gray" x:Name="Indicator" />
                </Ellipse.Fill>
            </Ellipse>
            <ProgressBar Minimum="0" Maximum="100" Value="{Binding Path=Progress}" 
                         MinHeight="20" MinWidth="200" Grid.Row="1" Margin="3" />
            <Button Command="{Binding Path=ConnectAndUpdateAsync}" Grid.Column="1">Connect and Update</Button>
            <Button Name="CancelButton" Command="{Binding Path=CancelConnectAndUpdateAsync}" Grid.Column="1" Grid.Row="1"
                    Visibility="Hidden">Cancel</Button>
        </Grid>
    </StackPanel>
</Window>
using System.Windows;

namespace AsyncAwaitDemo
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new MainWindowViewModel(
                targetState => VisualStateManager.GoToElementState(App.Current.MainWindow, targetState, false));
        }
    }
}

Visual Studio 2012 allows you to also use async/await in unit tests. Note how the unit test functions returns a Task.

using AsyncAwaitDemo;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace AsyncUnitTest
{
    [TestClass]
    public class TestAsyncSensor
    {
        [TestMethod]
        public void TestConnectDisconnect()
        {
            var sensor = new SyncHeatSensor();
            sensor.Connect(Dns.GetHostAddresses("localhost").First());
            Assert.IsTrue(sensor.TryDisconnect());
        }

        [TestMethod]
        public async Task TestConnectDisconnectAsync()
        {
            var sensor = new AsyncHeatSensor();
            await sensor.ConnectAsync((await Dns.GetHostAddressesAsync("localhost")).First());
            Assert.IsTrue(await sensor.TryDisconnectAsync());
        }
    }
}
comments powered by Disqus