Click here to Skip to main content
15,861,125 members
Articles / Desktop Programming / WPF

Sorting and filtering a WPF ListView in an MVVM setup

Rate me:
Please Sign up or sign in to vote.
4.80/5 (8 votes)
19 Sep 2010CPOL14 min read 80.6K   4.6K   30   7
A simple WPF app which implements a Master-Detail setup with sorting and filtering in an MVVM framework.

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.

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

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

masterdetaildemo.jpg

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.

MasterDetailDemo_vm_interaction.jpg

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.

MainViewModelEvents.jpg

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:

MasterDetailDemo_simple_diagram.jpg

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.

vm_interfaces.jpg

The main diagram would look like this:

masterdetaildemo_main.jpg

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.

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

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

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

XML
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).

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

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

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

License

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


Written By
Romania Romania
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralNearly 5 :P Pin
Bastien Neyer22-Sep-10 8:35
Bastien Neyer22-Sep-10 8:35 
GeneralRe: Nearly 5 :P Pin
Zoltan Aszalos26-Sep-10 6:41
Zoltan Aszalos26-Sep-10 6:41 
GeneralMy vote of 5 Pin
Mogyi19-Sep-10 20:32
Mogyi19-Sep-10 20:32 
GeneralNeither Project Compiles Pin
sam.hill19-Sep-10 13:42
sam.hill19-Sep-10 13:42 
On VS2008: Cannot create instance of 'ViewModelLocator' defined in assembly 'MasterDetailDemo.ViewModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null'. Exception has been thrown by the target of an invocation. Error in markup file 'MasterDetailDemo.View;component/app.xaml' Line 12 Position 10.

On VS2010: missing GalaSoft library.
GeneralRe: Neither Project Compiles Pin
Zoltan Aszalos19-Sep-10 20:42
Zoltan Aszalos19-Sep-10 20:42 
GeneralRe: Neither Project Compiles Pin
dasdasdasdasdasdasdsadas25-Jul-12 0:03
dasdasdasdasdasdasdsadas25-Jul-12 0:03 
GeneralRe: Neither Project Compiles Pin
KaizerVVV13-Jan-15 3:28
KaizerVVV13-Jan-15 3:28 

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.