Introduction
This article explains how to execute background tasks in a WPF application, complete with progress reporting. The techniques explained below
can be applied to any time-consuming task that involves repetitive processing of items in a list. For example, one might need to perform the same processing
on each file in a specified folder, or print a lengthy collection of documents.
There aren't many articles available that explain background and parallel processing in the context of an MVVM application,
and that's what this article is intended to do. The background and parallel processing is based on the Task Parallel Library (TPL) introduced
with .NET 4.0, which greatly simplified both types of processing in .NET applications.
The demo app for this article is built around the MVVM pattern, and the article demonstrates my approach to structuring MVVM apps, including the use of
command and service classes. If you aren’t familiar with MVVM, or if my implementation of MVVM isn’t familiar to you, I recommend reading
MVVM and the WPF DataGrid, which discusses my general approach in more detail.
Background
Like a lot of people, I avoided .NET background processing as long as I could. It seemed difficult and arcane, and as long as I could get
by without it, I would. Then I began working on an app that had to process 1,000 files at a time. As I delved into that app, it became very clear that the app
would have to do its heavy lifting in the background. Moreover, with the prevalence of multi-core processors, there was no way to justify writing the app
without parallel processing support.
What I was writing was an app to add a visible time stamp to time-lapse photography images. Time-stamping is a very time-consuming operation,
particularly when working with 1,000 images at a time. The application would obviously benefit from background and parallel processing, and it clearly needed
a progress dialog that could cancel processing on request. So, I took two Aspirin and started figuring out how to add all those
features to what I had thought would be a simple program.
As I was researching the issue online, I discovered the Task Parallel Library (TPL) in .NET 4. It is a truly amazing library, but I couldn't find an
end-to-end tutorial on using the library, particularly in an MVVM context. The remainder of this article
demonstrates the solution that I developed for the problem, in the context of the demo project attached to this article.
The Demo Project
The demo project is organized as an MVVM app. Each view element in the application (the main window and the Progress dialog) has its own View Model.
Unlike some other MVVM apps, the View doesn’t create its View Model, nor does the View Model create its View. Instead, the View and View Model are both
created by a coordinator that is superior to both objects. For example, the main window is owned by the application, so the main window and its View Model are
both created by the App
object, in an override to the OnStartup()
event handler:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var mainWindow = new MainWindow();
var mainWindowViewModel = new MainWindowViewModel();
mainWindow.DataContext = mainWindowViewModel;
mainWindow.Show();
}
The demo’s work is performed by operational service classes (in the Operations > Services folder of the .NET solution). The demo project does not
perform any real work. Instead, we use the .NET Thread.Sleep()
method to simulate a time consuming task:
Thread.Sleep(300);
The demo app is based on a production application I wrote recently to add time stamps to time-lapse photographs. The processing in that application
involved two steps:
- Apply a time stamp in each photograph in the time-lapse series; and
- Copy the ‘Created’ and ‘Last Modified’ file properties from the original time lapse photos to the copies with the time stamps.
In my production app, the first step takes much longer to complete than the second step, creating some additional challenges for progress tracking. I have
preserved this imbalance in the demo app by having the first background task call Thread.Sleep()
for 300 milliseconds for each work item, and by having the
second background task call the method for 100 milliseconds for each work item. We will see below how to compensate for this imbalance so that the progress bar
and percentage increment smoothly through both processes.
The demo app’s main window is very simple—it contains only a ‘Do Demo Work’ button. This button is bound to a DoDemoWork
command in the
MainWindowViewModel
:
<Button Content="Do Demo Work" Command="{Binding DoDemoWork}" ... />
The View Model’s DoDemoWork
command is initialized in the View Model’s Initialize()
method (called from the View Model constructor)
as an ICommand
object:
private void Initialize()
{
this.DoDemoWork = new DoDemoWorkCommand(this);
...
}
Note that the DoDemoWorkCommand
object receives a reference to the View Model via constructor injection. Encapsulating commands in separate ICommand
objects gets code out of the View Model. In MVVM apps, commands often act as bridges between the View Model and an app’s business layer (the model and services provided by the app).
In my MVVM apps, commands generally act as coordinators; they delegate most of the heavy lifting to separate service classes. That keeps the command classes lighter in weight and focused
on their job of encapsulating an application command.
The DoDemoWorkCommand
(located in the Operations > Commands folder of the application) derives from ICommand
, so most of its code is found in an
Execute()
method. Let's take a look at each of the steps performed by the command.
Step 1: Initialize the Work List
The command initializes a work list. Normally, this work list is driven by the business needs of the application. My image-processing app processes all
files in a specified folder, so the work list is a list of file paths for the images to be processed. Each folder typically contains 999 time-lapse images.
To simulate a problem with the same scope, the DoDemoWork
command generates a simple list of 999 integers for its work list:
...
var workList = Enumerable.Range(0, 999).ToArray();
Step 2: Create a Cancellation Token Source
The Progress dialog contains a Cancel button which can be used to cancel an operation in progress. To do that, the app will need a
System.Threading.CancellationTokenSource
object. We will discuss this object in more detail below. Both of the operational service classes and the Cancel
command will need this token source, so the command creates one and passes it to the Progress dialog View Model.
Step 3: Set the ProgressMax Value
The command sets the maximum progress value for the operation it controls. In this case, the operation consists of two tasks, the first of which takes three
times as long to complete as the second one. To keep the progress bar flowing smoothly, we will need to advance the progress counter three ‘clicks’ for every
item processed in the first task, and one click for each item processed in the second task. The result will be that the progress bar will show 75% completion
after the first task completes, and 100% completion after the second task. To make that work, we have to set the maximum progress value for the overall
operation to four times the length of the work list:
progressDialogViewModel.ProgressMax = workList.Length * 4;
In the demo app, we know that the first task takes three times as long as the second task because we set the tasks up that way. In a production
app, you will need to experiment with the ‘clicks’ settings to get the combination that provides the smoothest flow for your progress bar.
The ProgressMax
value will always be set to a multiple of the work list length; the multiplier will be equal to the aggregate number of clicks assigned to all
background tasks. In the demo app, the multiplier is four; three for the first task, and one for the second.
Step 4: Announce that Work is Starting
This fourth step is where my implementation of MVVM differs from some others. We all agree that MVVM works best when code is moved from the code-behind to the View
Model. However, some developers follow a rule that an MVVM window should have no code-behind at all. Other developers follow a rule that says the View should
have no code at all, even in separate view service classes. I don’t follow either rule, because I think they violate the Separation of Concerns principle.
Here is why: The View is generally responsible for managing interaction with the user, and the presentation of dialogs, such as a Progress dialog, is one of
those concerns. Thus the View needs to own a Progress dialog, and it should have exclusive control over when it is opened and closed. It will invariably be
necessary to add some code to the View to manage opening and closing the dialog.
So, rather than follow a “no code in the View” rule, I follow a rule of reason: Code that opens and closes the dialog goes in the View; all other code relating
to the state and behavior of the dialog go in the dialog’s View Model. In the demo app, that’s the ProgressDialogViewModel
.
Note that the Progress dialog is instantiated by its owner, the main window, and that the Progress dialog View Model is instantiated by its owner, the main window View Model (in its
Initialize()
method). When the main window instantiates a Progress dialog, it gets the dialog’s View Model from the main window View Model and attaches the
progress dialog to the dialog’s View Model.
Note the direction in which the dependencies run in this structure:
The View contains a reference to its View Model, which it receives when its DataContext
property is set. That reference makes the View dependent on the View
Model—we can’t change the View Model without changing the View. However, the View Model does not contain a reference to the View that is using it. That means
the View Model has no dependency on a particular View, so we can change the View, or completely swap it out for a different View without having to open up
the View Model and recompiling it, so long as the new or modified View complied with the API embodied in the View Model.
That is the central rule around which I organize MVVM apps: The View Model must be completely ignorant of the View that it supports. That approach makes
the UI layer completely independent of the other layers of the application, which provides a very clean separation of layers that make apps easier to test
and maintain. I can replace the View with a set of unit tests, and the View Model is none the wiser for it.
That’s all very well and good, but it does create a thorny problem. If the main window owns the dialog and controls opening and closing the dialog, how
does the DoDemoWork
command tell the View to display the dialog? The command, like the main window View Model, has no knowledge of the particular View that
the application uses. In other words, the DoDemoWork
command doesn’t have a reference to the main window, so it can’t call the window to display the dialog.
The answer is that the command doesn’t call the main window. The ability to do so would create a dependency of the View Model on the View, destroying the
clean separation of layers that MVVM is designed to promote. So instead, the command simply calls a View Model method to announce to the application-at-large
that a time-consuming task has begun. Later, it calls a corresponding method to raise an event announcing that the time-consuming work has ended. Here is the
command code that announces that work has begun:
m_ViewModel.RaiseWorkStartedEvent();
The code that announces the completion of the time-consuming work is a little different, and I will discuss it below.
Here is the event declaration in the View Model, and the methods that raise the WorkStarted
and WorkEnded
events:
#region Events
public event EventHandler WorkStarted;
public event
EventHandler WorkEnded;
#endregion
...
#region Event Invokers
internal void RaiseWorkStartedEvent()
{
if (WorkStarted == null) return;
WorkStarted(this, new EventArgs());
}
internal void RaiseWorkEndedEvent()
{
if (WorkEnded == null) return;
WorkEnded(this, new EventArgs());
}
#endregion
The main window subscribes to these events in an event handler that fires when the MainWindowViewModel
is set as the window’s data context:
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var viewModel = (MainWindowViewModel)e.NewValue;
viewModel.WorkStarted += OnWorkStarting;
viewModel.WorkEnded += OnWorkEnding;
}
This event subscription code is located in the main window’s code-behind, although it could have been placed in a view service class. Since it is
directly related to opening and closing the Progress dialog, it is consistent with MVVM design principles.
As a result of the event subscriptions, when the main window is attached to its View Model, it immediately subscribes to the View Model events that will notify it when a time-consuming
task starts and ends. The actual work of opening and closing the Progress dialog is performed by the event handlers referenced in the event subscriptions above:
void OnWorkEnding(object sender, EventArgs e)
{
ViewServices.CloseProgressDialog();
}
void OnWorkStarting(object sender, EventArgs e)
{
var mainWindowViewModel = (MainWindowViewModel)this.DataContext;
var progressDialogViewModel = mainWindowViewModel.ProgressDialogViewModel;
ViewServices.ShowProgressDialog(this, progressDialogViewModel);
}
When work begins, the command invokes the WorkStarted
event, which causes the main window to instantiate the Progress dialog and attach it to the
dialog’s View Model. When work ends, the command invokes the WorkEnded
event, which causes the main window to close the dialog.
Step 5: Execute the Background Tasks
Let’s go back to the DoDemoWork
command. Its next step is to execute the background tasks at the heart of the application. Here is the command code to perform these tasks:
var taskOne = Task.Factory.StartNew(() => ServiceOne.DoWork(workList, progressDialogViewModel));
var taskTwo = taskOne.ContinueWith(t => ServiceTwo.DoWork(workList, progressDialogViewModel));
The command uses the .NET 4.0 Task Parallel Library to execute these background tasks. As we will see shortly, the TPL contains powerful
methods that dramatically simplify the problem of partitioning repetitive tasks for distribution among the cores of multiple-core processors.
But the TPL also contains other methods that also simplify the chore of executing tasks on background threads.
The TPL contains a Task
object that contains factory methods for generating new background tasks. In the context of TPL, a Task
is
essentially a wrapper around a method that allows the method to be performed on a background thread. In the call to the first background task above,
we call Task.Factory.StartNew()
, passing it an Action
delegate in the form of a lambda expression. The delegate for the first task is the DoWork()
method
of an operational service class, ServiceOne
. The method takes the command’s work list, and a reference to the progress dialog View Model. We will look at the service method
in more detail shortly. For now, note that the first Task
is assigned to a variable named taskOne
.
My image-processing application initially threw access-violation exceptions that could only be prevented by adopting a two-pass processing on
the work list. The second pass could begin only when the first pass was complete. In other words, I had to chain two background tasks together sequentially.
The Task.ContinueWith()
method provides that functionality. I create a second Task
object, named taskTwo
, by calling
ContinueWith()
on taskOne
. I pass an Action
delegate to taskTwo
, the delegate is the DoWork()
method
of my second operational service class, ServiceTwo
. As a result, taskTwo
will begin immediately after taskOne
finishes.
Step 6: Announce that Work is Complete
The final step performed by the DoDemoWork
command should be straightforward at this point—it announces to the application-at-large that
the time-consuming work begun is complete. As we saw above, this announcement will cause the main window to close the Progress dialog.
The announcement that work is complete cannot be made until the second background task is complete. That means we have a third background task that
is chained to taskTwo
, in the same way that taskTwo
was chained to taskOne
:
taskTwo.ContinueWith(t => m_ViewModel.RaiseWorkEndedEvent(),
TaskScheduler.FromCurrentSynchronizationContext());
Note that we use a different overload of ContinueWith()
, one that takes a TaskScheduler
object as an argument. The Action
delegate we pass
to ContinueWith()
is a View Model method, which runs on the main thread. Since we are on a background thread, we can’t call the method directly. Passing
TaskScheduler.FromCurrentSynchronizationContext()
causes ContinueWith()
to pass the call to the application Dispatcher
object,
which places the call in its message queue. It is much the same as calling Dispatcher.Invoke()
from a background thread.
The Service Classes
The two operational service classes in the solution’s Operations > Services folder are more or less identical, so I will discuss them together.
In a production application, these classes would not be identical, since they would provide different services. For example, in my image-processing app, the first service adds time-stamps
to the photos in the work list, and the second service copies the ‘Created’ and ‘Last Modified’ file properties from original photos to their corresponding
time-stamped copies. Nonetheless, both service classes follow a general pattern of exposing a public service method to manage the overall task, and
a private method to handle the processing of each individual item. Since the demo app tries to simulate production
conditions as closely as possible, I set up two service classes, each of which takes a different amount of time to process an item, much as in my image-processing app.
Each service class exposes a DoWork()
method, which sets up a processing pass on the work list. DoWork()
uses the
System.Threading.Tasks.Parallel.ForEach()
method to partition the work list and distribute work items among all available cores of the local processor.
First, the method gets the CancellationTokenSource
from the Progress dialog View Model and uses the token source to generate a cancellation
token. It wraps this token in a ParallelOptions
object. Then it calls Parallel.ForEach()
, passing the method the work list, the parallel options
object, and an Action
delegate. .NET takes care of all the work involved in partitioning the work list and distributing it among available cores in an efficient manner:
try
{
Parallel.ForEach(workList, loopOptions, t => ProcessWorkItem(viewModel));
}
catch (OperationCanceledException)
{
Pvar ShowCancellationMessage = new Action(viewModel.ShowCancellationMessage);
PApplication.Current.Dispatcher.Invoke(DispatcherPriority.Normal, ShowCancellationMessage);
}
Note that the Parallel.ForEach()
call is wrapped in a try-catch
block. If this operation is cancelled, Parallel.ForEach()
will throw an
OperationCanceledException
. We trap that exception here and use it to display a cancellation message. I will discuss the message further below.
In this case, the Action
delegate passed to Parallel.ForEach()
is the ProcessWorkItem()
method in the same service class. That method performs two
steps. First, it does whatever work is involved. In my image-processing app, that involves either time-stamping an image, or copying file properties. In the demo app,
it simply involves sleeping for either 300 (ServiceOne
class) or 100 (ServiceTwo
class) milliseconds.
After a work item is processed, the ProcessWorkItem()
method advances the progress counter. The Progress dialog View Model contains a method called
IncrementProgressCounter()
, which takes an integer argument. The integer specifies how many clicks the counter should be advanced. The ServiceOne
class advances the counter three clicks for every item, while the ServiceTwo
class advances it one click for every item.
Since the service class methods are running on a background thread, they can’t call a View Model method (which runs on the main thread) directly.
We saw this above, in the last ContinueWith()
call. In this case, we use the Dispatcher.Invoke()
approach to call the View Model from our background task:
var IncrementProgressCounter =
new Action<int>(viewModel.IncrementProgressCounter);
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal,
IncrementProgressCounter, 3);
We create an Action
delegate that points to the View Model method we want to call. Then, we pass that delegate to the application Dispatcher
object,
which controls the application’s main thread. The Dispatcher
adds the call to its message queue, so that it gets executed in due course.
Displaying Progress
The Progress dialog displays progress as a percentage, and as a progress bar:
The progress display is managed by three properties in the Progress dialog View Model: Progress
, ProgressMax
, and
ProgressMessage
. The IncrementProgressCounter()
method works with these properties to advance the display as work items are completed.
The IncrementProgressCounter()
method adds a specified number of ‘clicks’ to the Progress
property when it is called.
The number of clicks is determined by the caller. As we saw above, ServiceOne
advances the counter three clicks for each item,
and ServiceTwo
advances the counter one click per item. After advancing the counter, the method updates the progress message for the new count and
stores the result in the ProgressMessage
property. These View Model property changes are immediately reflected in the Progress dialog.
Cancelling an Operation in Progress
The Progress dialog’s Cancel button is bound to a Cancel
command in the Progress dialog View Model. Recall that the DoDemoWork
command created a CancellationTokenSource
object and passed it to the Progress dialog View Model. The two operational service classes used
these token sources to generate cancellation tokens that they wrapped in ParallelOptions
objects and passed to their respective
Parallel.ForEach()
methods. When the user clicks the Cancel button on the Progress dialog, we will put those cancellation tokens to work.
One of the really amazing things about the TPL is how easy it makes it to cancel an operation in progress. One simply calls Cancel()
on
the token source, and .NET takes care of cancelling any task that has been provided with a cancellation token from the same source. So, here is all that the Cancel command needs to do:
m_ViewModel.TokenSource.Cancel();
Once Cancel()
has been called on the token source, all tokens generated from that source are cancelled, and any Parallel.ForEach()
loops that have been
provided with those tokens will throw an OperationCanceledException
. As I noted above, the operational service classes trap this exception and use it to post
a cancellation message to the Progress dialog View Model:
var ShowCancellationMessage = new Action(viewModel.ShowCancellationMessage);
Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, ShowCancellationMessage);
We invoke the View Model’s ShowCancellationMessage()
method the same way we invoked the IncrementProgressCounter()
method—by declaring an Action
delegate and passing it to the application Dispatcher
object.
Conclusion
Hopefully, this rather lengthy tour through the demo project will help you structure your own MVVM apps that perform time-consuming tasks without
freezing the UI. The techniques used in the demo app can be adapted to tasks as diverse as printing multiple documents or batch-processing a thousand images.
As always, I am open to feedback and suggestions as to how to improve this article. One of the benefits of publishing on CodeProject is the
quality of the peer review that it generates. I plan to update this article from time-to-time, and I will, of course, credit the author of any
suggestion incorporated into an update. Please leave any general questions, comments, or suggestions in the Comments and Discussions section below this article.
History
- 2011/09/08: Initial version completed.