Introduction
The Model-View-ViewModel pattern, a variant of Model-View-Controller, provides a nice way to develop graphical applications that are testable. Several features in Windows Presentation Foundation, such as data binding and the command architecture, go a long way towards making it possible to follow this pattern. Unfortunately, once you start to follow the pattern in a real application, you start to run into various things that make it very difficult to maintain the strict separation of Model, View and ViewModel. This article implements the basis of a full-featured application, illustrating various techniques that may be used to maintain the strict separation of concerns demanded by the M-V-VM pattern.
Background
I tried to implement this application while following the Behavior-driven Development methodology. I've captured the attempt by including a complete version history in the form of a Bazaar repository. Bazaar is a distributed version control system and, as such, the entire repository was included in the downloadable source archive. You can see every step of development, including all of the various mistakes I made while coding.
Despite the attempt to follow BDD, don't expect production-quality code here. First, I'm new to this methodology and you can be certain that I didn't follow it as well as one should. Second, although the project includes a fairly complete example application, it is only an example. I've focused on the necessary things to illustrate how to implement an application in WPF following M-V-VM, not on what would be necessary to make a production-worthy application. This is a starting place only.
Also, note that the unit-testing code uses a lot of custom code. This was a large enough portion of the effort that I split it out into a separate article: Visual Studio Unit Testing Extensions.
Using the Code
The "core" to a lot of the code in this project can be found in the ViewModel
class. This class is both a MarkupExtension
class, as well as a class providing a few attached properties. In order to associate your ViewModel with your View, you use the ViewModel
class like this:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeProject.Windows.Markup;assembly=CodeProjectCore"
xmlns:vm="clr-namespace:TaskList.ViewModel;assembly=TaskList.ViewModel"
c:ViewModel.Instance="{c:ViewModel {x:Type vm:MainWindowViewModel}}">
Assigning the ViewModel.Instance
attached property does several things:
- Assigns the property to an instance of an object. Note that this property is inherited by children.
- Assigns the
DataContext
property to the same instance.
- Assigns the
ViewModel.Commands
property to the empty string.
That last bullet point is worth explaining further. The ViewModel.Commands
attached property is used to create command bindings on the View for command handlers in the ViewModel. This is actually one of the trickier things to accomplish in a simple and "clean" manner. I've blogged about this on my blog (check out the archives for several posts on this topic) and others, such as Brownie, have as well.
When ViewModel.Commands
is attached to an element, it searches ViewModel.Instance
for a property of type CommandBindingCollection
with the CommandSinkAttribute
and a matching KeyName
. All CommandBinding
instances in this collection are added to the element's Commands
. This solution was inspired by the article Smart Routed Commands in WPF, although obviously the purpose and implementation are different here.
The ViewModel does not need to inherit from any base class or implement any interfaces. The ViewModel
markup extension will try to instantiate an instance of the ViewModel first by looking for a constructor that takes a single parameter compatible with the element. You should avoid creating a tight coupling to the View here by using an interface that the element can implement. If the ViewModel doesn't have a constructor that fits the criteria here, it will instead be instantiated using the default constructor, if present. It's generally best to not create any coupling here at all, but there are some things you simply can't do in the ViewModel, such as navigate to another page. A View-specific interface allows you to put such code in the View while retaining as little coupling as possible.
A similar interface can be used for the Application
. This gives the ViewModel access to application-wide settings and functionality, while not being closely tied to the actual Application
. The sample application demonstrates using both the View interfaces, as well as an application interface, including how to use Mock Objects that implement these interfaces to facilitate testing.
The goal with the ViewModel is to put as much UI state as possible into it. The View then binds this state to the necessary elements. The classic example here is the selection state for a collection that will be presented in the View. The problem is that some states in the View are given in read-only properties that cannot be used in data-binding. The SelectedItems
property on several controls is an example of this. In order to maintain such states in the ViewModel, a unique solution must be found to bind the state to the read-only properties on elements in the View. The Selection
class provides an attached SelectedItems
property to illustrate one way in which this can be accomplished. The attached property is responsible for watching the state both on the element as well as in the ViewModel, and keeping the two in sync.
Bonus: DataErrorInfo
This class isn't really related to M-V-VM, the main focus of this article. So, consider it a bonus. This is the starting point for a validation framework based on IDataErrorInfo
. It uses various ValidatorAttribute
classes to specify how properties are supposed to be validated. Only StringLengthValidatorAttribute
is included in the code, but it should be straightforward to create other validators. The Task
Model class illustrates the use and the EditTaskPage shows how to do validation with it in WPF.
History
12/16/2007 - Initial article published.