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:
- The [Download not found] for the main components.
- The CoNatural.Extensions Source Code (194) for the extension methods we are using in our examples.
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).
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.
