Practical .NET

How To Manage Asynchronous Tasks Using the Task Object

You want the responsiveness that asynchronous programming in the Microsoft .NET Framework 4 provides, but also need your asynchronous methods to work with other code in your application. Here's how the Task object answers all of your problems.

In an earlier column looking at some asynchronous methods new to Entity Framework, I got beaten up by my readers for using those methods in a fluent way; my code, I was told, was far more complicated than it needed to be. So, in a later column, I looked at writing simpler asynchronous code by using the Async and Await keywords. But, as a reader for that column pointed out, the code from that second column is probably too simple for real-world applications.

To see what the problem is, here's that "oversimple" Async/Await code from the second column:

Private Sub Button1_Click(sender ...
  LoadGrid()
  ...more code...
End Sub

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

My Button1_Click event handler is calling my LoadGrid method, which I've marked as Async (Microsoft refers to methods declared with the Async keyword as "async methods"). Declaring LoadGrid as Async allows me to use the Await keyword inside the method when calling asynchronous methods such as ToListAsync (which I introduced in the first column).

When the Microsoft .NET Framework hits the Await command in my LoadGrid method, it saves the code's current context, starts to execute the code following the Await keyword and returns control to the code that called the method (my Button1_Click event, in this case). As a result, the ToListAsync method -- and whatever code that follows it in LoadGrid -- will execute in parallel with whatever code in my Button1_Click event that follows the call to my LoadGrid method. In fact, if the Button1_Click event handler ends before the LoadGrid method finishes, whatever code that called the event handler will also execute in parallel with the LoadGrid method's code. The benefit from using Async and Await like this is that the form will return to the user sooner than if the user had to wait until the LoadGrid method finished (though users will still have to wait about as long to see the data in the grid).

Problems
The code is certainly simple, but because the way I've written it, I've given up a lot of control. The code works, but you should only use code like this if you don't care about how the results of your async processing are delivered. With this code, you really have no clean way of determining when the grid will be filled and, as a result, have no way of synchronizing filling the grid with other activities in the application. For example, if I'm allowing users to filter or sort the data in the grid, I can't really be sure when I can turn that functionality on because I won't know when the grid has been populated.

I also have no way of handling errors thrown by the LoadGrid method. In fact, error-handling code with async methods can be deceptive. For example, this code looks like it might actually capture errors from LoadGrid in its Try…Catch block:

Private Sub Button1_Click(sender ...
  Try
    LoadGrid()
  Catch ex As Exception
    ...error handling code...
  End Try
  ...more code...

But, remember, once the .NET Framework hits the Await statement inside the LoadGrid method, control returns to my Button1_Click event handler and carries on from there. By the time the LoadGrid method throws an exception, I've probably exited the Try…Catch block (and, possibly, even exited the Button1_Click event handler). The odds are that, by the time the LoadGrid method throws an exception, I won't be inside the Try…Catch block anymore. If LoadGrid throws an exception after exiting the Try…Catch block, the various .NET development platforms will handle the error differently, but whatever way the exception is handled, I bet that I won't be pleased with the result.

Managing Asynchronous Results
I can regain that control by having my method return a Task object. If I do make that change, I should follow the .NET convention and add the word Async to the end of my function name (the "Async" prefix signals that a method contains asynchronous code and returns a Task object). Because I need a new method name, I've written a whole new method that returns a result as my example. This will let me demonstrate the control a Task object gives you (and, while it's not essential to this example, I'll also show off one of the asynchronous methods built into ADO.NET 6.1).

In the following method, the Async keyword on my function declaration lets me use the Await keyword inside the function. I want my function to return the row count for a table as an Integer value, which creates a potential conflict: If an Async method returns anything then it must return a Task object. Fortunately, I can still return the row count by declaring the method as Task(of Integer):

Async Function GetCountAsync() As Task(Of Integer)

The initial part of my new method contains some standard, synchronous code that runs relatively quickly:

Dim con As New SqlConnection
Dim res As Integer

  con.ConnectionString = "...connection string..."
  Dim cmd As SqlCommand = con.CreateCommand()
  cmd.CommandText = "Select Count(*) from SalesLT.Product"

However, once I get to the part of my method that might take some time (and, as a result, hold up other processing), I call an asynchronous method: In this case, the ADO.NET 6.1 OpenAsync method. I precede the method with the Await keyword, which causes the .NET Framework to return control to whatever code called my GetCountAsync method and then execute the rest of the code in GetCountAsync in parallel to that other code. The Await keyword also ensures that inside my async method, execution won't proceed to the following line in my GetCountAsync method until the OpenAsync method completes:

Await con.OpenAsync()
res = cmd.ExecuteScalar()
con.Close()

I suppose I could've used the new ExecuteScalarAsync method in this code, but it wouldn't have gained me much -- the first Await statement has already caused the rest of my GetCountAsync method to execute in parallel to the code that called my GetCountAsync method. If you wanted to think of the Await keyword as causing your Async method to execute synchronously internally while executing asynchronously relative to the external code that called it, then you'd have a reasonably useful mental model for working with Await/Async.

Finally, I return the row count:

 
  Return res
End Function

There's some .NET magic happening here: My method returns an integer variable (res), even though the method is declared as returning Task(Of Integer). The compiler will take care of wrapping my integer result in a Task object. That magic also gives me some flexibility in how I want to call my async method.

Two Ways To Call Async Methods
Because my GetCountAsync method returns a value of Task(Of Integer), it's tempting to catch the result of the method in a variable declared as, well, Task(of Integer). This example does just that:

Dim recordCountTask As Task(Of Integer)

Private Sub Button1_Click(sender …
  recordCountTask = GetCountAsync()

However, within GetCountAsync, the code following the Await keyword is still executing asynchronously relative to the code in the Button1_Click event. This means that, when control returns to my Button1_Click event, my GetCountAsync probably hasn't actually retrieved the row count yet (and, by the way, trying to retrieve the value before the method completes probably won't end well). And don't try to use the Await keyword here -- the compiler won't let you use Await if you're catching the Task returned by an async method.

A simple change enables me to access the row count in the same method that calls GetCountAsync: I call my async method using the Await keyword I used on OpenAsync, letting me catch the result of my async method in an integer variable. Here's that version of my code (and, because I'm using the Await keyword in my event handler, I must declare the event handler with the Async keyword):

Dim recordCount As Integer

Private Async Sub Button1_Click(sender ...
  recordCount = Await GetCountAsync()
  TextBox1.Text = recordCount
  ...more code...

Because of the Await keyword here, control returns to whatever called the Button1_Click event. But, within the Button1_Click method, processing doesn't continue to the line that sets the Text property until GetCountAsync completes. If you wanted to think that, in addition to everything else that Await does, it also strips off the Task object that wraps your function's result, your mental model wouldn't be far wrong.

Given the flexibility of Visual Basic in converting datatypes, I'll reduce this to a single line of code to save some space in this article:

Private Async Sub Button1_Click(sender ...
  TextBox1.Text = Await GetCountAsync()
  ...more code...

This change also means that a Try...Catch block around the call to GetCountAsync will actually be useful:

Try
  TextBox1.Text =  Await GetCountAsync()
Catch ex As Exception
  MessageBox.Show(ex.Message)
End Try

While Await returns control to the code that calls my Button1_Click method, within the Button1_Click method itself, Await effectively prevents the code from advancing to the statements following the awaited statement (in this case, to the End Try) until the awaited method (GetCountAsync) finishes. If GetCountAsync throws an exception, the Try...Catch block will still be in effect and will catch the exception.

One final note: If you do try this code and are using the .NET Framework 4 (not version 4.5), then you'll need to add Asynchronous Processing=true to your connection string.

Using the Task Object
All of that is great, provided you want to use the value returned by your async method in the method that calls your async method. If I want to use the row count in some other method (that is, if I need to synchronize my async method with some other method in my application) then I need some way to determine, in that other method, if my GetCountAsync method has completed without errors.

Here's where the code that catches the Task object is useful: The Task object gives me the ability to synchronize my async method with other code. To begin with, I can use the Await keyword with the Task returned by my method. This code, effectively, duplicates my previous example:

Dim recordCountTask As Task(Of Integer)

Private Async Sub Button1_Click(sender ...
Try
  recordCountTask = GetCountAsync()
  TextBox1.Text = Await recordCountTask
Catch ex As Exception
  MessageBox.Show(ex.Message)
End Try

Unless you're paid by the keystroke, however, this code doesn't give you anything more than the shorter version that just awaited GetCountAsync and caught the integer result. But let's assume that my application doesn't need the row count until the LostFocus method of some other control. In that case, in my Button1_Click event, I just catch the Task object returned by my async method (and, because I'm eliminating the Await keyword, I can eliminate the Async keyword on the event handler's declaration):

Dim recordCountTask As Task(Of Integer)

Private Sub Button1_Click(sender...
  recordCountTask = GetCountAsync()
End Sub

In my LostFocus event, I still can't be sure the result generated in GetCountAsync is available yet. So, in the LostFocus event, I Await the Task object. As always, with the Await keyword, control is returned to whatever code calls the LostFocus method while the rest of the LostFocus method executes in parallel to that other code. But now, when my asynchronous method completes, I can, through the Task object, get the GetCountAsync method's result in my LostFocus code. My error handling code tags along with the Await statement to the LostFocus event, so the code looks like this:

Private Async Sub TextBox1_LostFocus(sender...
  Try
    TextBox1.Text = Await recordCountTask
  Catch ex As Exception
    MessageBox.Show(ex.Message)
  End Try
End Sub

If you wanted to think of the Await statement as returning everything from the async method, including any exceptions the method raised, you'd have an even better mental model for working with Await. And I've ignored one wrinkle here: If the asynchronous method tied to the Task object has completed, the Await just returns the method and continues on processing in the method -- control isn't returned to the code that called the async method.

Whatever way you decide to access the .NET Framework 4 support for asynchronous processing (I still like the fluent methods I used in the first article), all of these tools make it much easier for you to deliver more responsive applications to your users ... and they'll like that.

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

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube