Introduction
This tip shows how to simplify your code for progress reports and busy indication in a MVVM project.
The first part is about handling busy indication for multiple concurrent tasks.The second extends the solution to support progress reports. At the end, there will be some code to report status from LINQ-Queries.5
Background
In my current project, multiple concurrent actions activate the BusyIndicator
. Therefore a simple bool was not enough and a more powerful implementation was needed. In this scenario, I ran into the problem, that each action had to be wrapped in a try
-finally
-block or to ensure, that the busy indication gets stopped correctly. In the end, there was a lot of overhead in the code just for ensuring that the BusyIndicator
works correctly.
After finding a solution for this, I decided to add progress report using the same pattern as for normal busy indication. There were also some LINQ-Queries that took a while to process. I also wanted them to report the current progress. So I came up with some ideas that solve the problems in my scenario.
Using the Code
First let's take a look at the BusyHandler
that manages multiple concurrent progresses. I decided to create a method "Activate()
" that returns an IDisposable
. This has the advantage, that the code for doing the work can be encapsulated in an using
-block. The BusyHandler
has a nested class, that implements the IDisposable
interface. In the constructor, an internal counter of the BusyHandler
gets incremented and Dispose
decrements the counter in the end. The BusyHandler
has a property "IsBusy
" which notifies if the status changes and can be bound directly in XAML. Using the BusyIndicator
is straight forward:
private async void DoSomething()
{
using (BusyHandler.Activate())
{
await Task.Delay(3000);
}
}
As you can see, the usage of it is really simple and the busy progress gets stopped in any case. Now, it is possible to handle exceptions in a different place, without catching them, deactivating the busy progress and rethrowing them again.
The next step is to report progress. For this, the .NET-Framework has the interface IProgress<T>
. I have created a new interface that implements IProgress<T>
and IDisposable
to support progress report in the same fashion as the simple busy indication. For this scenario, a counter is not enough, so I decided to create a dictionary that holds the status of the progress for all of the currently running tasks. A second nested class implements my new IDisposableProgress
interface. In the constructor, it registers its progress in the internal Dictionary
of the BusyHandler
. The "Report
"-method from the IProgress
-interface updates the current status of the progress and the dispose
method marks the item as finished. The BusyHandler
has a property "Progress
" which sums up the individual values and divides it by the number of items in the dictionary.
Removing the entries of a progress in the dictionary in the dispose
method made the ProgressBar
jump back to a lower value when a progress is finished and others are still running. The solution for getting a normal behavior is to remove the items only when all of them are finished or when another item is added.
Now, we have a BusyHandler
that is easy to use and that supports progress reports:
private async void DoSomethingWithProgressReport()
{
using (var busyHandler = BusyHandler.ActivateWithProgress())
{
for (int i = 0; i < 1000; i++)
{
busyHandler.Report(i/10.0);
await Task.Delay(10);
}
}
}
After creating the infrastructure for progress reports, let's enable LINQ to report progress. I assume you know that LINQ works with ExtensionMethods
that are set on top of the IEnumerable
interface. The only thing we have to do to get a progress report is to create another ExtensionMethod
that reports the progress while enumerating the source collection. The following snippet shows how that can be done:
public static IEnumerable<T> WithProgressReport<T>
(this IEnumerable<T> source, IProgress<double> progressHandler,
int itemCount, double reportInterval)
{
var reportingCountInterval = (int)(itemCount / (100/reportInterval));
var counter = 0;
foreach (var item in source)
{
yield return item;
counter++;
if (counter % reportingCountInterval == 0)
{
progressHandler.Report(counter * 100.0 / itemCount);
}
}
}
As you can see, we need the number of items and the interval for sending the progress report. After processing an item, we update the progress if needed. If the source collection is an array or an ICollection<T>
, the number of items is known. With this, we can create overloads which are much nicer to use. For example:
public static IEnumerable<T> WithProgressReport<T>(this ICollection<T>source,
IProgress<double> progressHandler,double reportInterval = 0.1)
{
return source.WithProgressReport(progressHandler, source.Count, reportInterval);
}
Using it is again quite simple:
private async void ActicateBusyWithProgressWithLinq()
{
using (var busyHandler = BusyHandler.ActivateWithProgress())
{
await Task.Run(() =>
{
var result =
Enumerable.Range(0, 1000)
.ToList()
.WithProgressReport(busyHandler)
.Select(DoHeavyProcessing)
.ToList();
});
}
}
Now we have a BusyHandler
that supports busy indication and progress report and we have an Extension to enable progress report within LINQ-queries. For me in the beginning, it all looked nice and smart but there are still some issues left. First of all, the progress report for LINQ only works for one iteration of the IEnumerable
. If you iterate the IEnumerable
that is returned from "WithProgressReport
", the progress gets reported from the beginning over and over again. So if you use this implementation, please enumerate the IEnumerables
only once after using "ReportWithProgress
". My second concern is that I'm pretty sure that IDisposable
was not made for using it like this, but until now for me it works fine and removed a lot of boilerplate code.