Introduction
If you have programmed Silverlight, you have noticed that the web service access inherently in Silverlight is asynchronous. On the other hand, in WPF the default model is synchronous. In synchronous mode, while the round-trip to the server is made to fetch data, your UI will remain locked up and this might give your users an impression that the application has hung up. Some users might jump around the window and click multiple times in an effort to make the application respond. Some may "end-task" the process by hitting Ctrl+Alt+Delete.
Fortunately, .NET 4.5 introduced two keywords async
and await
, which magically convert a method into being asynchronous. Within the body of that method, you can call a long-running process like a web service by prefixing the call with await
keyword. Every line of code that comes after this will be made to wait until the execution of the operation is finished. During the time, the control is transferred back to the original caller of the async
method. Once the long-running process completes execution, the rest of the lines that come after the await
method take over and complete their execution.
This article is for those who have programmed this kind of situation in the past on Winforms/WPF using background worker component or using another thread or even the combination of Async methods generated by the web service proxy plus operation-completed events. The feature introduced in .NET 4.5 gives the developers a much better and cleaner alternative to handling event procedures, etc., and manage the operations at a single place - for example the click event of a button.
Background
While designing a WPF + WCF application in our organization, it was our policy that the application should be user-friendly and should remain responsive as far as possible even while it is busy processing something under the hood. We decided to make use of the async
/await
capabilities of .NET framework 4.5 so that we could avoid using background worker for implementing asynchrony, which used to be what we did during the ASMX web service days. To get the similar functionality to busy indicator present in Silverlight toolkit, we made use of the Busy Indicator available as part of Extended WPF Toolkit from Codeplex.
Using the code
The example shown here has two projects:
- A WCF service application project, which contains one method (GetBooks) to retrieve the information of Books and their authors.
- A WPF client application that consumes the service. The WPF application should be of .NET 4.5 or else you will not be able to make use of
async
/await
keywords.
Requirements:
- Visual Studio 2012
- Extended WPF Kit downloaded and added to the Toolbox.
Though a complete working example has been attached as a zip file along with the article, I would brief on the main steps involved:
Step I: Create WCF Service Application:
Open Visual Studio 2012 and create a WCF service application and name it as MyService. In the solution explorer, right-click the IService1.cs file and rename it to IBookService.cs. Visual Studio will ask you whether you want to rename all references. Choose Yes. Now, right click the Service1.svc file and rename it BookService.svc. Open the BookService.svc file, right-click the word Service1, choose Refactor => Rename it to BookService. This will make sure that Visual Studio correctly renames all references of service1 to BookService.
Delete/modify all the code that is inside inside both IBookService.cs, and make it look like following:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace MyService
{
[ServiceContract]
public interface IBookService
{
[OperationContract]
ObservableCollection<Book> GetBooks();
}
[DataContract]
public class Book
{
[DataMember]
public int BookId { get; set; }
[DataMember]
public string BookName { get; set; }
[DataMember]
public string Author { get; set; }
}
}
Now, delete/modify all the code that is inside inside both BookService.cs, and make it look like following:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
namespace MyService
{
public class BookService : IBookService
{
public ObservableCollection<Book> GetBooks()
{
System.Threading.Thread.Sleep(5000);
return new ObservableCollection<Book>(
new List<Book>
{
new Book{ BookId = 1, BookName="Learning C#", Author = "Nejimon CR"},
new Book{ BookId = 2, BookName="Introuction to ADO.NET", Author = "Nejimon CR"},
new Book{ BookId = 3, BookName="Lambda Made Easy", Author = "Nejimon CR"},
new Book{ BookId = 4, BookName="Robinson Crusoe", Author = "Daniel Defoe"},
new Book{ BookId = 5, BookName="The White Tiger", Author = "Aravind Adiga"},
new Book{ BookId = 6, BookName="The God of Small Things", Author = "Arunthati Roy"},
new Book{ BookId = 7, BookName="Midnight's Children", Author = "Salman Rushdie"},
new Book{ BookId = 8, BookName="Hamlet", Author = "William Shakespeare"},
new Book{ BookId = 9, BookName="Paradise Lost", Author = "John Milton"},
new Book{ BookId = 10, BookName="Waiting for Godot", Author = "Samuel Beckett"},
}
);
}
}
}
What we essentially did was define an interface, define a class that will be used as a data contract to return the book data back to the client, and implement the interface in the in the service class. The GetBooks method returns the names of a few books along with their authors. Of course, in a real application, you may retrieve the data from a database.
I have also put the following line at the top:
System.Threading.Thread.Sleep(5000);
This will simulate the lag experienced while accessing the web service over the internet so that I can demonstrate the use of busy indicator and abort feature.
Once your web service is ready, build it and see if it builds without errors.
Step II: Create WPF Client Application:
As the next step, add one WPF project to the solution (File => New Project) and name it AsyncAwaitDemo. Remember to keep the target framework version as .NET 4.5. In the solution explorer, right click the WPF project and set it as Startup Project.
Right click the WPF project again and open the "Add Service Refeference" dialog. Click "Advanced" at the bottom of the dialog and make sure "Allow generation of asynchronous operations" is checked. Discover the service in your solution, keep the Namespace as "BookService," and click ok. Now you have added a reference to your BookService.
Open the Mainwindow.xaml and add a Button, a Datagrid, and a Busy Indicator. Please refer to the markup for the correct placement of controls:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit" x:Class="AsyncAwaitDemo.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Grid>
<xctk:BusyIndicator Name="busyIndicator">
<xctk:BusyIndicator.BusyContent>
<StackPanel>
<TextBlock HorizontalAlignment="Center">Please wait...</TextBlock>
<Button Content="Abort" Name="btnAbort" HorizontalAlignment="Center"/>
</StackPanel>
</xctk:BusyIndicator.BusyContent>
<xctk:BusyIndicator.Content>
<StackPanel>
<Button Content="Get Data" Name="btnGetData" HorizontalAlignment="Center"/>
<DataGrid Name="grdData" AutoGenerateColumns="True"/>
</StackPanel>
</xctk:BusyIndicator.Content>
</xctk:BusyIndicator>
</Grid>
</Window>
Open the MainWindow.xaml's code window (press F7) and alter the code to make it look as below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using AsyncAwaitDemo.BookService;
namespace AsyncAwaitDemo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
btnGetData.Click += btnGetData_Click;
btnAbort.Click += btnAbort_Click;
grdData.AutoGeneratingColumn += grdData_AutoGeneratingColumn;
}
BookServiceClient client;
void grdData_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ExtensionData")
{
e.Cancel = true;
}
}
void btnAbort_Click(object sender, RoutedEventArgs e)
{
if (client != null)
{
if (client.State == System.ServiceModel.CommunicationState.Opened)
{
client.Abort();
}
}
}
async void btnGetData_Click(object sender, RoutedEventArgs e)
{
try
{
busyIndicator.IsBusy = true;
client = new BookServiceClient();
var result = await client.GetBooksAsync();
client.Close();
grdData.ItemsSource = result;
busyIndicator.IsBusy = false;
}
catch (Exception ex)
{
busyIndicator.IsBusy = false;
if (!ex.Message.Contains("The request was aborted: The request was canceled."))
{
MessageBox.Show("Unexpected error: " + ex.Message,
"Async Await Demo", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
this.UpdateLayout();
}
}
}
We are done with the coding. Build the solution. If everything has gone as expected, the application starts up and shows the UI. Click Get Data button and you will see the following:
Points of Interest
What is interesting here is the following line:
async void btnGetData_Click(object sender, RoutedEventArgs e)
The async
keyword converts the event handler into an asychronously executing method, thus preventing the application UI from being unresponsive when a long-running process is on.
var result = await client.GetBooksAsync();
When the above line is met, after entering the execution of the GetBooksAsync, without waiting for completion, the control is handed back to the caller, thereby keeping the UI responsive. On the other hand, any lines that come after GetBooksAsync are made to wait (hence the keyword await
). So the following lines are held from being executed until the web service call returns:
client.Close();
grdData.ItemsSource = result;
busyIndicator.IsBusy = false;
Read more about async/await here.
It might also be interesting that during serialization, WCF may add one more column to your data contract objects. To prevent it from being displayed on the data grid (of course if you have turned on the AutoGenerateColumns), you may need to add the following code:
void grdData_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.Column.Header.ToString() == "ExtensionData")
{
e.Cancel = true;
}
}
History
None.