Introduction
In this article, I will implement a simple Master-Detail scenario using WPF technology and the MVVM paradigm. For the UI, I will use two ListView
elements. The first ListView
will enable sorting and filtering. The application was built with the aim to provide an overview of the many modern best practices used in .NET programming.
Background
You should have a basic understanding of how WPF applications work. Also, a basic understanding behind the Model View ViewModel (MVVM) paradigm should be useful (MSDN or http://live.visitmix.com/MIX10/Sessions/EX14).
For this example, I used the MVVM Light Toolkit, which you can find here.
For unit testing, I used NUnit, which you can find here. In the unit tests, I used Moq to mock objects; Moq can be found here.
As an Inversion of Control container, I used Castle Windsor, which can be found here.
Although you can download each library separately, in the downloadable projects, the necessary DLL files are all included.
The Model View ViewModel paradigm
So, what is this all about? It's a very useful Design Pattern for WPF applications because it allows the separation of concerns by decoupling the implementation details of an application from its presentation layer. The three words that describe this pattern each represent a separate layer of abstraction: the Model layer contains all the entities (in MasterDetailDemo, such classes are Person
and Car
) that are used by the application and all the functionality that is needed to retrieve these entities from an external location (in MasterDetailDemo, such classes are XmlPersonsRepository
and XmlCarsRepository
); the View layer contains UI components that give the application a visible interface; the ViewModel layer contains the actual implementation of the application, and serves as a mediator between the View and Model, basically, it prepares the data in such a format that the View can understand.
There are many reasons why you should use MVVM, but I won't go into more details right now because there are many presentations about it on the web. So, Google it up, or take a look at the links provided in the Background section.
The application
The application we're going to build will be as simple as possible. We will have a list of persons, and each person owns some cars. On the main window, we'll create two tables: one for the persons, and one for the cars owned by the selected person. To complicate things, we want the table of persons to be sortable and filterable. Clicking on the header of each column will invoke the sorting. On the header of each column, we'll have a kind of toggle button that, if activated, will bring up a filter list filled with check boxes. Checking/unchecking the check boxes will invoke the filtering.
Now, let's get down to business, shall we?
If you open up the VS solution of MasterDetailDemo, you'll see four projects. I will describe each project in detail now.
MasterDetailDemo.Model
This one will be easy. All we need for our application is two kinds of entities: one for the persons and one for the cars. The Person
class has some properties with attributes attached to them (I used the DescriptionAttribute
to store the description of the property that will be shown on the UI, and the BindableAttribute
is used to know which properties should be sortable and filterable; more on this in the MasterDetailDemo.ViewModel section).
In the Repositories folder, you will find interfaces and concrete classes for repositories for the persons and for the cars. This way, the retrieval of persons and cars from an external location is cleanly separated. Concrete repositories are created for fakes and for XML files. In the case of XmlPersonsRepository
and XmlCarsRepository
, the actual data is provided by the Persons.xml file. The fake repositories have hard-coded values for the entities.
This kind of design is needed for testing purposes; it will make sense to you when we'll be writing unit tests.
MasterDetailDemo.ViewModel
Things should get interesting here. The ViewModelLocator
class was created by the MVVM Light Toolkit. This class handles the creation of the MainViewModel
object which is then used as the binding source for the MainWindow
class (in the MasterDetailDemo.View project, more on this later.) I modified the CreateMain ()
method just to enable the instantiation of the IPersonsRepository
object by using the Castle Windsor framework.
if (MainViewModel.IsInDesignModeStatic)
personsRepository = new FakePersonsRepository ();
else
{
var container =
new WindsorContainer (
new XmlInterpreter (new ConfigResource ("castle")));
personsRepository =
container.Resolve (typeof (IPersonsRepository)) as IPersonsRepository;
}
_main =
new MainViewModel (personsRepository, new MainViewModelEvents (),
new ColumnHeaderFactory ());
All this code does is that it checks first whether we're in Design mode. If we are, then it creates a FakeRepository
object; otherwise, it creates WindsorContainer
and instantiates the concrete IPersonsRepository
object run-time depending on the settings in the App.config file. The App.config file is located in the MasterDetailDemo.View project. It is configured in the following way:
<component
id="personRepository"
service="MasterDetailDemo.Model.Repositories.Abstract.IPersonsRepository,
MasterDetailDemo.Model"
type="MasterDetailDemo.Model.Repositories.Concrete.XmlPersonsRepository,
MasterDetailDemo.Model">
<parameters>
<filePath>c:\Users\aszalos\Documents\Visual Studio 2010\
Projects\WPF projects\MasterDetailDemo\
MasterDetailDemo.Model\bin\Debug\Persons.xml</filePath>
</parameters>
</component>
This config tells WindsorContainer
to create an XmlPersonsRepository
object whenever an IPersonsRepository
object is requested. Note that you need to provide the correct path where your Persons.xml file is located. The constructor of XmlPersonsRepository
has a filePath parameter which should be specified in the config file. It is the path where Persons.xml is located. Using the Castle Windsor IoC container makes it easy to setup the dependency on the MainViewModel by modifying the configuration file, so the code will instantiate the actual IPersonsRepository
implementation which will be used by the ViewModel, thus the ViewModelLocator
class' definition doesn't need to be changed in the future.
The MainViewModel
class is also created automatically by the MVVM Light Toolkit template. This class represents the ViewModel for the MainWindow
(part of the MasterDetailDemo.View project); this is the class that gets created by the ViewModelLocator
. Before we go into the details of the implementation, I would like to summarize what the whole MasterDetailDemo.ViewModel project does.
Basically, we want to display the persons and their cars, therefore we need to store a reference to an IPersonsRepository
object. The cars of a person are directly available by calling Person.CarsOwned
, therefore we don't need to store cars in a separate object. (Actually, to maintain the Master-Detail relation between persons and cars won't need any extra code in the ViewModel; the relation will be maintained in the View, and I leave the explanation of that to the next part.)
The tricky part of the whole application is to realize the sorting and filtering of the persons. As I said before, we want the user to be able to set filtering conditions on each column of the persons table.
Each column needs to know what kind of values it contains (for example, the "First nam" column needs to know the values "Nancy", "Andrew", "Janet", and "Margaret"), therefore we need a VM class that implements this. ColumnHeaderViewModel
is the VM class that will be the binding source for each column of the persons table. Besides storing the list of the values in the respective column, it needs to know whether the popup with the check box list is currently shown or not. Why do we need that? Well, when the user clicks on another toggle button in a different column, we want to hide the currently shown popup (if it is activated), and this way, only one popup will be shown at a time.
In the popup, we'll have the filter texts together with the check boxes, therefore we need a VM class that will store information about a filter. FilterViewModel
will store the filter text that will be displayed on the UI and a boolean value that indicates whether it is active or not.
The following diagram illustrates how the communication between the main VM classes will occur.
Let's see how we can realize this communication between these three classes. The main idea was that I didn't want to have a hard reference to the MainViewModel
in the FilterViewModel
and ColumnHeaderViewModel
classes. For this reason, I used the IMainViewModelEvents
interface which has two events that the MainViewModel
object can subscribe to and two methods that the other VM classes can use to notify the subscriber (the MainViewModel
object) that a specific action occurred.
This way, all the three VM classes will have a reference to the same IMainViewModelEvents
class (the reason why I used an interface for this will be clear when we get to unit testing, but we can state right away that we need to test whether the interaction has occurred or not). The basic structure of the VM classes would like this:
Before we go into the details of the implementation of VM classes, I want to specify what the MainViewModel
object should do when the above mentioned events get fired. When the ColumnHeaderFiltersChanged
event is fired in the IMainViewModelEvents
class, we need to do the filtering. How will we do that? For each Person record, we need to iterate through the ColumnHeaders
collection of the MainViewModel
object, and for each ColumnHeaderViewModel
object, we need to check its Filters
collection. For each FilterViewModel
object, we need to check whether it is active or not. (For the logic of the filtering process, please see the code.) When the IsHeaderPopupOpenChanged
event is fired, we need to iterate through the ColumnHeaders
collection of the MainViewModel
object and set the other ColumnHeaderViewModel
objects' IsHeaderPopupOpen
property to false
.
For unit testing purposes, we need to test the VM classes in isolation, without any dependencies on other classes, but if we implement the above scenario, the MainViewModel
will depend on the implementation of the ColumnHeaderViewModel
, which in turn will depend on the implementation of the FilterViewModel
. Therefore, we create interfaces for ColumnHeaderViewModel
and FilterViewModel
which will allow us to define the dependencies by means of interfaces. This way, we'll be able to create mock objects to break those dependencies.
The main diagram would look like this:
We can assume right away that the MainViewModel
's constructor will take care of the initialization of the ColumnHeaders
property. But because we need to be able to control what kind of IColumnHeaderLocator object will be created by the constructor, we create a factory class (ColumnHeaderFactory
) which will take care of the creation of the actual IColumnHeaderLocator
object.
public class ColumnHeaderFactory
{
public virtual IColumnHeaderLocator Create (IMainViewModelEvents
mainVMEvents, String headerText, String propertyName,
List<String> filterTexts)
{
return new ColumnHeaderViewModel (mainVMEvents,
headerText, propertyName, filterTexts);
}
}
The ColumnHeaderFactory
's Create
method will be called by the constructor of the MainViewModel
class. In the case of unit testing, we'll override the Create
method and create mock objects for IColumnHeaderLocator
and IFilterLocator
.
The constructor of the MainViewModel
class looks like this:
public MainViewModel (
IPersonsRepository personRepository,
IMainViewModelEvents mainVMEvents,
ColumnHeaderFactory chFactory)
I used constructor injection to provide the dependencies for the class. The ColumnHeaders
property is initialized in the constructor; it is of type Dictionary<String, IColumnHeaderLocator>
, and stores all the columns that should be displayed on the UI. The dictionary's key property stores the bindable object's property name (e.g., Person.FirstName => FirstName), and the value property stores an IColumnHeaderLocator
object. A column header is created for each property of the Person
class that has the BindableAttribute
defined on it. Sorting by a property name can be done by executing the SortCommand
command. SortCommand
is of type RelayCommand<String>
, and receives the property name as an argument to execute the Sort ()
method. Executing the SortCommand
with the same property name argument twice sorts the persons list by that property in descending order.
In the case of the ColumnHeaderViewModel
class, I want to clarify the IsHeaderPopupOpen
property which looks like this:
public bool IsHeaderPopupOpen
{
get
{
return _isHeaderPopupOpen;
}
set
{
_isHeaderPopupOpen = value;
RaisePropertyChanged ("IsHeaderPopupOpen");
if (_isHeaderPopupOpen)
_mainVMEvents.RaiseIsHeaderPopupOpenChanged (this);
}
}
Whenever IsHeaderPopupOpen
is set to a new value, the RaisePropertyChanged (
) method is called (this method is defined by ViewModelBase
, which is the base class of all VM classes) to notify any binding targets that the value of the property has changed. In the meantime, I check whether the value is true (which means the popup header should be open), and if that's the case, then I call the RaiseIsHeaderPopupOpenChanged
method on the _mainVMEvents
object to notify the MainViewModel
object to check the other column headers. The FilterViewModel
class' IsActive
property is defined in a similar fashion, so I'll skip its explanation.
MasterDetailDemo.View
MainWindow.xaml contains the markup which sets up the UI of our application. The MainWindow
DataContext
property is bound to the ViewModelLocator
's Main
property (which is of type MainViewModel
). This is set up when you create an MVVM Light Toolkit project template, so you don't need to modify this.
SortCommand
is defined as an attached property on the MainWindow
. (You can find info on attached properties here.) We need this property because we want to execute this command when the user clicks on the header of a column of the persons table. The SortCommand
attached property is bound to the MainViewModel
's SortCommand
property in the XAML file by the code:
main:MainWindow.SortCommand="{Binding Source={StaticResource Locator}, Path=Main.SortCommand}"
To show persons and cars in a master-detail setup, all we need to do is pick a common ancestor of the ListView
controls and define the MainViewModel
's Persons
property as a binding source on its DataContext
. Additionally, you need to set IsSynchronizedWithCurrentItem="true"
on both ListView
controls (note that in the sample project, this is set via styles).
<StackPanel
DataContext="{Binding Persons}">
<ListView
ItemsSource="{Binding}"
GridViewColumnHeader.Click="ListView_Click" ... />
<ListView
ItemsSource="{Binding CurrentItem.CarsOwned.Cars}" ... />
</StackPanel>
Notice the CurrentItem
property in the second ListView
's binding expression. The CurrentItem
property is set to the Person
object which corresponds to the first ListView
's selected row. This way, whenever a new Person
object is selected in the first ListView
, the Person.CarsOwned.Cars
collection gets bound to the second ListView
.
The persons ListView
's GridView
property looks like this:
<GridView
ColumnHeaderTemplate="{StaticResource dt_ColumnHeader}"
ColumnHeaderContainerStyle="{DynamicResource stl_ColumnHeaderContainer}">
<GridViewColumn
Width="125"
DisplayMemberBinding="{Binding FirstName}"
Header="{Binding DataContext.ColumnHeaders[FirstName],
RelativeSource={RelativeSource AncestorType={x:Type Window},
Mode=FindAncestor}}"/>
...
</GridView>
Notice that the GridView
's DataContext
is implicitly set to the MainViewModel
's Persons
property (which is of type ObservableCollection<Person>
), so when we set the DisplayMemberBinding
property of the GridViewColumn
, the binding implicitly refers to the current Person
's property (e.g., FirstName). But when we want to bind the GridViewColumn
's Header
property to the MainViewModel
's ColumnHeaders[xyz]
property, we need to set RelativeSource
of the Binding to the main MainViewModel
object (which is defined on the Window).
The dt_ColumnHeader
DataTemplate
is defined in the Window.Resources
section of the XAML file, and contains two Path
elements (one for the up and one for the down arrow, respectively) and a TextBlock
element. The respective Path
element's Visibility
is set to Visible
only when the ColumnHeaderViewModel
's PropertyName
property equals the MainViewModel
's SortBy
property and the MainViewModel
's SortDirection
property equals the ConverterParameter
value ("asc" or "desc") specified for the MultiBinding
. The MultiBinding
uses the ColumnPropertyToVisibilityConverter
to convert the three values to a Visibility
object.
The stl_ColumnHeaderContainer
style is defined in the MainSkin.xaml file, and defines a ControlTemplate
for the GridViewColumnHeader
elements.
<DockPanel>
<Popup
IsOpen="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
Path=Content.IsHeaderPopupOpen, Mode=OneWay}"
PlacementTarget="{Binding ElementName=exp_Filter}" Placement="Bottom">
<ItemsControl
ItemsSource="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
Path=Content.Filters, Mode=OneWay}"
ItemTemplate="{DynamicResource dt_CheckBoxItem}" ... >
<ItemsControl.Resources>
<DataTemplate
x:Key="dt_CheckBoxItem">
<CheckBox
IsChecked="{Binding IsActive, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
Content="{Binding FilterText}" />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
</Popup>
<Expander
Name="exp_Filter"
IsExpanded="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},
Path=Content.IsHeaderPopupOpen, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}"
</Expander>
</DockPanel>
...
The Popup
's IsOpen
property is bound to the ColumnHeaderViewModel
's IsHeaderPopupOpen
property. The Popup
contains an ItemsControl
element which in turn defines CheckBox
elements as its ItemTemplate
property. The CheckBox
element's IsChecked
property and Content
property are bound to the FilterViewModel
's IsActive
property and FilterText
property, respectively. The exp_Filter
Expander element defines us a toggle button which will serve as an input element to bring up the Popup
control. Its IsExpanded
property is bound to the ColumnHeaderViewModel
's IsHeaderPopupOpen
property. The Binding mode is set to TwoWay
because we need to notify the VM class when the user clicks the toggle button.
How does the SortCommand
get executed? The ListView_Click()
event handler method is called whenever the user clicks on a column header, and if the source of the event is not the toggle button (when you click on this, you expect the popup to be shown with the filter texts instead of sorting), the SortCommand
is executed with the property name of the Person
class by which we want to sort the persons collection.
So, basically we're done with the discussion of the application. All that's left is the Test project.
MasterDetailDemo.Tests
We want to test each VM class of MasterDetailDemo.ViewModel. The tests written are basically self explanatory, so I won't go into the details except in the case of the MainViewModel
class. Because the OnColumnHeaderFilterChanged()
and OnIsHeaderPopupOpenChanged
methods rely on the actual implementation of the IColumnHeaderLocator
and IFilterLocator
interfaces, we need to create mock objects the MainViewModel
can use. As I said before, the MainViewModes
's constructor uses ColumnHeaderFactory
's Create (
) method to create the IColumnHeaderLocator
object. We need to override the Create ()
method and create mock objects in it. The mocks are stored in the ColumnHeaderLocatorMocks
and the FilterLocatorMocks
properties of the XColumnHeaderFactory
class. For more details, check the code.
Well, that was it. I hope you enjoyed it and learned something from it.
History
- 17 September 2010: Initial release.