When dealing with Tasks, where we create our Progress<T> objects affects where our progress handlers run (or don't run).
Let's jump into a sample. Our application is pretty simple (and you can find the code on GitHub: jeremybytes/progress-task). Here's the UI:
What we're doing is starting a new Task and then reporting progress back to our UI. We've got our code in the code-behind to see how the Progress<T> object reacts. This isn't ideal, but it's what we have for small apps, and it will let us see where we can run into issues.
We've got a couple of methods that we want to use (these are in MainWindow.xaml.cs along with all the other code):
The "StartLongProcess" method is what we'll put into our Task. This is pretty simple. We have a "for" loop that goes from 0 to 50. Then inside the loop, we send back the value of our indexer through the progress object. Finally, we Sleep for 100 milliseconds to add some delay to our process. We'll pretend that that "Sleep(100)" is actually processing data or some other operation like that.
When "progress.Report" is called, it runs the "UpdateProgress" method (we'll see how these get associated in just a bit). The parameter "message" is the value that is passed in to the "Report" method above. Since we have a "Progress<string>", our parameter will be a string type.
So ultimately, we'll take the value of the indexer from our "for" loop and put it into a text block in our UI.
Progress<T> in "Bad Progress"
We'll start by looking at the code we have on our "Bad Progress" button:
This creates and starts a new Task by using "Task.Run" (for more information see "Task.Run() vs. Task.Factory.StartNew()").
We create a task that will execute the "StartLongProcess" method. This takes an "IProgress<string>" as a parameter (as we saw above). We use "new Progress<string>" to create the progress object. "Progress<string>" implements the "IProgress<string>" interface, so we can pass it to this method as a parameter.
The Progress constructor take a delegate as a parameter. This is the method that will be called when we "Report" progress. Notice that this is the "UpdateProgress" method from above. This is how our progress object gets associated with the progress handler.
Let's run this code to see what happens. If we click the "Bad Progress" button, we get the following message:
As we can see, we're getting an "InvalidOperationException". The additional information tells us what we need to know: "The calling thread cannot access this object because a different thread owns it."
This means that our "UpdateProgress" method is not running on the UI thread. When we try to directly interact with UI elements from a non-UI thread, we get this error.
We can fix this by marshalling this call back to the UI thread. But there's actually an easier way to get this code to work.
Progress<T> in "Good Progress"
What's the easier way? Let's look at our "Good Progress" button for a clue:
The difference here is subtle. We're still calling "StartLongProcess" inside our Task, and we're still creating a "Progress<string>" object that points to our "UpdateProgress" method.
But as we'll see, our results are completely different when we click the "Good Progress" button:
This code actually works as expected! We can see our text block has the value of "22" (I grabbed a screenshot in the middle of the process).
This calls the exact same "UpdateProgress" method, but it has no trouble interacting with the UI elements. Let's focus on the differences between our 2 methods.
Where We Create Progress<T> Matters
Here are both methods together:
What we need to pay attention to is where we create the "Progress<string>" object in each case.
In the "BadProgress" method, we create the progress object inside of our Task, i.e. we're creating the object inside the lambda expression that we have to define our Task. Since we're creating the progress object on our Task thread, the progress handler ("UpdateProgress") will be run on this thread -- not on the UI thread.
In the "GoodProgress" method, we create the progress object outside of our Task -- before we even create the Task. Since we're creating the progress object on our UI thread, the progress handler will also run on that thread. This is why we can safely interact with our UI elements from the "UpdateProgress" method here.
As a side note, our "progress" variable is captured by the lambda expression. Captured variables are pretty cool. You can learn more about them here: Video - Lambda Expression Basics.
What we've seen is that it's important to pay attention to where we create our Progress<T> objects. This is a subtle difference, but it can cause unexpected problems if we're not looking out for it.
There are other ways that we can get this code to work. For example, we can get a reference to the Dispatcher of our UI thread and then use "Dispatcher.Invoke" in the "UpdateProgress" method to marshal things back to the UI thread. This will let us interact with the UI elements as we expect.
Another option is to move this code out of the code-behind of our form and into a separate view model class. In this scenario, our "UpdateProgress" method could update a property in the view model rather than updating the UI directly. This would eliminate the direct interaction with the UI elements -- that would all be handled through data binding.
These options have their pros and cons (and special quirks that we need to be aware of). It's always good to think about other ways of accomplishing a task so that we can pick the one that fits our needs.
I came across this particular issue when I was helping another developer troubleshoot some code. In this code, there were 2 different Tasks that were created a little bit differently. One of them worked, and one of them didn't. On closer examination, we found that the progress objects were being created on different threads (just like our sample code here).
I will be exploring progress reporting in asynchronous methods a bit more in the future. In the meantime, be sure to take a look at the basics of Task, Await and Asynchronous Methods.
When dealing with multiple threads, it's common to run across issues just like this one. Task and Await make some things easier, but we still need to understand some of the underlying functionality so that we can use these appropriately -- and hopefully not be surprised when something strange crops up.