Practical .NET

How To Simplify Asynchronous Programming with Await and Async

Prune your Entity Framework with the help of asynchronous methods.

A couple of months ago, I did a column on using the new asynchronous methods that come with Entity Framwork (EF) 6.1. I got taken to task by my readers for creating overly complex solutions, who claimed I could've achieved the same goals with much less code by incorporating the Async and Await keywords. So here's the follow-up column to that column: This one is on writing asynchronous code in your presentation layer using the Async and Await keywords.

Creating a More Responsive UI
The goal in the original version of the code was to create a form that would appear on the screen faster. That form (technically, a Windows Presentation Foundation [WPF] window) included a grid full of data, loaded before the form displayed. Because of that grid, using synchronous code would force the user to wait until the grid's data was retrieved and the grid populated before being able to interact with the form, or even to see the form at all. The synchronous code for that window's Load event would look like this:

Private Sub MainWindow_Loaded(sender ...
  Me.CustomersDataGrid.ItemsSource = (From cust In ctx.Customers
                                      Select cust).ToList
End Sub

The reason for the delay is the ToList method that triggers the data retrieval executes synchronously: WPF can't go on to the next line of code (and display the form) until the ToList completes. As a result, the user first sees the outline of the window appear, waits through a significant pause while the data is fetched, and then, finally, sees the rest of the window appear, along with the populated grid.

Leveraging the asynchronous extensions that come with EF 6.1 gave me this code in the previous article:

 Dim GridTask As Task
 Dim ctx As New AdventureWorksLTEntities

 GridTask = (From cust In ctx.Customers
             Select cust).ToListAsync().
                        ContinueWith(
                          Sub(tk)
                            Dispatcher.BeginInvoke(
                              Sub()
                                Me.CustomersDataGrid.ItemsSource = tk.Result
                                Me.CustomersDataGrid.IsEnabled = True
                              End Sub)
                          End Sub)

This code uses ToListAsync to trigger the data retrieval, and when that task finishes, goes on to load the grid with the results of that task by using ContinueWith. The ToListAsync method causes the data retrieval to happen asynchronously so, after executing this line of code to start the data retrieval, the Microsoft .NET Framework goes on to the next line of code rather than waiting for the data to be retrieved. From the user's point of view, the window appears on the screen immediately (though with an empty grid) and then, shortly thereafter, the grid is filled with data.

If there's something else for the user to do with the form while waiting for the grid to fill, the user can start using the form sooner. But even if there's nothing else for the user to do, there's a psychological impact from using the asynchronous code: Users will report the asynchronous version runs faster than the synchronous version. You'll be a hero to your users, even though the time between starting the application and seeing the filled grid is about the same in both versions.

Simplifying with Await and Async
Here's where readers objected: That whole fluent jumble can be reduced to two lines of code, provided you put the code in the right context (and you're creating a WPF or Windows Form application; I'll look at how to do this in an ASP.NET Web Forms application near the end of this article). First, here are the two lines of code you need in place of my previous 10 lines of fluent code:

Me.CustomersDataGrid.ItemsSource = (From cust In ctx.Customers
                                    Select cust).ToListAsync()

The problem with this code is that, by the time the ToListAsync returns the objects, the grid can no longer be updated because WPF has moved on to do other things. From the user's point of view, the full window appears immediately, but the grid is never filled. The first step in solving this problem is to have the update to the grid's ItemSource wait until the ToListAsync method finishes. That's easy to do: just insert the Await keyword in front of the LINQ statement that's retrieving the Customer objects:

Me.CustomersDataGrid.ItemsSource = Await(From cust In ctx.Customers
                                         Select cust).ToListAsync()

The Await keyword causes the .NET Framework to save the current context, execute the code following the Await on another thread, and (when that other thread completes) return to the application, restoring the context and returning the result generated on that other thread.

The major problem with this code is that, inserted into the Load event, it won't compile: You can't use the Await keyword in an ordinary method. The solution is to wrap the code in a method declared with the Async keyword and call that method from the Load event (Microsoft refers to these as async methods). The async method to hold my now asynchronous code looks like this:

Public Async Sub LoadGrid()
Dim ctx As New AdventureWorksLTEntities
  Me.CustomersDataGrid.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust).ToListAsync()
End Sub

When the .NET Framework hits the Await keyword, control returns to the method that called the async method while the data retrieval continues on a background thread, allowing WPF to go on and display the form. My fluent code in the Load event can now be replaced with a simple call to my LoadGrid method:

Private Sub MainWindow_Loaded(sender ...
  LoadGrid()
End Sub

The user will see the Window appear, followed by the grid being populated. We're up to five lines of code, still half the size of the fluent code; and, for many readers, with substantial readability improvements.

But depending on the event, you might not even need to create a separate method. You can convert many events to async methods just by adding the async keyword to them (though not the Windows Load event or other events tied closely to the application's lifecycle). This example converts a Button's Click event to asynchronous processing:

Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
Dim ctx As New AdventureWorksLTEntities
  Me.CustomersDataGrid.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust).ToListAsync()
End Sub

Not only are you getting the readability benefits of using Await, you're back to just two lines of code.

Asynchronous Issues
While this asynchronous code is almost identical to the original synchronous code, asynchronous coding does have some special issues. To begin, the EF asynchronous methods aren't thread safe. So, to prevent errors, EF throws an exception if two asynchronous operations execute at the same time on the same DbContext object. If, for example, you call either of the following two methods before the other method finishes, EF will throw an exception:

Dim ctx As New AdventureWorksLTEntities
Public Async Sub LoadDropDown()
  Me.CustomersDropDown.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust.CustomerID).ToListAsync()
End Sub

Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
  Me.CustomersDropDown.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust.CustomerID).ToListAsync()
End Sub

There are several solutions for this issue. One solution is to provide each method with its own context:

Public Async Sub LoadDropDown()
Dim ctx As New AdventureWorksLTEntities
  Me.CustomersDropDown.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust.CustomerID).ToListAsync()
End Sub

Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
Dim ctx As New AdventureWorksLTEntities
  Me.CustomersDropDown.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust.CustomerID).ToListAsync()
End Sub

Assuming the time spent loading the second context doesn't wipe out your gains in responsiveness, this will solve your problem (and creating dedicated contexts might be the answer if your contexts are taking too long to load). Alternatively, if the two methods are always used together, you could combine them into a single method:

Public Async Sub LoadData()
Dim ctx As New AdventureWorksLTEntities
  Me.CustomersDataGrid.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust).ToListAsync()
  Me.CustomersDropDown.ItemsSource = Await (From cust In ctx.Customers
                                            Select cust.CustomerID).ToListAsync()
End Sub

In the async method, the first Await will execute on a background thread and when it returns, the .NET Framework will restore the context and go on to execute the second statement. A third option would be to SyncLock the two statements to ensure they never run simultaneously.

There's a different wrinkle if you're working with ASP.NET Web Forms: Microsoft has made it very clear that using the Async keyword with Web Forms events isn't a good idea. The issue is that, by the time an asynchronous method returns, the Web Form it was called from may no longer be in memory (this isn't the case with WPF and Windows Forms, where the application stays in memory until it decides to leave). But that doesn't mean you can't use asynchronous processing. The ASP.NET Page object includes a RegisterAsyncTask method, and the .NET Framework provides the PageAsyncTask object: Together, they allow you to call an async method in a Web Form.

To use these tools you must first, in your Web Form's source view, set your Page's Async property to true (you can't do this through the Properties List):

<%@ Page Async="true"…

Then, in the code file for the page, create a PageAsyncTask object, passing a reference to the async method you want to execute. Finally, pass that PageAsyncTask object to the page's RegisterAsyncTask method. That code would look like this:

Public Sub Page_Load(sender as Object, e as EventArgs)
  Dim pat As PageAsyncTask
  pat = New PageAsyncTask(Function(d) LoadData())
  RegisterAsyncTask(pat)
End Sub

Now you can write the async method holding your asynchronous code. That method must return a Task object and (as you might expect by now) be declared with the Async keyword. This example loads a GridView and a DropDownList and databinds the results with code almost identical to the desktop version:

Public Async Function LoadData() As Task
Dim ctx As New AdventureWorksLTEntities

  Me.CustomersGridView.DataSource = Await (From cust In ctx.Customers
                                           Select cust).ToListAsync()
  Me.CustomersDropDown.DataSource = Await (From cust In ctx.Customers
                                           Select cust.CustomerID).ToListAsync()
  Me.CustomersGridView.DataBind()
  Me.CustomersDropDown.DataBind()
End Function

Not only is this Web Form code almost identical to the desktop code, much of the code in this article is almost identical to the non-asynchronous code -- one of the beauties of asynchronous programming with Async and Await. Some readers have suggested that the logical extension of these tools is that asynchronous programming should be your default mode for coding, especially for server-side code. But even if that isn't true, it does mean the code you need to solve responsiveness issues (and look like a hero to your users) is just a few keystrokes away.

About the Author

Peter Vogel is a system architect and principal in PH&V Information Services. PH&V provides full-stack consulting from UX design through object modeling to database design. Peter tweets about his VSM columns with the hashtag #vogelarticles. His blog posts on user experience design can be found at http://blog.learningtree.com/tag/ui/.

comments powered by Disqus

Featured

Subscribe on YouTube