Thursday, December 15, 2016

Simple Progress Reporting with Task

Last week at Live! 360 Orlando, I got to give one of my favorite talks: I"ll Get Back to You: Task, Await, and Asynchronous Methods. Afterwards, I had a couple people ask me about reporting progress, and that's when I realized, I didn't have any articles to point to about it. So, it's time to correct that.

Today we'll look at reporting progress using just an integer value. In the next article, we'll see how we can create our own custom payload to use as part of progress reporting.

We'll just create a simple countdown while we're waiting for the data to return:

Application w/ Progress Reporting

The "Countdown to Data: 2" is our progress item.

Note: Yes, I realize that this is an overly simple example, but we'll be able to see the progress reporting in action. We'll look at a more useful example a bit later.

Reporting Progress with Task
The good news is that we have an interface we can use for progress reporting: IProgress<T>. The generic type parameter is for the payload that we're returning. The interface itself has only one member: Report. As you might guess, this is used to report progress.

On the consumer side, we give our progress object a delegate that will get called whenever the Report method is called in the asynchronous process.

So let's see how this will works. For this example, we'll start with the same code that was used in prior articles. The GitHub repo is here: GitHub: jeremybytes/using-task

Specifically, we'll be looking at a branch: 10a-SimpleProgress.

We'll only modify 3 files to get this work.

Updating the Asynchronous Method
We need a way to get the progress object into our asynchronous method, so we'll add a parameter. As noted above, we just need to add a parameter of type IProgress<T>.

This code is in the "PersonRepository" class (PersonRepository.cs):

Updated Method Parameters

For this example, we're going to report an integer value back to the calling code, so we have are using "int" as the generic type parameter.

Then in our code, we just need to call the "Report" method whenever we want to send something back to the caller. We're keeping things really simple here (we'll get a bit more complex in the next article).

Our original code has a 3-second delay in the code. This was to demonstrate the asynchronous nature of the method calls; we could still interact with the UI while waiting for the asynchronous process to complete. For this example, we'll split that 3-second delay into 3 separate 1-second delays. Then we'll report progress on each one by creating a simple countdown.

Here's what the updated code looks like inside our "Get" method:

Reporting Progress

We are reporting progress 3 times in this case. As you can see, all we need to do is call the "Report" method on our progress object and then pass in the value we want to send back -- in this case an integer.

I'm using the null conditional operator (?.) just in case the calling code does not pass in a valid progress object (we'll look at the calling side in just a bit). If "progress" is null, then nothing happens: "Report" is not called, but we also won't get a NullReferenceException. If "progress" is not null, then the "Report" method is called with the specified payload.

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.

Now let's see what we need to do to the calling code to use this.

Updating the UI
The UI needs to be updated to give us a spot to report the progress. In this case, I simply added a text block to our existing WPF form. Here's the XAML for that (from MainWindow.xaml):

UI Elements for Progress Reporting

This is a pretty simple change. It's mostly about positioning and sizing. But now we have a spot to put our progress message.

Updating the Calling Code
This is where things get interesting. In our calling code, we need an implementation of the "IProgress<int>" interface. We'll start by creating a class-level field to hold our progress object. This is at the class level because we'll share the same progress reporting across our methods.

Here's the field added to the code-behind of our form (MainWindow.xaml.cs):

Field for our Progress object

Now we need an object that implements the "IProgress<int>" interface. Fortunately, there is a built in "Progress" type that implements this interface.

The constructor for the Progress<T> type takes a delegate parameter. This delegate is the code that we want to run when the "Report" method is called. The delegate takes a parameter that matches the generic type parameter ("int" in our case), and this is how we get access to the payload.

For our code, we'll create the Progress object in the constructor, and we'll have a separate delegate that performs the work:

Creating the Progress object

In the constructor, we create the "Progress<int>" and pass in the "ReportProgress" method as a parameter. Then we assign the result to our class-level field.

The signature for our delegate (ReportProgress) is important. Notice that it returns void and takes and integer as a parameter. The parameter has to match the generic type parameter of the Progress object.

Then we can use that parameter value to update our UI. In this case, if it's not zero, we put "Countdown to Data" in our text block. If it is zero, then we clear out the text block.

Using the Progress Object in the Calling Code
The last step is to use the progress object in our calling code. For this, we simply need to pass our "progress" field as a parameter to the "Get" method.

Using Progress with Task
Here's the updated code from "FetchWithTaskButton_Click" where we are using Task directly:

Updated "Get" Call

When we call the "Get" method, we just pass in our "progress" object (in addition to the cancellation token that was already there).

With this code in place, we get progress reporting in our UI. Again, this is a pretty simple example, but it is not difficult to do.


Using Progress with Await
When we're using "await" (rather than interacting with Task directly), we can do progress reporting the same way. All we need to do is add the "progress" parameter to the "Get" call. This is from the "FetchWithAwaitButton_Click" method:

Updated "Get" Call

With this in place, we get the same progress reporting when we use "await".

One Last Update
There is one other update to this code: when we report a progress of "0", then the text block is completely cleared out. In order to accomplish this, we call "Report(0)" just before returning the result set from our asynchronous method:


This way, when our data has returned, our UI will not show progress (since it's done):

No Progress Showing (Completed)

There's More
This shows us some very simple progress reporting. In the next article, we'll look at sending back some more information. For example, if we're processing a bunch of records in the asynchronous process, we might want to let the caller know which record we're on and how many are left to process. We can easily do this by creating a custom type to use as our progress payload.

And there are also some subtleties about where the progress delegate runs. Since we're interacting with UI elements, we want to make sure that our delegate runs on the UI thread. That works with the way we have things here, but if we create things a little differently, we can get into trouble. For more information, check out this article: Pay Attention to Where You Create Progress Objects.

Wrap Up
The basics of progress reporting when using Task aren't that complex. Most of the time, we're trying to figure out whether we can actually report progress (it's hard to report progress on a database or service call).

With the IProgress<T> interface along with the built in Progress type, we can get progress reporting up and running pretty quickly. Next time, we'll look at a more complex payload. Until then...

Happy Coding!

No comments:

Post a Comment