Streamlining .NET Asynchronous Operations - Part III

Roger Torres - February 21st, 2009

This is the last part of my series about .NET asynchronous operations. Here you can download:

Since we didn’t quite finish our example in part II, in this post I’m going to show you a simple Windows Forms application that will:

  • Provide a GUI to our last example.
  • Allow the user to cancel the asynchronous operation.
  • Show the managed thread id where internal actions execute, so you can understand how ThreadPool threads are created.
  • Show the code that handles operation events and errors.

The Form

Let’s start by creating a new Windows Application Project named WinFormDemo. The default Form1 will do, we just need to add a toolbar with two buttons (to start and cancel the asynchronous operation), and a list view to show the execution log - see the screenshots below.

Now we can associate the following code-behind to our form:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;
using CoNatural.Threading.Async;
using CoNatural.Extensions.Threading.Async.IO;
using CoNatural.Extensions.Threading.Async.Net;
 
namespace WinFormDemo {
   public partial class Form1 : Form {
      AsyncContext context;
 
      public Form1() {
         InitializeComponent();
 
         // create context
         context = new AsyncContext();
         context.Completed += new EventHandler<AsyncCompletedEventArgs>(Completed);
         context.AsyncActionCompleted += new EventHandler<AsyncActionCompletedEventArgs>(ActionCompleted);
         context.AsyncActionProgressChanged += new EventHandler<AsyncActionProgressChangedEventArgs>(ReportProgress);
         context.AsyncActionTraced += new EventHandler<AsyncActionTracedEventArgs>(TraceAction);
      }
 
      private void WriteLine(string message, params object[] args) {
         logListView.Items.Add(string.Format(message, args));
      }
 
      private IEnumerable<AsyncAction> DownloadAsync(string url) {
         WebRequest req = HttpWebRequest.Create(url);
 
         // get contents of site
         AsyncAction<WebResponse> response = req.GetResponseAsync();
         yield return response;
 
         // copy results to memory stream
         Stream responseStream = response.Result.GetResponseStream();
         MemoryStream copyTo = new MemoryStream();
         yield return new Serial(url, true, responseStream.CopyAsync(copyTo));
 
         // return results
         copyTo.Seek(0, SeekOrigin.Begin);
         string html = new StreamReader(copyTo).ReadToEnd();
         yield return new Return<string>(html);
      }
 
      void DownloadAll() {
         Serial<string> microsoft = new Serial<string>("microsoft", true, DownloadAsync("http://www.microsoft.com"));
         Serial<string> conatural = new Serial<string>("conatural", true, DownloadAsync("http://www.conatural.com"));
 
         Serial<string> google = new Serial<string>("google", true, DownloadAsync("http://www.google.com"));
         Serial<string> yahoo = new Serial<string>("yahoo", true, DownloadAsync("http://www.yahoo.com"));
 
         Parallel favorite = new Parallel("Favorite Sites", true, google, yahoo);
 
         Parallel all = new Parallel("All Sites", true, microsoft, conatural, favorite);
 
         all.Start(context);
      }
 
      private void Completed(object sender, AsyncCompletedEventArgs args) {
         if (args.Cancelled)
            WriteLine("CONTEXT WAS CANCELLED.");
 
         if (args.Error != null)
            foreach (Exception ex in ((AsyncContextException)args.Error).Errors)
               DisplayError(ex);
 
         WriteLine("CONTEXT COMPLETED.");
      }
 
      private void ActionCompleted(object sender, AsyncActionCompletedEventArgs args) {
         if (args.Error != null)
            DisplayError(args.Error);
 
         if (args.Cancelled)
            WriteLine(args.Action.Name + " Cancelled");
 
         if (args.Error == null && !args.Cancelled) {
            if (args.Action is Serial<string>) {
               // Form2 displays the contents of a web site in a Browser control
               //Form2 f2 = new Form2();
               //f2.SetContent(((Serial<string>)args.Action).Result);
               //f2.Text = args.Action.Name;
               //f2.Show();
            }
            else {
               WriteLine("[{0}][{1}] completed", args.ManagedThreadId, args.Action.Name);
            }
         }
      }
 
      private void ReportProgress(object sender, AsyncActionProgressChangedEventArgs args) {
         WriteLine("[{0}][{1}] {2}", args.ManagedThreadId, args.Action.Name, args.ProgressPercentage);
      }
 
      private void TraceAction(object sender, AsyncActionTracedEventArgs args) {
         WriteLine("[{0}][{1}] {2}", args.ManagedThreadId, args.Action.Name, args.Message);
      }
 
      private void DisplayError(Exception ex) {
         if (ex != null) {
            if (ex is AsyncActionException)
               WriteLine("{0} -> {1}", ((AsyncActionException)ex).Action.Name, ex.Message);
            else
               WriteLine(ex.Message);
            DisplayError(ex.InnerException);
         }
      }
 
      private void startToolStripButton_Click(object sender, EventArgs e) {
         try {
            DownloadAll();
            logListView.Items.Clear();
            WriteLine("[{0}] GUI thread is alive...", System.Threading.Thread.CurrentThread.ManagedThreadId);
         }
         catch (Exception ex) {
            WriteLine(ex.Message);
         }
      }
 
      private void cancelToolStripButton_Click(object sender, EventArgs e) {
         context.Cancel();
      }
   }
}

We are using exactly the same code from our example in part II, but we are now handling operation events. Note that we don’t need to check if the event handling methods are executing in the GUI thread since the AsyncContext class is already taking care of the details by posting the events to the current synchronization context.

The first of the following two screenshots depicts the application after successfully downloading the contents of several web sites. The log is created by handling Trace events that return the managed thread id where the action is executing, the action’s name, and total bytes downloaded so far.

The second screenshot shows what happens when the Start button is hit twice after the operation has been started - An exception is raised by a “busy” context.

Also note that when the Cancel button is hit, the entire operation stops (the completion event is raised with the Cancelled flag set to true).

Async Test Results

Async Test Results

The best way to understand what’s going on here is by playing with this code, so I encourage you to build your own prototypes… and let me know if you find any issues please ;-)

What’s next?

Now that I have a system to invoke asynchronous operations in any predetermined sequence, I think it would be a good idea to go back to the drawing board and add an asynchronous component to the CoNatural.Data DAL. The ADO.NET SQL Server provider (under System.Data.SqlClient) already supports asynchronous execution, so with a couple of extension methods I think it will be very easy to invoke asynchronous data commands that will:

  • Execute without blocking the calling thread, making our GUI more responsive.
  • Allow the user to cancel commands.
  • Invoke multiple commands in an ordered sequence or in parallel to maximize performance.
  • The commands can be executed on one or multiple databases, and under the same transaction scope.

Stay tuned.

Leave a comment