dotnet Cologne 2013 - async/await
03 May 2013
-
.NET ,
C# ,
Visual Studio ,
WPF
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 ());
}
}
}