Thursday, December 15, 2016

Custom Progress Reporting with Task

Last time, we looked at how to do simple progress reporting with Task. In that example, we passed an integer from our asynchronous method to the calling method. This time around, we'll use a custom reporting payload. This will let us pass along any information we want.

For this, we'll show information as each record in our dataset is "processed". Here's the output:

Application w/ Progress Reporting

The text shows us the progress. "Processing 6 of 7" tells us which record we're on and how many there are total. And "Dante Montana" tells us some details about the current record.

So let's see how we can get this data from our asynchronous method into the calling code for display.

Reporting Progress with Task
Just like we saw last time, we'll use the IProgress<T> interface. But instead of using an integer, we'll use a custom object (which we'll call "PersonProgressData"). The steps for using this are pretty much the same as we saw before.

Just like with our previous example, we'll start with the code that was used in prior Task articles: GitHub: jeremybytes/using-task.

This time, we'll be looking at branch 10b-CustomProgress.

Let's see what we need to do to add this custom progress to our application.

Adding the Custom Reporting Object
First, we'll create the custom object that will hold our reporting payload. For this, we'll have 3 properties: the current item number, the total number of items, and the name on the current item.

I've added a new class to the top of the PersonRepository.cs file:

Custom Progress Reporting Class

Here we have 3 read-only properties and a constructor to populate the values.

Updating the Asynchronous Method
We use the IProgress<T> interface to get things to work. So we'll create a parameter of that type in our asynchronous method.

Here's the updated "Get" method of the PersonRepository class (PersonRepository.cs):

Updated Method Parameters

Notice that we're using our custom type (PersonProgressData) as the generic type parameter.

To use this in our code, I've modified the method itself a bit. First, we had an artificial 3 second delay at the top of the method (this was to help us see the asynchronous process in action). I've removed that for this example:

Delay has been removed

Next, I made some modifications to the code before it returns the data. In the original code, we returned the data directly from the service. This time, we are going to "process" the data before returning it. Here's that block of code:

Reporting Progress

Rather than returning the result of the "await", we store it in a variable we can use. Then we use a "for" loop to iterate through all of the records. This is where we "process" the data. We're just simulating that.

First we check our cancellation token. Inside a loop is a great place to check for cancellation. This allows us to short-circuit the processing.

The next step is to report the progress. We do this with "progress?.Report()". The "Report" method will take our custom object as a parameter, so we need to new up an instance of the "PersonProgressData" class with the appropriate values.

One other thing to note is that we're using the null-conditional operator (?.) here. If the "progress" object happens to be null (due to the parameter), then nothing will happen (and we also won't get a NullReferenceException). If the "progress" object is not null, then the "Report" method is called.

The last step is to delay for 300 milliseconds. This simulates our processing and gives us a chance to see the values updating in the UI as progress is reported.

The result is that we'll get a progress report for each item in our dataset.

Note: If you try to compile the code now, you'll get a bunch of errors since we added a new parameter. You can update the calls to the "Get" method by adding a "null" parameter, and the code will work just like it did before.

That takes care of progress reporting from the asynchronous method, now let's see what changes we need to make to the calling code.

Updating the UI
The UI needs to be updated to give us a spot to report the progress. We'll add a text block to our existing WPF form just like we did when we reported simple progress in the prior article. Here's the XAML (from MainWindow.xaml):

UI Elements for Progress Reporting

Now that we have this spot in our UI, we just need to update the calling code.

Updating the Calling Code
We need an implementation of the IProgress<PersonProgressData> interface. We'll start by creating a class-level field to hold this object. This will be in the code-behind of our form (MainWindow.xaml.cs):

IProgress<T> Field

By having a class-level field, we can share this object between our various method calls.

But now we need an object that implements this interface. Fortunately, we can use the built in Progress<T> object. The constructor for Progress<T> take a delegate as a parameter. This delegate is the method that we want to run when progress is reported.

The delegate needs to return void and take a "PersonProgressData" object as a parameter. In the last example (with the simple progress), we used a separate named delegate. Here we'll use a lambda expression instead.

Here's what our constructor looks like now:

Creating the Progress object

Let's take a closer look at the lambda expression. The "d" is our parameter and represents the "PersonProgressData" object (I used "d" for "data").

The body of the lambda expression sets the value in our text block. This is a string that uses the "Item" (number of the current item), "Total" (total number of items), and "Name" (name on the item) that comes from our custom reporting object.

Note: If you want to get a better understanding of lambda expressions, take a look at Learn to Love Lambdas (and LINQ, Too!).

Adding the Parameter to the Calling Code
The last step is to add the class-level field as a parameter in the calling code.

Here's the updated call in the "FetchWithTaskButton_Click" method:

Updated "Get" Call

We just added "progress" as a parameter to the "Get" method call. This code looks exactly the same as the code we saw with the simple progress reporting.

The result is that we get the progress reported in our UI:


To update the call where we use "await", things look very similar. Here's the code from "FetchWithAwaitButton_Click":

Updated "Get" Call

This will give us the same type of progress reporting when we use the "await" button.

Where To Go From Here
We've seen how we can pass whatever we like for progress reporting. We can use a built in type (like "int") or we can create a custom type that will hold whatever values we need (like "PersonProgressData").

Showing Progress
We also have a lot of freedom on how we handle things in the UI. Here we just updated a text block. But we could just as easily hook things up to a progress bar or simply display a status message. Concepts around progress reporting get interesting once we start diving in a bit deeper.

Incremental Progress Reporting
Something else to think about: we might not process the items in order. For example, rather than "processing" in sequence, we could kick off parallel tasks to process the items. In this case, having an item number might not make sense because the actual order could be different based on the task scheduler. Stephen Cleary recommends doing "incremental" progress reporting rather than "cumulative" progress reporting. This makes a lot of sense in asynchronous environments. See the "Defining 'Progress'" section of his article for more information.

Update: I wrote an article on how adding a bit of parallelism completely destroys our example. But it can be fixed by using incremental reporting: When Progress Reporting Goes Bad: Incremental Progress vs. Cumulative Progress.

Thread Warning
One last warning. There are some subtleties regarding where we create the Progress<T> object. In our code, we're updating UI elements, so we need the delegate to run on the UI thread. Since we're creating the Progress<T> object on our UI thread, things work as expected. It's tempting to create this object in a different spot in the code. If we're not careful, we could end up with unexpected behavior. For more information, take a look at this article: Pay Attention to Where You Create Progress Objects.

Reporting progress is always lots of fun. For more information on Task and await, check out the resources available here: I'll Get Back To You: Task, Await, and Asynchronous Methods.

Happy Coding!

No comments:

Post a Comment