Click here to Skip to main content
15,861,168 members
Articles / Programming Languages / C#

Progress Reporting in C# 5 Async

Rate me:
Please Sign up or sign in to vote.
4.89/5 (28 votes)
6 Apr 2013CPOL6 min read 123.5K   3.2K   73   14
The new progress reporting pattern explained and revealed

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

Introduction

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).

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:

C#
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 Event Handler

C#
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

C#
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).

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 the CTP and deserves a section to itself. Just assume I have some progress object:

C#
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:

C#
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

The TAP method DownloadStringTaskAsyncEx doesn't exist in the CTP, so I had to write it.

There is an extension method on WebClient:

C#
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:

C#
public static Task<Stream> OpenReadTaskAsync(this WebClient webClient, string address);

public static Task<int> ReadAsync
	(this Stream source, 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:

C#
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 ); // calls SynchronizationContext.Post
            }
         }
      }

      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().

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 defined in the CTP's AsyncCtpLibrary.dll assembly.

C#
namespace System.Threading
{
   // Summary:
   //     Represents progress of an asynchronous operation.
   //
   // Type parameters:
   //   T:
   //     Specifies the type of the progress data.
   public interface IProgress<T>
   {
      // Summary:
      //     Reports that progress has changed and provides the new progress value.
      //
      // Parameters:
      //   value:
      //     The new progress value.
      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.

EventProgress<T>

Fortunately, there is an implementation of IProgress in the CTP: EventProgress<T>. Here it is:

C#
namespace System.Threading
{
   // Summary:
   //     Provides an implementation of IProgress(Of T) that raises an event for each
   //     reported progress update.
   //
   // Type parameters:
   //   T:
   //     Specifies the type of data provided with a reported progress update.
   public sealed class EventProgress<T> : IProgress<T>
   {
      public EventProgress();

      // Summary:
      //     Occurs whenever a progress change is reported.
      public event EventHandler<EventArgs<T>> ProgressChanged;

      // Summary:
      //     Creates the progress object from the specified delegate.
      //
      // Parameters:
      //   handler:
      //     The delegate to invoke for each progress report.
      //
      // Returns:
      //     The initialized progress object.
      public static EventProgress<T> From( Action<T> handler );
      
      // This is captured in the constructor from SynchronizationContext.Current
      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.

EventArgs<T>

Notice the definition of the event in the class above:

C#
public event EventHandler<EventArgs<T>> ProgressChanged;

The EventHandler delegate is already in the framework:

C#
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:

C#
namespace System
{
   // Summary:
   //     Provides an EventArgs type that contains generic data.
   //
   // Type parameters:
   //   T:
   //     Specifies the type of data included with the event args.
   [DebuggerDisplay( "Value = {m_value}" )]
   public class EventArgs<T> : EventArgs
   {
      private readonly T m_value;
      
      // Summary:
      //     Initializes the event arguments.
      //
      // Parameters:
      //   value:
      //     The state to include with the event arguments
      public EventArgs( T value )
      {
         m_value = value;
      }

      // Summary:
      //     Get the state included with the event arguments.
      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.

Conclusion

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
I discovered C# and .NET 1.0 Beta 1 in late 2000 and loved them immediately.
I have been writing software professionally in C# ever since

In real life, I have spent 3 years travelling abroad,
I have held a UK Private Pilots Licence for 20 years,
and I am a PADI Divemaster.

I now live near idyllic Bournemouth in England.

I can work 'virtually' anywhere!

Comments and Discussions

 
QuestionVery nice Pin
BillW338-Apr-13 10:05
professionalBillW338-Apr-13 10:05 
GeneralMy vote of 5 Pin
CS14017-Apr-13 0:27
CS14017-Apr-13 0:27 
QuestionWhat point am I missing? Pin
Kamran Behzad20-Jan-13 19:41
Kamran Behzad20-Jan-13 19:41 
Questioneasier? Pin
pip0105-Dec-12 4:56
pip0105-Dec-12 4:56 
NewsC# 5 CTP refresh 28 April 2011 Pin
Nicholas Butler4-Jun-11 10:50
sitebuilderNicholas Butler4-Jun-11 10:50 
GeneralMy vote of 5 Pin
Joshi, Rushikesh17-May-11 2:32
professionalJoshi, Rushikesh17-May-11 2:32 
GeneralMy vote of 5 Pin
sshanmuga405-Mar-11 4:40
sshanmuga405-Mar-11 4:40 
GeneralMy vote of 5 Pin
Duarte Cunha Leão23-Dec-10 12:51
Duarte Cunha Leão23-Dec-10 12:51 
GeneralRe: My vote of 5 Pin
Nicholas Butler25-Dec-10 22:29
sitebuilderNicholas Butler25-Dec-10 22:29 
Questionis SynchronizationContext necessary ? Pin
embroidery21-Nov-10 20:26
embroidery21-Nov-10 20:26 
AnswerRe: is SynchronizationContext necessary ? Pin
Nicholas Butler22-Nov-10 0:14
sitebuilderNicholas Butler22-Nov-10 0:14 
GeneralRe: is SynchronizationContext necessary ? Pin
embroidery22-Nov-10 3:57
embroidery22-Nov-10 3:57 
GeneralMy vote of 5 Pin
rht34121-Nov-10 4:32
rht34121-Nov-10 4:32 
GeneralRe: My vote of 5 Pin
Nicholas Butler21-Nov-10 5:57
sitebuilderNicholas Butler21-Nov-10 5:57 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.