Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF
Article

Create MVVM Background Tasks with Progress Reporting

Rate me:
Please Sign up or sign in to vote.
4.93/5 (34 votes)
9 Sep 2011CPOL18 min read 93K   6.1K   85   14
This article explains how to implement background processing and parallel processing for time consuming tasks in MVVM, and how to create a progress dialog with cancellation for those tasks.

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:

C#
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    // Set up the main window and its view model
    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:

C#
// 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:

XML
<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:

C#
private void Initialize()
{
    // Initialize command properties
    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:

C#
// Initialize 
...
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:

C#
// Set the maximum progress value
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:

C#
// Announce that work is starting
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:

C#
#region Events

public event EventHandler WorkStarted;
public event 
EventHandler WorkEnded;

#endregion

...

#region Event Invokers

internal void RaiseWorkStartedEvent()
{
    // Exit if no subscribers
    if (WorkStarted == null) return;

    // Raise event
    WorkStarted(this, new EventArgs());
}

internal void RaiseWorkEndedEvent()
{
    // Exit if no subscribers
    if (WorkEnded == null) return;

    // Raise event
    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:

C#
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:

C#
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:

C#
// Launch first background task
var taskOne = Task.Factory.StartNew(() => ServiceOne.DoWork(workList, progressDialogViewModel));

// Launch second background task
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:

C#
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:

C#
// Process work items in parallel
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:

C#
// Increment progress counter
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:

C#
// Cancel all pending background tasks
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:

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Foresight Systems
United States United States
David Veeneman is a financial planner and software developer. He is the author of "The Fortune in Your Future" (McGraw-Hill 1998). His company, Foresight Systems, develops planning and financial software.

Comments and Discussions

 
QuestionOnDataContextChanged Pin
Mike Heggie19-Jul-15 13:21
Mike Heggie19-Jul-15 13:21 
QuestionWhen does the task/thread check for OperationCanceledException Pin
Member 107346794-Feb-15 7:43
Member 107346794-Feb-15 7:43 
QuestionAwesome article! Pin
Stepan Markakov20-Nov-14 3:09
Stepan Markakov20-Nov-14 3:09 
QuestionIf I want run the two tasks concurrently, how? Pin
zhshqzyc22-Sep-14 10:29
zhshqzyc22-Sep-14 10:29 
GeneralMy vote of 5 Pin
fredatcodeproject29-Apr-14 10:01
professionalfredatcodeproject29-Apr-14 10:01 
QuestionHow to provide a dialog in the service? Pin
Supermagle11-Sep-13 0:06
Supermagle11-Sep-13 0:06 
GeneralMy vote of 5 Pin
Member 24867431-Nov-12 1:10
Member 24867431-Nov-12 1:10 
GeneralMy vote of 5 Pin
jonnyboy824-Oct-12 15:56
jonnyboy824-Oct-12 15:56 
QuestionStarting a new Task vs using the Dispatcher. Pin
mwccl12-Jun-12 17:11
mwccl12-Jun-12 17:11 
AnswerRe: Starting a new Task vs using the Dispatcher. Pin
Wayn0r10-Jun-15 7:50
Wayn0r10-Jun-15 7:50 
GeneralMy vote of 5 Pin
geoffhirst27-Jan-12 3:08
geoffhirst27-Jan-12 3:08 
GeneralMy vote of 5 Pin
Philippe Devaux9-Oct-11 21:06
Philippe Devaux9-Oct-11 21:06 
QuestionPrisim Pin
Bob Ranck14-Sep-11 2:57
Bob Ranck14-Sep-11 2:57 
AnswerRe: Prisim Pin
David Veeneman14-Sep-11 4:02
David Veeneman14-Sep-11 4:02 
Sure--I can't think of any reason why it can't be incorporated into a Prism module.
David Veeneman
www.veeneman.com

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.