A few words about the BackgroundWorker component
This article is not designed to teach you how to use the BackgroundWorker component. You can get an overview of using the component here: Keep Your UI Responsive with the BackgroundWorker Component. From this link, you can get a walkthrough on using the component as well as the sample code that is used in the walkthrough.
Regardless of whether you are familiar with the BackgroundWorker component, I would highly recommend reviewing the walkthrough and sample code since our MVVM application is based on the original.
The application itself is fairly simple. We have an input text box ("Iterations") that allows us to enter a number. When we click the Start button, the BackgroundWorker fires off a simulated long-running process (which uses the infamously slow "Sleep()" method). The application also has a progress bar and output text box that report the progress of the process. The Cancel button allows us to cancel the process before it has completed. The Start and Cancel buttons are enabled or disabled depending on the state of the running process.
A few words about MVVM
This article is not designed to teach you how to implement the MVVM pattern. We will be looking at *an* implementation of MVVM, but I'm not promoting this as the "best" or "right" implementation (that's a discussion for the big brains who work with this pattern all the time). I have deliberately kept my implementation simple so that we can focus on the pattern components and how the BackgroundWorker fits in. As such, I am not using any type of MVVM framework (such as Prism, Caliburn, or the MVVM Lite Toolkit).
For an overview of the pattern itself, you can refer to a previous article: Overview of the MVVM Design Pattern. For this example, I will be taking an approach similar to Paul Sheriff. He has several MVVM articles floating around; here is one published in CoDe magazine: MVVM Made Simple.
One thing that keeps this implementation simple is the use of event handlers in the View over using commanding. If we were to use commanding, we would need quite a bit more code. The frameworks all provide a messaging system that makes this additional code much easier to implement. Using event handlers does not "break" the pattern -- as we will see, we will still maintain good separation between our View and ViewModel.
The source code for the sample application can be downloaded here: BackgroundWorker w/ MVVM. The solution contains 2 projects: the original sample ("BackgroundWorkerInWPF") and the MVVM sample ("BackgroundWorker-MVVM"). This allows you to compare the implementations. Other than moving some code around to fit the MVVM pattern, the core of the projects are very similar.
All of our business logic should be in the Model -- in our case, that means our long-running process. The ProcessModel.cs file contains our custom Model: a class that implements IEnumerable<int>. The idea behind this is that we can use this class in a foreach loop. The Sleep() method is used to add some time to the iteration.
Because the model implements IEnumerable, we can write code similar to this:
The value that is returned by the iteration is an integer between 1 and the total number of iterations (set in the constructor or through a separate Iterations property). Since there is a Sleep(100) as part of each iteration, it would take a total of 10 seconds to process 100 iterations. We definitely would not want our application to lock up for those 10 seconds.
The ViewModel is where we will find the bulk of our code. If we were to compare to our original application, all of the code that was in the code-behind (MainPage.xaml.cs) is now in the ViewModel (ProcessViewModel.cs); the exception is the "business logic" that was moved to the Model.
Let's step through the ViewModel so that we can see all of the pieces; these are all in the ProcessViewModel.cs file. First up are the fields.
Next we have a number of properties. As a reminder on the MVVM pattern, we should never be updating UI controls directly. Instead, the ViewModel provides a number of properties that are then data bound to the controls in the View. We'll see the difference more clearly when we look at a couple of the methods below. We have 5 properties that will be used for the View: Iterations, ProgressPercentage, Output, StartEnabled, and CancelEnabled. Here is the Iterations property:
The other 4 properties follow this same pattern. Notice that the property setter calls the OnPropertyChanged method. This is because our ViewModel implements the INotifyPropertyChanged interface. Here is the implementation of the INotifyPropertyChanged interface:
By implementing this interface, we ensure that the data binding on our View will operate as expected. The controls will be notified whenever the underlying property is updated.
The ViewModel constructor initializes the BackgroundWorker component and sets the properties and events:
These are exactly the same properties and events that we set in the original sample. In the original, we set these in the XAML (since the BackgroundWorker was a resource on the page). Since the BackgroundWorker has been moved to the ViewModel, we now have to hook these up in code.
StartProcess and CancelProcess
The Start and Cancel methods were button event handlers in our original code. Since we are moving our presentation functionality to the ViewModel, these now become methods in ProcessViewModel:
I'll point out a few things here. First, notice that we are setting properties in the ViewModel (Output, StartEnabled, CancelEnabled) and *not* setting UI controls directly. We are relying on the data binding to get these values to the View.
Next, we are passing the entire Model as a parameter to the RunWorkerAsync() method. This is slightly different from our original (which only passed an integer). The change was made to better separate the concerns. We'll see this more clearly when we look at the DoWork event handler. Note that we have the check to see if the BackgroundWorker is busy (meaning, the background process is already running) before we perform any work. We then check to see if the Model has been instantiated. If not, then we instantiate it with the Iterations property (which is data bound to the UI input box). If the Model already exists, then we will just update its Iterations property.
The CancelProcess code is exactly the same as the original:
The RunWorkerCompleted event handler of the BackgroundWorker component has not changed much from the original. Let's compare the two.
The only difference is that we are no longer directly interacting with the UI objects. Instead of setting "outputBox.Text", we set the "Output" property of the ViewModel. Instead of setting "IsEnabled" of the startButton and cancelButton, we are setting the "StartEnabled" and "CancelEnabled" properties on the ViewModel. Again, we are relying on the data binding to handle the actual update of the UI controls.
Note: I simply removed the MessageBox.Show() as it is not appropriate for the ViewModel to directly interact with the UI. In a more robust application, we would want to have error notification moving through to the View, where the View can decide the most appropriate display for the error. This is something that the messaging/notification systems in the various MVVM frameworks help us with.
The ProgressChanged event handler of the BackgroundWorker component is similarly updated.
Again, rather than interacting with the progress bar and output text box directly, we are setting properties on the ViewModel.
The biggest change is in the DoWork event handler of the BackgroundWorker component. However, this is primarily due to some refactoring work and not simply because we are using the MVVM pattern. Here is the event handler in the ViewModel:
This is really a combination of the "DoWork" and "DoSlowProcess" methods in the original. Above, we pass the Model as a parameter to the RunWorkerAsync method. We can pull this out here from the DoWorkEventArgs. Because our Model implements IEnumerable, we can use a "foreach" loop on the model itself. This replaces the "for" loop that we had in our original.
The contents of the "foreach" loop (from the MVVM sample) and the "for" loop (from the original) are pretty much the same. We check for CancellationPending and WorkerReportsProgress and take appropriate action as necessary. As a reminder, the "Sleep()" step that was in the original "for" loop has been moved into the model. Whenever the "foreach" moves to the next item, the "Sleep()" runs in the model.
That wraps up the code in our ViewModel. If we compare this to the original code, we can see that the BackgroundWorker event handlers are pretty much the same. The biggest difference is that we added the Properties and code to support data binding to our View. But overall, large code sections match up from a functional standpoint.
The last part of our MVVM implementation is the View. This is contained in the MainPage.xaml. There are only a few changes here -- all to support the data binding to the ViewModel.
First, we add the ViewModel to the window resources. This is one way (easy) way to associate a View and a ViewModel. The various frameworks have different ways of handling the mating of the View and ViewModel.
Here we added the namespace for our ViewModel (xmlns:local), and then added the ProcessViewModel to our resources. The effect of this is that the ViewModel is automatically instantiated with our View (XAML). This means that the ViewModel must have a constructor with no parameters (which we do). Again, the MVVM frameworks offer options for instantiating the ViewModel and passing any parameters (if necessary).
We use our ViewModel as the data context for the border which encompasses all of our UI controls:
This makes the properties of the ViewModel available for data binding for all of our controls.
Control Data Binding
As mentioned (several times now), the controls are data bound to appropriate properties in the ViewModel. Here are the text boxes and progress bar:
Input text box (Iterations):
For each of these, we can see that the Text or Value property is data bound to the corresponding property in the ViewModel. The same is true for our buttons:
We have the "IsEnabled" property data bound to the appropriate ViewModel property. This is how we can enable and disable the View by setting properties in the ViewModel.
Finally, we have the code-behind for our View. Notice that our buttons still have Click event handlers. Here is the total code behind in MainWindow.xaml.cs:
You can see that our code is minimal. We have a variable to reference our ViewModel. In the constructor, we populate that variable based on the Window Resource (from the XAML). Then, for the click event handlers, we simply pass the request through to the ViewModel by calling a corresponding method.
So, although we do not have zero code-behind, we have minimal code-behind. And in the process, we have still kept our separation of concerns. The code-behind of the View is not doing any actual work; it is passing the responsibility to the ViewModel.
What we have seen is that the BackgroundWorker component is still useful in an MVVM world. If our MVVM sample were to simply "foreach" through our model without the BackgroundWorker, our UI would lock up while that processing was taking place. But we can still use the BackgroundWorker for exactly what it was designed for: to prevent UI lock-ups.
Our code had to move around a little bit to support the separation of concerns that is described by the MVVM pattern, but our overall code (the moving parts) are essentially the same.
This sample makes it seem like I am a proponent of not using an MVVM framework, but that is not the case. As I mentioned, the frameworks add a lot of functionality and take away the boiler-plate code that you might otherwise write when implementing MVVM -- especially when writing something larger than this trivial one-screen application. What I think is most important is that you understand the MVVM pattern before using any of the frameworks. If you do not have a good handle on the pattern itself, then you may find yourself taking shortcuts that will "break" the pattern. A strong foundation is essential to make sure that you can use your tools appropriately.
Great tutorial, thank you very much! This is more understandable than anything else I read so far.ReplyDelete
One last thing.. let's say I made some calculations in the ProcessModel and some variable changed which I want to show on the GUI.
How does the ProcessModel notify the ProcessViewModel about that change?
Generally, the view model would make calls into the model (so it would initiate activity). The results would come back from the model through a return value, task, or event handler.Delete
If you have changes happening in the model (for example, if the model is monitoring the file system for changes), then you would probably want to have an event that gets fired with the change. The relationship to consider is that the view model can know about the model, but the model should not have any knowledge of the view model. For a bit more description on the relationships, you can take a look at this article: Overview of the MVVM Design Pattern (http://jeremybytes.blogspot.com/2012/04/overview-of-mvvm-design-pattern.html).
Another issue: Where do I have to put the business logic and how to call it? Let's say I have a method called "doStuff()" in the ProcessModel, where do I call this method?Delete
When using MVVM, the View and View Model represent the UI parts of the application. The Model is "the rest of the application". The Model is often split up into separate pieces (when we want to keep our business logic separate from our data access, and things like that). This organization is really up to you and depends on the needs of the application.Delete
Or do I have to create a subclass in ProcessModel which implements the IEnumerator Interface corresponding to ProcessEnum?ReplyDelete
You don't need to have an IEnumerable to use the BackgroundWorker. The primary reason I used an IEnumerable in the example is to show how we can make an iterative action very easy to update progress and cancel. Cancellation gets interesting in different situations -- some processes are more "cancelable" than others. If you'd like to dig a little deeper, you can take a look at my course on Pluralsight: http://www.pluralsight.com/courses/dotnet-backgroundworker-component-introduction (a 10-day free trial is available on their website if you'd like to watch this course).Delete