Simply Genius .NET
Software that makes you smile
The Progress Reporting Pattern in C# 5 async
|
Publicly recommend on Google
|
Downloads
Table of Contents
Introduction
This article is about the new TAP - the Task-based Asynchronous Pattern - and concentrates on the support for progress reporting.
I explain a simple program that gets text from a web server asynchronously.
As I show each layer of the solution, I explain the implementation and framework support.
I will assume you already know how async
and await
work.
Firstly, I will show you how simple this has become for application developers.
Then I will go under the hood and explore the implementation of a TAP method with progress reporting.
The Requirement
I have hosted a page on my web server that shows a short list of trite quotes, generated randomly from a list I obtained... The page takes 7 seconds to generate a list of 8 quotes. Each quote is flushed to the network as it is generated, one per second.
The requirement is to write an application that consumes this service and shows both the current progress percentage and all the quotes as soon as they arrive from the ether.
The Client
I wrote a WinForms app as the client.
Here is the entire code for the main Form
:
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
Shown += async
( s, e ) => { txtResult.Text = await
DownloadAsync() + "Done!"; };
}
async
Task<string> DownloadAsync()
{
using ( var wc = new WebClient() )
{
var progress = new Progress<DownloadStringTaskAsyncExProgress>();
progress.ProgressChanged += ( s, e ) =>
{
progressBar.Value = e.ProgressPercentage;
txtResult.Text += e.Text;
};
return await
wc.DownloadStringTaskAsyncEx(
@"http://ancillaryasync.nickbutler.net/Murphy.ashx", progress );
}
}
}
public class DownloadStringTaskAsyncExProgress
{
public int ProgressPercentage { get; set; }
public string Text { get; set; }
}
There is quite a lot going on here. I will explain each piece in turn.
Shown event handler
Shown += async
( s, e ) => { txtResult.Text = await
DownloadAsync() + "Done!"; };
I am using a lambda
to handle this event.
Notice that you can use async
and await
here too, not just for named methods.
The method that does the work, DownloadAsync()
, eventually returns a string
.
When this method completes, the handler just appends "Done!"
to the result and shows that.
This is how I know the whole process has finished.
Doing the work
return await
wc.DownloadStringTaskAsyncEx( @"http://ancillaryasync.nickbutler.net/Murphy.ashx", progress );
The work is done by an extension method on the WebClient
class: DownloadStringTaskAsyncEx
.
This is also a TAP method, so I can use await
to yield control while it is executing.
It takes a URI and returns a string
- all well and good.
But it also takes an object called progress
as a parameter.
This is the new pattern for progress reporting.
The progress event
I'll fudge a little bit here and gloss over the implementation of the progress object. It uses some new classes in .NET 4.5 and deserves a section to itself.
Just assume I have some progress
object:
var progress = new Progress<DownloadStringTaskAsyncExProgress>();
All I need to tell you now is that it has a ProgressChanged
event that is raised by the TAP method
at suitable points during its execution.
This event will be raised on the UI thread,
and the e
parameter will be an instance of my DownloadStringTaskAsyncExProgress
class that contains info about the current progress.
So, all I need to do is add a handler that updates my UI controls:
progress.ProgressChanged += ( s, e ) =>
{
progressBar.Value = e.ProgressPercentage;
txtResult.Text += e.Text;
};
Well, now we have our client code, so now for the fun bit...
The TAP method
There is a new TAP method on WebClient
,
but it doesn't take an IProgress<T>
parameter
and doesn't give access to the response stream,
so it doesn't meet the requirements.
public Task<string> DownloadStringTaskAsync( string address ) { ... }
However, there is another new TAP method on WebClient
that returns a Stream
:
public Task<Stream> OpenReadTaskAsync( string address ) { ... }
And the Stream
class also has some new TAP methods. Here is the one I need:
public Task<int> ReadAsync( byte[] buffer, int offset, int count );
Implementation
I used these two methods to write the implementation of DownloadStringTaskAsyncEx
,
with progress reporting that includes the result text as it arrives.
Here is the entire source:
public class DownloadStringTaskAsyncExProgress
{
public int ProgressPercentage { get; set; }
public string Text { get; set; }
}
static class WebClientExtensions
{
public static async
Task<string> DownloadStringTaskAsyncEx(
this WebClient wc,
string url,
IProgress<DownloadStringTaskAsyncExProgress> progress )
{
var receivedBytes = 0;
var receivedText = String.Empty;
using ( var stream = await
wc.OpenReadTaskAsync( url ) )
{
int totalBytes = -1;
Int32.TryParse( wc.ResponseHeaders[ HttpResponseHeader.ContentLength ], out totalBytes );
var buffer = new byte[ 4096 ];
for ( ; ; )
{
int len = await
stream.ReadAsync( buffer, 0, buffer.Length );
if ( len == 0 ) { await Task.Yield(); break; }
string text = wc.Encoding.GetString( buffer, 0, len );
receivedBytes += len;
receivedText += text;
if ( progress != null )
{
var args = new DownloadStringTaskAsyncExProgress();
args.ProgressPercentage = ( totalBytes < 0 ? 0 : ( 100 * receivedBytes ) / totalBytes );
args.Text = text;
progress.Report( args ); // calls SynchronizationContext.Post
}
}
}
return receivedText;
}
}
This TAP method also happens to be an async
method.
This is perfectly correct and allowed me to use the TAP forms of
WebClient.OpenRead
and Stream.Read
.
This means that there is no blocking in the method and so it is safe to execute on the UI thread.
One interesting detail is that I must create a new instance of DownloadStringTaskAsyncExProgress
each time I call progress.Report()
.
This is because the ProgressChanged
event is marshalled on to the UI thread by using
SynchronizationContext.Post()
.
If I tried to reuse a single Progress
object, there would be a race condition between
the event handlers and the next call to Report()
.
That's all there is to it:
the caller creates an IProgress<T>
object and passes it in
and all the TAP method has to do is call Report()
.
The Progress object
So what is this magic progress
object?
There are a few new types we need to look at here.
IProgress<T>
The signature for DownloadStringTaskAsyncEx
actually takes an
IProgress<T>
.
This is a new interface in .NET 4.5, defined in the mscorlib.dll
assembly.
namespace System
{
// Summary:
// Defines a provider for progress updates.
//
// Type parameters:
// T:
// The type of progress update value.This type parameter is contravariant. That
// is, you can use either the type you specified or any type that is less derived.
// For more information about covariance and contravariance, see Covariance
// and Contravariance in Generics.
public interface IProgress<in T>
{
// Summary:
// Reports a progress update.
//
// Parameters:
// value:
// The value of the updated progress.
void Report( T value );
}
}
It only has one method: void Report( T value )
.
Remember, an object that implements this interface is passed into the TAP method.
The implementation can call the Report
method of this object when it wants to report progress.
Progress<T>
Now I need to create the progress
object, so I need a class that implements the interface.
Fortunately, there is an implementation of IProgress<T>
in the framework, called just Progress<T>
.
It is also in mscorlib.dll
and here it is:
namespace System
{
// Summary:
// Provides an System.IProgress<T> that invokes callbacks for each reported
// progress value.
//
// Type parameters:
// T:
// Specifies the type of the progress report value.
public class Progress<T> : IProgress<T>
{
// Summary:
// Initializes the System.Progress<T> object.
public Progress();
//
// Summary:
// Initializes the System.Progress<T> object with the specified callback.
//
// Parameters:
// handler:
// A handler to invoke for each reported progress value. This handler will be
// invoked in addition to any delegates registered with the System.Progress<T>.ProgressChanged
// event. Depending on the System.Threading.SynchronizationContext instance
// captured by the System.Progress<T> at construction, it is possible that this
// handler instance could be invoked concurrently with itself.
public Progress( Action<T> handler );
// Summary:
// Raised for each reported progress value.
public event EventHandler ProgressChanged;
// Summary:
// Reports a progress change.
//
// Parameters:
// value:
// The value of the updated progress.
protected virtual void OnReport( T value );
}
}
Basically, when you instantiate this class, it captures the current thread's SynchronizationContext
.
Then, each time Report
is called from inside the TAP method,
it raises the ProgressChanged
event on the right thread.
Note that Report()
uses a SynchronizationContext.Post()
.
This is why you would get a race condition between previous events being handled and subsequent calls to Report
,
if you reused your value
objects ( instances of T ) in your TAP method.
Conclusion
As I have shown, the new Task-based Asynchronous Pattern makes this sort of code much easier to write - and read. Consumers of TAP methods are almost trivial. Even if you have to write a method TAP yourself, it is still quite simple if you know the patterns.
The async
and await
keywords just work.
Anders and the team have done a great job.
The arduous bit for Microsoft was implementing TAP versions of the methods in the framework that block on IO.
Application developers can now give their users more information about what an app is doing in an increasingly asynchronous world.
Well, that's it. I hope you have enjoyed reading this article. Thanks.
License
This article, along with any associated source code and files, is licensed under The Code Project Open License.
Contact
If you have any feedback, please feel free to contact me.