You can download the source code and see other parts of this application here.
It is highly recommended that you walk through the explanations by looking at the source code as it will help you gain a more solid understanding. You can also run the application on your computer. The goal of this project is to show you how to build a flexible and extensible application by using a very simple approach.
This is a WPF Master Detail application using the MVVM pattern at the UI tier. For now let's just see what the application looks like.
There are a list of customers and each customer can have multiple orders. When you click on a customer it shows you the list of orders belonging to that customer:
You can add, edit, or delete customers by clicking on the buttons. The Add and Edit Customer window uses the same View so they look the same. Deleting the customer will delete all the orders belonging to that customer:
Similarly, you can add, edit, or delete an order for a customer. The Add and Edit Order window also uses the same View. There are validations to make sure the order information entered is correct. The button is enabled only after it passes the validations.
The UI tier is built using WPF MVVM pattern. It shows you how to:
- Design the View and the ViewModel
- Handle the user interactions
- Open new windows using the MVVM pattern with an example of using Dependency Injection
The View and the ViewModel In the View we have:
Below is the diagram that shows you the main classes of the View and the ViewModel:
- MainWindow -- the main window when the application starts
- CustomerListView -- lists all the customers, and can open the CustomerView when clicked on the Add Customer or Edit Customer
- CustomerView -- the view to Add Customer or Edit Customer
- OrderListView -- lists all the orders belonging to the selected customer, and can open the OrderView when clicked on the Add Order or Edit Order
- OrderView -- the view to Add Order or Edit Order
The ViewModel classes are designed for binding to the views. These classes are not meant to replicate the domain model which is already done in the Business tier.
The MainWindow initializes the DataContext for the application by assigning it to the CustomerListViewModel singleton:
In the xaml of the MainWindow
it specifies the Views to use (CustomerListView
) by using DataTemplates:
Notice that the CustomerListView binds to the CustomerListViewModel, and the OrderListView binds to the CustomerListViewModel's SelectedCustomer property, which is a CustomerViewModel that has a reference to the orders belonging to the customer.Below is the UI of the CustomerListView:
The CustomerListView binds to the CustomerListViewModel, and we specify the properties of the ViewModel that we want each UI controls to bind:
Notice that the CellTemplate of the GridViewColumn defines the Edit and the Delete buttons. But more importantly, the SelectedItem of the ListView is bound to the SelectedCustomer of the ViewModel, which shows the list of orders belonging to the customer when the user clicks on the customer. This is because OrderListView is also bound to the SelectedCustomer of the ViewModel:
Below is the xaml of the OrderListView:
The button checks to see if the DataContext is null (when no customer is selected), if it is null then the Add Order button is disabled. The ListView binds to the Orders property of the CustomerViewModel to show the list of orders.
The CustomerView window is opened when you click on the Add Customer or the Edit Customer button. Below is the UI of the CustomerView which can be an Add Customer or an Edit Customer window:
The window determines if it is an Add or the Edit window is by looking at the Mode property of the CustomerViewModel. The Mode can be either an Add or an Edit:
The CustomerView displays different texts by looking at the Mode property:
Clicking on the Add or Save button on the window will call the Update() method of the CustomerViewModel, which determines if it should add or edit a customer based on the Mode property:
The same concept is applied to the OrderView.
Execute the Commands
To execute the commands we write a CommandBase base class that we can use in the ViewModel. The base class merely passes the delegate that will be run when the command is executed:
Then in the ViewModel we declare the commands that is available for the View to bind. For example, in the CustomerViewModel we define the command to delete the customer:
Which will run the Delete() method when called. Then in the CustomerListView
we specify the command to execute when the delete button is clicked:
All of the commands are set up this way to facilitate the binding of the commands to the ViewModel.Setup Automatic UI Update
Next we need to make sure the entire UI is updated when a property of the ViewModel is changed. This is done by including a ViewModelBase
class in which all the ViewModels inherits:
For example, the CustomerViewModel
inherits from the ViewModelBase
When the FirstName
property of the CustomerViewModel
changes (such as when editing a customer) the entire UI should reflect such change. In the ViewModel's property we just call the OnPropertyChanged method with the property name and WPF will take care of the rest:
Undo After an UI Update
When an user cancel a change you will encounter an issue with data binding. For example, the user may change the first name of a customer in the edit customer window which will update the entire UI:
then later decide to cancel by pressing the Cancel button. With the default two-way data binding the UI will retain the changed value even though the user canceled the change.
The first thought may be to use one-time binding and commit the change only if the user clicks on the Save button. This will work except you will encounter a few more issues:
- You will need to pass multiple values to the save command, which clutters up the xaml and you will also need to cast the multi-value being passed from the command from a list of type object
The way to solve this is to let WPF proceed with the binding and do a rollback when the cancel button is clicked. We store the original value using MemberwiseClone() in the constructor and the Update() method of the ViewModel (although you can write your own deep copy methods if you have reference type fields):
- You will not be able to use the validation provided by WPF which requires data binding
Then the CancelCommand in the ViewModel will just call Undo() and rollback to the original value and the entire UI will be updated:
The same method is applied to the Edit Order screen.
Below is the OrderView where the Save button is disabled with the validation message:
The button is disabled based on the Validation.HasError property of the textboxes in the OrderView.xaml:
To perform the validation on the textboxes we set the ValidatesOnDataErrors, NotifyOnValidationError, and UpdateSourceTrigger properties:
The PropertyChanged value of the UpdateSourceTrigger means that validation is performed on every keystroke in the textbox. We define the UI to show when an error happens in ValidationStyle.xaml:
The AdornedElementPlaceholder shows the control being validated, and the ErrorContent is the validation message string.
The validation check is done in the ViewModel by implementing the IDataErrorInfo interface:
In the implementation of the IDataErrorInfo interface we specify the message to display based on the condition:
Notice that we set the Quantity property of the OrderViewModel to string instead of int, this is so that we can display a more user friendly message when a non-integer is entered instead of the default message on casting which is not very intuitive to a regular user.
Open New Window
There are 2 ways to open a new window:
Using the code-behind creates less code, while using the ViewModel lets you abstract out the details on opening of a window by using an established framework.Open Window Using the Code Behind
- Use the ViewModel via Dependency Injection
We open the Add/Edit Order window using the code-behind by assigning the DataContext to the new window and open it:
Open Window Using the ViewModel via Dependency Injection
Dependency Injection means you pre-register a type to an interface, and when the interface is requested the pre-registered type is automatically created for the interface. The is shown in the opening of the Add/Edit Customer window. In the CustomerViewModel we define the command to open the Edit Customer window, which opens an IModalDialog:
Notice that the ViewModel does not know what is the View being opened. It only needs to get the reference to the pre-registered IModalDialog interface and call its methods to open the window.
The pre-registration of the View to the IModalDialog is done in the BootStrapper when the application starts. In this case we use the CustomerViewDialog as the IModalDialog when the interface is requested:
Also notice that we use the UnityServiceLocator
for managing the registrations, underneath the cover it simply uses the Unity Framework from the Microsoft Enterprise Library (you can use other frameworks if you like):
We hope you find this project useful. Although there are a lot more things you can do but this should give you a good start on how to build a flexible master details application.