MVVM# Episode 3
Introduction
In part one of this series of articles, I introduced my take on MVVM pattern, and discussed some shortfalls I felt existed in some implementations and, indeed, with the model itself.
In part two, I introduced the base classes and interfaces I use in my implementation that, for want of a better title, I'm calling MVVM#.
In this part of the series, I will add the application specific classes to give us a (very) simple running application.
Models
Whether we're dealing with a legacy system or a new one, I tend to think about the data first and foremost - after all, if you don't have the right data, it doesn't matter how cool the application is! (AKA GIGO).
We're just dealing with Customer
s in our example application. So we will need a Customer
class. This would be the full details of a customer
and may, in a real system, have a lot of data. When we're just dealing with a selection list, though, we really don't want to have a huge collection of large Customer
objects, just to display a customer
Name. For this purpose, I create 'ListData
' classes. The CustomerListData
class will hold just the basic details of a customer
that I want to show in my selection list.
Because the CustomerListData
is a subset of the full Customer
data, I actually inherit my Customer
data from the CustomerListData
for convenience. It means that I can always replace a collection of CustomerListData
with a collection of Customer
if I want.
CustomerListData.cs
namespace Model
{
/// <summary>
/// Summary information for a Customer
/// As a 'cut down' version of Customer information, this class is used
/// for lists of Customers, for example, to avoid having to get a complete
/// Customer object
/// </summary>
public class CustomerListData
{
/// <summary>
/// The unique Id assigned to this Customer in the Data Store
/// </summary>
public int? Id
{
get;
set;
}
/// <summary>
/// The Business name of the Customer
/// </summary>
public string Name
{
get;
set;
}
/// <summary>
/// Which State the Customer is in
/// </summary>
public string State
{
get;
set;
}
}
}
Customer.cs
namespace Model
{
/// <summary>
/// A Customer
/// This inherits from the CustomerSummary class, which contains the basic Customer
/// information provided in lists.
/// In real implementations this class may use lazy loading to get transactions
/// </summary>
public class Customer : CustomerListData
{
/// <summary>
/// The address of the customer.
/// </summary>
public string Address
{
get;
set;
}
public string Suburb
{
get;
set;
}
public string PostCode
{
get;
set;
}
public string Phone
{
get;
set;
}
public string Email
{
get;
set;
}
}
}
I've stripped my Model
class down to the bare bones for this article.
Services
Now we have some data objects, we need some way to retrieve and store them into our data store (be that a database, a text file, some XML files, a web service or whatever). So in theServices
project, we need to create our Service
Interface ...
IcustomerService.cs
using System.Collections.Generic;
using Model;
namespace Service
{
public interface ICustomerService
{
/// <summary>
/// Return the Customer for the given id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
Customer GetCustomer(int id);
/// <summary>
/// Return a list of Customers' List Data filtered by State
/// </summary>
/// <returns></returns>
List<CustomerListData> GetListOfCustomers(string stateFilter);
/// <summary>
/// Update a customer in the data store
/// </summary>
/// <param name="?"></param>
void UpdateCustomer(Customer data);
}
}
That will give us what we need for this application. So let's implement the Interface...
CustomerService.cs
using System.Collections.Generic;
using Model;
namespace Service
{
/// <summary>
/// Provide services for retrieving and storing Customer information
/// </summary>
public class CustomerService : ICustomerService
{
/// <summary>
/// A fake database implementation so we can store and retrieve customers
/// </summary>
private List<Customer> fakeDatabaseOfCustomers;
public CustomerService()
{
// Add some data to our database
fakeDatabaseOfCustomers = new List<Customer>();
fakeDatabaseOfCustomers.Add(DummyCustomerData(1));
fakeDatabaseOfCustomers.Add(DummyCustomerData(2));
fakeDatabaseOfCustomers.Add(DummyCustomerData(3));
fakeDatabaseOfCustomers.Add(DummyCustomerData(4));
fakeDatabaseOfCustomers.Add(DummyCustomerData(5));
fakeDatabaseOfCustomers.Add(DummyCustomerData(6));
fakeDatabaseOfCustomers.Add(DummyCustomerData(7));
}
/// <summary>
/// Make a fake customer
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private Customer DummyCustomerData(int id)
{
Customer customer = new Customer()
{
Id = id,
Address = id.ToString() + " High Street",
Suburb = "Nether Wallop",
State = (id % 2) == 0 ? "Qld" : "NSW",
Email = "Customer" + id.ToString() + "@BigFoot.Com",
Phone = "07 3333 4444",
Name = "Customer Number " + id.ToString()
};
return customer;
}
#region ICustomerService
public Customer GetCustomer(int id)
{
return fakeDatabaseOfCustomers[id - 1];
}
public List<CustomerListData> GetListOfCustomers(string stateFilter)
{
List<CustomerListData> list = new List<CustomerListData>();
foreach (var item in fakeDatabaseOfCustomers)
{
if (string.IsNullOrEmpty(stateFilter) ||
item.State.ToUpper() == stateFilter.ToUpper())
{
list.Add(new CustomerListData()
{
Id = item.Id,
Name = item.Name,
State = item.State
});
}
}
return list;
}
public void UpdateCustomer(Customer data)
{
fakeDatabaseOfCustomers[(int)data.Id - 1] = data;
}
#endregion
}
}
You'll see that the CustomerService
class creates a 'fake' collection (fakeDatabaseOfCustomer
) - no saving to any repository - but it serves the purposes for this demonstration. It's just there to help us get an application running with some data without having to populate a database - don't confuse it with design-time data which will be discussed later.
Don't forget to add a reference to the Models
project!
ViewData
So, we have our data objects (in Models
) and we have some services to store and retrieve the data. Now we need to think about the actual presentation. Remember that our ViewData
need to have Observable
properties for each of the properties the user needs to see.
We'll think first about what data is going to be used.
- We'll need a class containing all of the editable properties of the
Customer
(CustomerEditViewData
) - We'll need a class containing a minimal set of data for displaying lists of
Customer
information (CustomerListItemViewData
) - We'll need a class containing a collection of
CustomerListItemViewData
so we can show a list (CustomerSelectionViewData
)
These classes have a more or less 1-1 relationship with the ViewModels
(and thus the Views
) we'll be creating. In this case, there's also a (more or less)1-1 relationship between the ViewData
and the Model
objects - but that's not necessarily going to be the case in larger more complex applications.
I say 'more or less' because while the CustomerEditViewData
maps to the CustomerEditViewModel
, and the CustomerSelectionViewData
maps to the CustomerSelectionViewModel
, CustomerSelectionViewData
is really just a collection of CustomerListItemViewData
- which doesn't have its own ViewModel
at all.
These classes all live in the ViewModel
's project, in their own sub folder, ViewData.
They all inherit from BaseViewData
, and use their base's RaisePropertyChanged
method to notify the View
(s) of any changes.
So, let's start off with the CustomerListItemViewData
:
using System.Windows;
namespace ViewModels
{
/// <summary>
/// A minimalist view of a Customer - for displaying in lists
/// </summary>
public class CustomerListItemViewData : BaseViewData
{
#region Private Fields
private string customerName;
private int? customerId;
private string state;
#endregion
#region Observable Properties
/// <summary>
/// The Id of the Customer represented by this item.
/// </summary>
public int? CustomerId
{
get
{
return customerId;
}
set
{
if (value != customerId)
{
customerId = value;
base.RaisePropertyChanged("CustomerId");
}
}
}
public string CustomerName
{
get
{
return customerName;
}
set
{
if (value != customerName)
{
customerName = value;
base.RaisePropertyChanged("CustomerName");
}
}
}
public string State
{
get
{
return state;
}
set
{
if (value != state)
{
state = value;
base.RaisePropertyChanged("State");
}
}
}
#endregion
#region Constructor
#endregion
}
}
That's all fairly simple - so let's move on to the CustomerSelectionViewData
, which, as we've said, is just a collection of CustomerListItemViewData
using System.Collections.ObjectModel
.
namespace ViewModels
{
public class CustomerSelectionViewData : BaseViewData
{
private ObservableCollection<CustomerListItemViewData> customers;
public ObservableCollection<CustomerListItemViewData> Customers
{
get
{
return customers;
}
set
{
if (value != customers)
{
customers = value;
base.RaisePropertyChanged("Customers");
}
}
}
}
}
Well, now we're getting somewhere!
We also need CustomerEditViewData
- this is a big one, but still pretty simple in concept.
namespace ViewModels
{
/// <summary>
/// Editable Customer Info
/// </summary>
public class CustomerEditViewData : BaseViewData
{
#region Private Fields
private string name;
private int? customerId;
private string address;
private string suburb;
private string email;
private string postCode;
private string phone;
private string state;
#endregion
#region Observable Properties
public int? CustomerId
{
get
{
return customerId;
}
set
{
if (value != customerId)
{
customerId = value;
base.RaisePropertyChanged("CustomerId");
}
}
}
public string Name
{
get
{
return name;
}
set
{
if (value != name)
{
name = value;
base.RaisePropertyChanged("Name");
}
}
}
public string Address
{
get
{
return address;
}
set
{
if (value != address)
{
address = value;
base.RaisePropertyChanged("Address");
}
}
}
public string Suburb
{
get
{
return suburb;
}
set
{
if (suburb != value)
{
suburb = value;
base.RaisePropertyChanged("Suburb");
}
}
}
public string Email
{
get
{
return email;
}
set
{
if (email != value)
{
email = value;
base.RaisePropertyChanged("Email");
}
}
}
public string PostCode
{
get
{
return postCode;
}
set
{
if (postCode != value)
{
postCode = value;
base.RaisePropertyChanged("PostCode");
}
}
}
public string Phone
{
get
{
return phone;
}
set
{
if (phone != value)
{
phone = value;
base.RaisePropertyChanged("Phone");
}
}
}
public string State
{
get
{
return state;
}
set
{
if (state != value)
{
state = value;
base.RaisePropertyChanged("State");
}
}
}
#endregion
#region Constructor
#endregion
}
}
Let's finish there with the ViewData
and move over to our Controller
.
ICustomerController
We need now to look at our CustomerController
. What functionality do we need it to perform?
- Provide a
CustomerSelectionViewData
object to be shown to the user - Handle the selection of a
Customer
- Handle the request to edit a
Customer
- Handle updating a
Customer
when changes are saved
It is worth just looking closely at items 2 and 3. In a simplistic view, you might think that we don't need both of these - after all, when a Customer
is selected, we're going to edit it; but there's actually two steps here - the selection and the editing - even though the selection in this case is specifically for editing.
What we're going to be doing is sending a message when the customer
is selected - and that will be the end of the job for the CustomerSelectionViewModel
. The Controller
will send a message informing anyone that's interested that the Customer
has been selected for editing. If there's nothing our there that has both registered to receive the message, and that confirms they can handle editing this specific customer, then the controller will need to take steps to edit the customer itself - by instantiating a new CustomerEditViewModel
and CustomerEditView
.
It may sound overly complicated but what I had in mind here was allowing us to have several CustomerEditViews
open at one time - each editing a different customer. So, if the user selected a customer, all of the CustomerEditViewModels
would receive the message telling them that Customer 1234
has been selected for editing. Most ViewModels
would ignore the message - but one that is currently editing that very customer, could then 'make itself known'.
So - here's our ICustomerController
interface. This is in the ViewModels
project - under BaseClasses
(yeah, I know it's not a class, but if you're worried, change the folder name to BaseClassesIntrefacesAndOtherNonProjectSpecificClasses
or something!
namespace ViewModels
{
public interface ICustomerController : IController
{
/// <summary>
/// Return a collection of Customer information to be displayed in a list
/// </summary>
/// <returns>A collection of Customers</returns>
CustomerListViewData GetCustomerSelectionViewData(string stateFilter);
/// <summary>
/// Do whatever needs to be done when a Customer is selected (i.e. edit it)
/// </summary>
/// <param name="customerId"></param>
void CustomerSelectedForEdit(CustomerListItemViewData data, BaseViewModel daddy);
/// <summary>
/// Edit this customer Id
/// </summary>
/// <param name="customerId"></param>
void EditCustomer(int customerId, BaseViewModel daddy);
/// <summary>
/// Update Customer data in the repository
/// </summary>
/// <param name="data"></param>
void UpdateCustomer(CustomerEditViewData data);
}
}
ViewModel
Well, now we have the basics, let's start thinking about our first ViewModel
. At last!
The CustomerSelectionViewModel
needs to display a list of customers, and allow the user to select one. Initially, that's it, so let's write the CustomerSelectionViewModel
class.
CustomerSelectionViewModel
using System;
using System.Windows.Input;
using Messengers;
namespace ViewModels
{
/// <summary>
/// This view model expects the user to be able to select from a list of
/// Customers, sending a message when one is selected.
/// On selection, the Controller will be asked to show
/// the details of the selected Customer
/// </summary>
public class CustomerSelectionViewModel : BaseViewModel
{
#region Properties
/// <summary>
/// Just to save us casting the base class's
/// IController to ICustomerController all the time...
/// </summary>
private ICustomerController CustomerController
{
get
{
return (ICustomerController)Controller;
}
}
#region Observable Properties
private CustomerListItemViewData selectedItem;
public CustomerListItemViewData SelectedItem
{
get
{
return selectedItem;
}
set
{
if (value != selectedItem)
{
selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
}
#endregion
#endregion
#region Commands
#region Command Relays
private RelayCommand userSelectedItemCommand;
public ICommand UserSelectedItemCommand
{
get
{
return userSelectedItemCommand ??
(userSelectedItemCommand = new RelayCommand(() =>
ObeyUserSelectedItemCommand()));
}
}
#endregion
#region Command Handlers
private void ObeyUserSelectedItemCommand()
{
CustomerController.CustomerSelectedForEdit
(this.SelectedItem, this);
}
#endregion
#endregion
#region Constructors
/// <summary>
/// Required to allow our DesignTime version to be instantiated
/// </summary>
protected CustomerSelectionViewModel()
{
}
public CustomerSelectionViewModel(ICustomerController controller,
string stateFilter = "")
: this(controller, null, stateFilter)
{
}
/// <summary>
/// Use the base class to store the controller
/// and set the Data Context of the view (view)
/// Initialise any data that needs initialising
/// </summary>
/// <param name="controller"></param>
/// <param name="view"></param>
public CustomerSelectionViewModel(ICustomerController controller,
IView view, string stateFilter = "")
: base(controller, view)
{
controller.Messenger.Register(MessageTypes.MSG_CUSTOMER_SAVED,
new Action<Message>(RefreshList));
// Leave it for half a second before filtering on State
RefreshList();
}
#endregion
#region Private Methods
private void RefreshList(Message message)
{
RefreshList();
message.HandledStatus = MessageHandledStatus.HandledContinue;
}
/// <summary>
/// Ask for an updated list of customers based on the filter
/// </summary>
private void RefreshList()
{
ViewData =
CustomerController.GetCustomerSelectionViewData("");
}
#endregion
}
}
A few things to note in the CustomerSelectionViewModel
...
First, to save me having to cast the BaseViewModel
's Controller
property to ICustomerController
all the time, I've added a private
property CustomerController
. Much like the flushable toilet, it's just a convenience thing.
We have an Observable
property of SelectedItem
. This is the CustomerListItemViewData
that is currently selected from the list presented to the user - so whatever binds to this property needs to tell us via that binding what is currently selected.
We have a UserSelectedItemCommand
. As its name suggests, this is the Command
that our View
will send when the user has selected an item. It is up to the designer whether this is on the press of a button, or as each row on a grid list is clicked, or via some quirky user interface dreamed up over several pints of Guinness.
There is a parameterless constructor. This is required because I want to be able to provide design-time support for data - and design time support demands a parameterless constructor. Every ViewModel
requires a Controller
, so the other constructors require an ICustomerController
parameter. I'm also allowing the constructor to (optionally) provide a State Filter. This isn't implemented in the listing above, but the aim is to allow a CustomerSelectionViewModel
to be created, filtering the customers to only show those from a particular state - perhaps the state the operator is in.
The other constructor allows us to create the ViewModel
without an injected View. But what good is a ViewModel
without a View
? Well, no good at all - but good question, it shows you're paying attention! The non-view variant of our constructor will allow us to instantiate a ViewModel
for a View
that is created at design time - for example, if the designer decides that the Customer
selection and edit should both appear together on a 'parent' view, she can design it like that, and we'll need to create a parent ViewModel
that instantiates the CustomerSelectionViewModel
and assigns it to the DataContext
of the design-time created view.
Notice that the constructor also registers our ViewModel
to receive messages of type MSG_CUSTOMER_SAVED
and, when it does so, it uses the RefreshList
method to ask the Controller
to provide an updated list of Customers
. This way, whenever a customer
is updated somewhere, our list will reflect any changes.
When it's instantiated, our ViewModel
also calls its Refresh()
method to get the initial data for display. I sometimes struggle with the "right" way to do this - should the ViewModel
get the data when it's instantiated, or should the Controller
feed in the data? There's pros and cons for each school of thought, and in this case I chose to use the 'pull' method - where the ViewModel
pulls the data from the Controller
- rather than the 'push' method - where the Controller
pushes the data into the ViewModel
.
View
We really should think about creating a View
now - so we not only have something to see, but also so our highly paid designer can have something to do!
Create a new WPF UserControl
in the Views
project, called CustomerSelectionView
. You'll need to change the base class in the .cs file to BaseView
(from UserControl
). Then, do your design. Here's my XAML. (I'm not a designer!)
<view:BaseView x:Class="Views.CustomerSelectionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:view="clr-namespace:Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Background="#FF190000"
Margin="0"
Padding="1"
Height="304"
Width="229"
d:DataContext="{d:DesignInstance
Type=view:DesignTimeCustomerSelectionViewModel,
IsDesignTimeCreatable=true}">
<view:BaseView.Resources>
<view:NullToFalseBooleanConverter x:Key="NullToFalseBooleanConverter" />
<view:NullToHiddenVisibilityConverter
x:Key="NullToHiddenVisibilityConverter" />
</view:BaseView.Resources>
<StackPanel Background="#FF0096C8">
<StackPanel Orientation="Horizontal"
Margin="20,20,20,2"
Height="20">
<TextBlock>State:</TextBlock>
<TextBox Width="80"
Margin="10,0,0,0"
Text="{Binding Path=StateFilter,
UpdateSourceTrigger=PropertyChanged}"></TextBox>
</StackPanel>
<DataGrid AutoGenerateColumns="False"
Height="186"
Margin="4"
ItemsSource="{Binding ViewData.Customers}"
SelectedItem="{Binding Path=SelectedItem}"
Background="#FFE0C300"
CanUserReorderColumns="False"
AlternatingRowBackground="#E6FCFCB8"
CanUserAddRows="False"
CanUserDeleteRows="False"
CanUserResizeRows="False"
SelectionMode="Single"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Customer"
Binding="{Binding Path=CustomerName}"
Width="*" />
<DataGridTextColumn Header="State"
Binding="{Binding Path=State}" />
</DataGrid.Columns>
</DataGrid>
<TextBlock Visibility="{Binding Path=SelectedItem,
Converter={StaticResource NullToHiddenVisibilityConverter}}">
<TextBlock.Text>
<MultiBinding StringFormat="{}Selected {0} with Id {1}">
<Binding Path="SelectedItem.CustomerName" />
<Binding Path="SelectedItem.CustomerId" />
</MultiBinding>
</TextBlock.Text></TextBlock>
<Button Content="Edit Customer"
Command="{Binding Path=UserSelectedItemCommand,
Mode=OneTime}"
Width="Auto"
HorizontalAlignment="Right"
Margin="4"
Padding="8,0,8,0"
IsEnabled="{Binding Path=SelectedItem,
Converter={StaticResource NullToFalseBooleanConverter}}" />
</StackPanel>
</view:BaseView>
If you're following along rather than downloading the project, you'll see that there's a couple of errors in the XAML.
In our resources section, we have two resources referenced that we've not written yet; NullToFalseBooleanConverter
and NullToHiddenVisibilityConverter
. The reason for the first is that my designer wants to display the Id and Name of the currently selected customer in a text block - so obviously if nothing is currently selected, she wants the textblock
to be hidden. The second is used because the designer wants the Edit Customer button to be disabled when no customer is selected.
I stick all my converters into a single source file, in a converters folder in the views
project - so we can go ahead and write these two simple converters now.
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Views
{
/*
* This source file contains all the converters used.
*/
/// <summary>
/// Returns false if the object is null, true otherwise.
/// handy for using when something needs to be enabled or disabled depending on
/// whether a value has been selected from a list.
/// </summary>
[ValueConversion(typeof(object), typeof(bool))]
public class NullToFalseBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
return (value != null);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
/// <summary>
/// Returns false if the object is null, true otherwise.
/// handy for using when something needs to be enabled or disabled depending on
/// whether a value has been selected from a list.
/// </summary>
[ValueConversion(typeof(object), typeof(Visibility))]
public class NullToHiddenVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null)
{
return Visibility.Hidden;
}
else
{
return Visibility.Visible;
}
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
}
When these two converters are written, we're left with a single compile error. The line:
d:DataContext="{d:DesignInstance Type=view:DesignTimeCustomerSelectionViewModel,
IsDesignTimeCreatable=true}">
can't find the DesignTimeCustomerSelectionViewModel
class. which is fair enough, as we haven't written it yet!
This class is the design-time only class that I can populate with some realistic-looking data to allow my designer to see what she's dealing with. So much nicer for her to see real data rather than an empty grid.
using System.Collections.ObjectModel;
using ViewModels;
namespace Views
{
/// <summary>
/// This class allows us to see design time customers. Blendability R Us
/// </summary>
public class DesignTimeCustomerSelectionViewModel : CustomerSelectionViewModel
{
public DesignTimeCustomerSelectionViewModel()
{
ViewData = new CustomerListViewData();
var customers = new ObservableCollection<CustomerListItemViewData>();
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 1,
CustomerName = "First Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 2,
CustomerName = "2nd Customer",
State = "Qld"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 3,
CustomerName = "Third Customer",
State = "NSW"
});
customers.Add(new CustomerListItemViewData()
{
CustomerId = 4,
CustomerName = "Fourth Customer",
State = "SA"
});
((CustomerListViewData)ViewData).Customers = customers;
}
}
}
The class itself inherits from the 'real' CustomerSelectionViewModel
, and just has a constructor that creates some dummy data for the designer to use.
Once this is written, rebuild and you should see the design time data, at design time! As Designed!

It seems we're so close to having a running program - just a few bits to do, so why not give the View
to your designer to pretty up while we do the technical stuff?
Controller
Remember we created the ICustomerController
interface earlier? Well, now we have to do some real implementation. In any large system, the Controller
can become a bit of a large beast, so I tend to split mine into several partial classes. The main one called CustomerController
, then others called CustomerController_DataRetrieval
and CustomerController_ViewManagement
. this is one of those things that I find useful, and you may like it, or use different partial classes, or just lump code into one source file with lots of #regions
- whatever takes your fancy. The thing I like about the logical separation into partial classes is in a multi-developer environment it allows me to assign a developer to write one area of the controller without affecting other developers who may work on other areas of the Controller
.
Because the Controller
is the central hub of the system, it requires references to all the other projects, and also PresentationCore
, PresentationFramework
, WindowsBase
and System.Xaml - add 'em now or wait to see the errors if you don't believe me. ;)
CustomerController.cs
using Messengers;
using Service;
using ViewModels;
using Views;
namespace Controllers
{
/// <summary>
/// The controller 'is' the application.
/// Everything is controlled by this :
/// it instantiates Views and ViewModels
/// it retrieves and stores customers via services
///
/// But it does all this only in response to requests
/// made by the ViewModels.
///
/// e.g. a ViewModel may request a list of customers
/// e.g. a ViewModel may want to save changes to a customer
///
/// set up as a partial class for convenience
/// </summary>
public partial class CustomerController : BaseController, ICustomerController
{
private static ICustomerService CustomerService;
#region Constructors
/// <summary>
/// Private constructor - we must pass a service to the constructor
/// </summary>
private CustomerController()
{
}
/// <summary>
/// The controller needs a reference to the service layer to enable it
/// to make service calls
/// </summary>
/// <param name="customerService"></param>
public CustomerController(ICustomerService customerService)
{
CustomerService = customerService;
}
#endregion
#region Public Methods
/// <summary>
/// Main entry point of the Controller.
/// Called once (from App.xaml.cs) this will initialise the application
/// </summary>
public void Start()
{
ShowViewCustomerSelection();
}
/// <summary>
/// Edit the customer with the Id passed
/// </summary>
/// <param name="customerId">Id of the customer to be edited</param>
/// <param name="daddy">The 'parent' ViewModel who will own the
/// ViewModel that controls the Customer Edit</param>
public void EditCustomer(int customerId, BaseViewModel daddy = null)
{
//BaseView view = GetCustomerEditView(customerId, daddy);
//view.ShowInWindow(false, "Edit Customer");
}
/// <summary>
/// A Customer has been selected to be edited
/// </summary>
/// <param name="data">The CustomerListItemViewData of the selected customer
/// </param>
/// <param name="daddy">The parent ViewModel</param>
public void CustomerSelectedForEdit(CustomerListItemViewData data,
BaseViewModel daddy = null)
{
// Check in case we get a null sent to us
if (data != null && data.CustomerId != null)
{
NotificationResult result = Messenger.NotifyColleagues
(MessageTypes.MSG_CUSTOMER_SELECTED_FOR_EDIT, data);
if (result == NotificationResult.MessageNotRegistered ||
result == NotificationResult.MessageRegisteredNotHandled)
{
// Nothing was out there that handled our message,
// so we'll do it ourselves!
EditCustomer((int)data.CustomerId, daddy);
}
}
}
#endregion
}
}
The main CustomerController
source will show a couple of build errors until we complete the other partial classes. notice also here I've commented out code in the EditCustomer
method - as we haven't yet created the ViewModel
or View
to perform this function.
CustomerController_Dataretrieval.cs
using System.Collections.ObjectModel;
using Messengers;
using Model;
using Service;
using ViewModels;
namespace Controllers
{
public partial class CustomerController
{
/// <summary>
/// Get a collection of Customers and return an Observable
/// collection of CustomerListItemViewData
/// for display in a list.
/// You could bypass this conversion if you wanted to present a
/// list of Customers by binding directly to
/// the Customer object.
/// </summary>
/// <returns></returns>
public CustomerListViewData GetCustomerSelectionViewData(string stateFilter)
{
CustomerListViewData vd = new CustomerListViewData();
vd.Customers = new ObservableCollection<CustomerListItemViewData>();
foreach (var customer in CustomerService.GetListOfCustomers(stateFilter))
{
vd.Customers.Add(new CustomerListItemViewData()
{
CustomerId = (int)customer.Id,
CustomerName = customer.Name,
State = customer.State
});
}
return vd;
}
/// <summary>
/// Get the Edit View Data for the Customer Id specified
/// </summary>
/// <param name="customerId"></param>
/// <returns></returns>
public CustomerEditViewData GetCustomerEditViewData(int customerId)
{
var customer = CustomerService.GetCustomer(customerId);
return new CustomerEditViewData()
{
CustomerId = customer.Id,
Name = customer.Name,
Address = customer.Address,
Suburb = customer.Suburb,
PostCode = customer.PostCode,
State = customer.State,
Phone = customer.Phone,
Email = customer.Email
};
}
public void UpdateCustomer(CustomerEditViewData data)
{
Customer item = new Customer()
{
Id = data.CustomerId,
Address = data.Address,
Name = data.Name,
Suburb = data.Suburb,
PostCode = data.PostCode,
Email = data.Email,
Phone = data.Phone,
State = data.State
};
CustomerService.UpdateCustomer(item);
Messenger.NotifyColleagues(MessageTypes.MSG_CUSTOMER_SAVED, data);
}
}
}
CustomerController_ViewManagement.cs
using ViewModels;
using Views;
namespace Controllers
{
public partial class CustomerController : ICustomerController
{
/// <summary>
/// The ShowView methods are private.
/// A ViewModel may request some action to take place,
/// but the Controller will decide whether this action will result
/// in some view being shown.
/// e.g. clicking a 'Search' button on a form may result
/// in a Command being sent from the
/// View (via binding) to the ViewModel; the Command handler
/// then asks the Controller to
/// Search for whatever.
/// The controller may (for example) use a service to
/// return a collection of objects. if there
/// is only a single object, then it may return a single object
/// rather than popping up a search
/// view only to have the User be presented with a single action
/// from which to select.
/// </summary>
#region Show Views
private void ShowViewCustomerSelection()
{
CustomerSelectionView v = GetCustomerSelectionView();
v.ShowInWindow(false);
}
#endregion
#region Get Views
private CustomerSelectionView GetCustomerSelectionView(BaseViewModel daddy = null)
{
CustomerSelectionView v = new CustomerSelectionView();
CustomerSelectionViewModel vm = new CustomerSelectionViewModel(this, v);
if (daddy != null)
{
daddy.ChildViewModels.Add(vm);
}
return v;
}
private BaseView GetCustomerEditView(int customerId, BaseViewModel daddy)
{
//CustomerEditView v = new CustomerEditView();
//CustomerEditViewModel vm = new CustomerEditViewModel(this, v);
//vm.ViewData = GetCustomerEditViewData(customerId);
//if (daddy != null)
//{
// daddy.ChildViewModels.Add(vm);
//}
//return v;
return new BaseView();
}
#endregion
}
}
And again, I've fiddled the GetCustomerEditView
method as we've not written the View
or ViewModel
yet.
Give that a build and it should be clean. Try to run it, though, and you will see an unhandled IO exception "cannot locate resource 'mainwindow.xaml'".
Fear not - this is expected. Remember we created a WPF application which expected us to use a mainwindow WPF window - which we got rid of? But we didn't tell the application that we didn't need it! Let's do that now. We'll need to add a reference to the Controllers
project from the CustomerMaintenance
project - so the application knows where to find its controllers, and a reference to the Services
project, as the Controllers
require a Service
injected into their constructor. we also need a reference to ViewModels
because that's where the CustomerController
interface is located.
You also need to ensure that the Build Action property of the App.Xaml file to be 'ApplicationDefinition
'.

Open up your App.xaml file in the Customermaintenance
project, and change it to look like this...
<Application x:Class="MyMVVMApplication.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="Application_Startup">
<Application.Resources>
</Application.Resources>
</Application>
The Startup=
attribute needs to point to our Event Handler that will start the whole thing going.
Finally, open up the App.xaml.cs file and change it to look like this...
using System.Windows;
using Controllers;
using Service;
using System;
namespace CustomerMaintenance
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
private void Application_Startup(object sender, StartupEventArgs e)
{
CustomerController controller = new CustomerController(new CustomerService());
controller.Start();
}
}
}
Well - what are you waiting for? Press F5!
The program runs, a form appears with the Customer
selection on it, showing a list of customers
.
Astute WPF programmers always check the Output window when they run an application. Just in case you are one of them, I will point out that, in fact, there is an error:
System.Windows.Data Error: 40 : BindingExpression path error: 'StateFilter'
property not found on 'object' ''CustomerSelectionViewModel' (HashCode=13304725)'.
BindingExpression:Path=StateFilter; DataItem='CustomerSelectionViewModel'
(HashCode=13304725); target element is 'TextBox' (Name='');
target property is 'Text' (type 'String')
That's just because I've left the StateFilter TextBox
on the View
but omitted any property in the ViewModel
to actually handle it.
But let's not dwell on the negatives, put on your party frock and celebrate - we've a working MVVM# application!
Next time, we'll add the filtering, and create CustomerEditViewModel
and associated View so we'll end up with a small, but functional, application.