Note: This article was written using the first CTP of TAP ( released 2010-10-28 )
There's an updated version for .NET 4.5 RTM here
http://simplygenius.net/Article/AncillaryAsyncProgress[^]
Table of Contents
This article is about the new TAP - the Task-based Asynchrony Pattern and concentrates on the support for progress reporting.
I explain a simple program that gets text from a 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.
Progress support was not really addressed in the framework in .NET 4, but appears to be front-and-centre in this CTP. It may just be wishful thinking, but I hope that all the new TAP methods in .NET 5 will support progress reporting (and cancellation).
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.
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 EventProgress<DownloadStringTaskAsyncExProgress>();
progress.ProgressChanged += ( s, e ) =>
{
progressBar.Value = e.Value.ProgressPercentage;
txtResult.Text += e.Value.Text;
};
return await wc.DownloadStringTaskAsyncEx
( @"http://ancillaryasync.nickbutler.net/Murphy.ashx", progress );
}
}
}
There is quite a lot going on here. I will explain each piece in turn.
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.
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 URL 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 ( at least in this CTP).
I'll fudge a little bit here and gloss over the implementation of the progress object. It uses some new classes in the CTP and deserves a section to itself. Just assume I have some progress
object:
var progress = new EventProgress<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 EventArgs
object will contain information 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.Value.ProgressPercentage;
txtResult.Text += e.Value.Text;
};
Well, now we have our client code, so now for the fun bit...
The TAP method DownloadStringTaskAsyncEx
doesn't exist in the CTP, so I had to write it.
There is an extension method on WebClient
:
public static Task<string> DownloadStringTaskAsync(
this WebClient webClient,
Uri address,
CancellationToken cancellationToken,
IProgress<DownloadProgressChangedEventArgs> progress);
However, the DownloadProgressChangedEventArgs
class only reports the progress percentage and doesn't give access to the stream
buffer, so it doesn't meet the requirements.
There are a couple of extension methods in the CTP that are just right though. One takes a WebClient
and returns a Stream
, and the other reads the stream
asynchronously:
public static Task<Stream> OpenReadTaskAsync(this WebClient webClient, string address);
public static Task<int> ReadAsync
(this Stream source, byte[] buffer, int offset, int count);
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:
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 buffer = new byte[ 1024 ];
var bytes = 0;
var all = String.Empty;
using ( var stream = await wc.OpenReadTaskAsync( url ) )
{
int total = -1;
Int32.TryParse( wc.ResponseHeaders[ HttpResponseHeader.ContentLength ],
out total );
for ( ; ; )
{
int len = await stream.ReadAsync( buffer, 0, buffer.Length );
if ( len == 0 ) break;
string text = wc.Encoding.GetString( buffer, 0, len );
bytes += len;
all += text;
if ( progress != null )
{
var args = new DownloadStringTaskAsyncExProgress();
args.ProgressPercentage = ( total <= 0 ? 0 : ( 100 * bytes ) / total );
args.Text = text;
progress.Report( args );
}
}
}
return all;
}
}
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 fired by using SynchronizationContext.Post
to get on the right thread. 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. All the TAP method has to do is call Report()
.
So what is this magic progress
object? There are a few new types we need to look at here.
The signature for DownloadStringTaskAsyncEx
actually takes an IProgress<T>
. This is a new interface defined in the CTP's AsyncCtpLibrary.dll
assembly.
namespace System.Threading
{
public interface IProgress<T>
{
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. That implementation can call the Report
method of this object when it wants to report progress. Makes sense, yes? Now I need to create the object, so I need a class that implements the interface.
Fortunately, there is an implementation of IProgress
in the CTP: EventProgress<T>
. Here it is:
namespace System.Threading
{
public sealed class EventProgress<T> : IProgress<T>
{
public EventProgress();
public event EventHandler<EventArgs<T>> ProgressChanged;
public static EventProgress<T> From( Action<T> handler );
private readonly SynchronizationContext m_synchronizationContext;
void IProgress<T>.Report( T value )
{
...
m_synchronizationContext.Post( o => ProgressChanged( this, value ), null );
...
}
}
}
I have edited the code above to highlight the interesting bits. 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.
Notice that a SynchronizationContext.Post
is used. 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.
Also, there is a bug in the implementation of the static
factory method, From( Action<T> handler )
, so you can't use it in this CTP.
Notice the definition of the event in the class above:
public event EventHandler<EventArgs<T>> ProgressChanged;
The EventHandler
delegate is already in the framework:
public delegate void EventHandler<TEventArgs>
(object sender, TEventArgs e) where TEventArgs : EventArgs;
Notice the constraint - the type parameter must derive from EventArgs
.
There is a new class in the CTP which helps: EventArgs<T>
. It's quite simple, but it keeps EventHandler
happy:
namespace System
{
[DebuggerDisplay( "Value = {m_value}" )]
public class EventArgs<T> : EventArgs
{
private readonly T m_value;
public EventArgs( T value )
{
m_value = value;
}
public T Value
{
get
{
return m_value;
}
}
}
}
So that ties it all together. The value
object can be of any type as it is wrapped in an EventArgs<T>
before the EventProgress<T>.ProgressChanged
event is raised.
As I have shown, the new Task-based Asynchrony 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 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 will be implementing TAP versions of all blocking methods in the framework. I hope and expect they will provide overloads that support progress reporting using IProgress<T>
. If they do, application developers will be able to 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.
History
- 21st November, 2010: Initial post