46

I'm completely new to C# 5's new async/await keywords and I'm interested in the best way to implement a progress event.

Now I'd prefer it if a Progress event was on the Task<> itself. I know I could just put the event in the class that contains the asynchronous method and pass some sort of state object in the event handler, but to me that seems like more of a workaround than a solution. I might also want different tasks to fire off event handlers in different objects, which sounds messy this way.

Is there a way I could do something similar to the following?:

var task = scanner.PerformScanAsync();
task.ProgressUpdate += scanner_ProgressUpdate;
return await task;
Connell
  • 13,925
  • 11
  • 59
  • 92

4 Answers4

67

The recommended approach is described in the Task-based Asynchronous Pattern documentation, which gives each asynchronous method its own IProgress<T>:

public async Task PerformScanAsync(IProgress<MyScanProgress> progress)
{
  ...
  if (progress != null)
    progress.Report(new MyScanProgress(...));
}

Usage:

var progress = new Progress<MyScanProgress>();
progress.ProgressChanged += ...
PerformScanAsync(progress);

Notes:

  1. By convention, the progress parameter may be null if the caller doesn't need progress reports, so be sure to check for this in your async method.
  2. Progress reporting is itself asynchronous, so you should create a new instance of your arguments each time you call (even better, just use immutable types for your event args). You should not mutate and then re-use the same arguments object for multiple calls to Progress.
  3. The Progress<T> type will capture the current context (e.g., UI context) on construction and will raise its ProgressChanged event in that context. So you don't have to worry about marshaling back to the UI thread before calling Report.
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 2
    Just so I understand and perhaps to help others in future... your reasoning for point 2. is that if a change was made to mutable object passed to Progress.Report() before the Report method completed then it would result in the orginal values not being reported? – Dave Williams Jun 17 '13 at 08:57
  • 4
    Almost. What I mean is that when the `Report` method has completed (and returned), the actual reporting hasn't yet taken place. If your `T` is mutable and you change it after passing it to `Report`, then you have a race condition. The actual reporting may see the old value, the new value, or a mixture (i.e., the old value for some fields and the new value for others). – Stephen Cleary Jun 17 '13 at 12:19
  • Yes that makes a lot more sense now thank you. I read a few of your blogs etc where you mentioned the race condition but it wasn't immediately obvious until I read this question. Thanks – Dave Williams Jun 17 '13 at 12:27
  • Is there no guarantee then that the ProgressChanged delegate gets what is Report()'d in order? From my understanding it would seem even if the object passed is immutable, if I update an interface with the latest T I've received there's a chance I'll update it to show more stale information because I've received an older progress object after a newer one. – deed02392 May 30 '15 at 23:33
  • @deed02392: Technically, there is no ordering requirement. However, every implementation I'm aware of does preserve order. – Stephen Cleary May 31 '15 at 00:58
  • I realised after I posted the above that what you meant was that we shouldn't mutate the progress object we passed to Report(), because Report() will return immediately and so you don't know what your handler at the other end of the interface will actually receive in terms of the progress object's values. Out of interest, what do you mean there's technically no ordering requirement but that implementations do preserve it? Wouldn't an implementation that did not be very archaic? – deed02392 Jun 01 '15 at 09:47
  • @deed02392: It would be surprising, for sure. I just meant that *technically*, I don't think anyone has thought to write that down. – Stephen Cleary Jun 01 '15 at 12:15
49

Simply put, Task doesn't support progress. However, there's already a conventional way of doing this, using the IProgress<T> interface. The Task-based Asynchronous Pattern basically suggests overloading your async methods (where it makes sense) to allow clients to pass in an IProgress<T> implementation. Your async method would then report progress via that.

The Windows Runtime (WinRT) API does have progress indicators built-in, in the IAsyncOperationWithProgress<TResult, TProgress> and IAsyncActionWithProgress<TProgress> types... so if you're actually writing for WinRT, those are worth looking into - but read the comments below as well.

Marc.2377
  • 7,807
  • 7
  • 51
  • 95
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 3
    For WinRT, it would be far easier to write a normal `async` method with `IProgress` (following C# conventions) and then use `AsyncInfo.Run` to wrap it into an `IAsyncOperationWithProgress`/`IAsyncActionWithProgress` rather than implementing those interfaces directly. – Stephen Cleary Mar 14 '13 at 11:54
  • @StephenCleary: Well it depends on whether you're thinking about the *caller* or the *implementation*. If you're trying to expose functionality for others to call into, I'd have thought the WinRT "native" types would be more appropriate. Obviously there are the various converters between the two. – Jon Skeet Mar 14 '13 at 12:40
  • 1
    It's easier to follow C# conventions in C#, and use wrappers (`AsTask`/`AsyncInfo`) at the boundaries. So a WinRT component implemented in C# would return the WinRT interface type, but would implement it using regular `async` wrapped in `AsyncInfo`. – Stephen Cleary Mar 14 '13 at 12:47
  • @StephenCleary: Fair enough, I'll take your word for it :) – Jon Skeet Mar 14 '13 at 12:48
11

I had to piece together this answer from several posts as I was trying to figure out how to make this work for code that is less trivial (ie events notify changes).

Let's assume you have a synchronous item processor that will announce the item number it is about to start work on. For my example I am just going to manipulate the content of the Process button, but you can easily update a progress bar etc.

private async void BtnProcess_Click(object sender, RoutedEventArgs e)
{       
    BtnProcess.IsEnabled = false; //prevent successive clicks
    var p = new Progress<int>();
    p.ProgressChanged += (senderOfProgressChanged, nextItem) => 
                    { BtnProcess.Content = "Processing page " + nextItem; };

    var result = await Task.Run(() =>
    {
        var processor = new SynchronousProcessor();

        processor.ItemProcessed += (senderOfItemProcessed , e1) => 
                                ((IProgress<int>) p).Report(e1.NextItem);

        var done = processor.WorkItWorkItRealGood();

        return done ;
    });

    BtnProcess.IsEnabled = true;
    BtnProcess.Content = "Process";
}

The key part to this is closing over the Progress<> variable inside ItemProcessed subscription. This allows everything to Just works ™.

Chris Marisic
  • 32,487
  • 24
  • 164
  • 258
  • Not sure where does the "SynchronousProcessor" come from. – Feng Jiang Dec 05 '20 at 16:06
  • 1
    @FengJiang that's your code that requires synchronous processing, *whatever it is*. It's an illustration of how you use the `ItemProcessed` event of **your** processor to be bounded to the `ProgressChanged` event to allow for asynchronous UI updates . – Chris Marisic Dec 07 '20 at 16:27
0

When using a Task.Run lambda I have used an Invoke Action inside of this to update a ProgressBar control. This may not be the best way but it worked in a pinch without having to restructure anything.

   Invoke(new Action(() =>

               {
                   LogProgress();
               }));

Which takes it to...

        private void LogProgress()
        {       
          progressBar1.Value = Convert.ToInt32((100 * (1.0 * LinesRead / TotalLinesToRead)));
        }
Ian Fafard
  • 51
  • 8
  • This will update your `progressBar` on the main GUI thread thus it will make the form unresponsive especially if the loop is too big. – Ahmed Suror Jul 08 '21 at 18:57