Other Articles In This Series:
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
In the fourth article, I'll finish off the application to show a (small) but functioning application demonstrating some
of the functions available.
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
{
public class CustomerListData
{
public int? Id
{
get;
set;
}
public string Name
{
get;
set;
}
public string State
{
get;
set;
}
}
}
Customer.cs
namespace Model
{
public class Customer : CustomerListData
{
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 the
Services
project, we need to create our
Service
Interface ...
IcustomerService.cs
using System.Collections.Generic;
using Model;
namespace Service
{
public interface ICustomerService
{
Customer GetCustomer(int id);
List<CustomerListData> GetListOfCustomers(string stateFilter);
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
{
public class CustomerService : ICustomerService
{
private List<Customer> fakeDatabaseOfCustomers;
public CustomerService()
{
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));
}
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
{
public class CustomerListItemViewData : BaseViewData
{
#region Private Fields
private string customerName;
private int? customerId;
private string state;
#endregion
#region Observable Properties
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
{
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
{
CustomerListViewData GetCustomerSelectionViewData(string stateFilter);
void CustomerSelectedForEdit(CustomerListItemViewData data, BaseViewModel daddy);
void EditCustomer(int customerId, BaseViewModel daddy);
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
{
public class CustomerSelectionViewModel : BaseViewModel
{
#region Properties
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
protected CustomerSelectionViewModel()
{
}
public CustomerSelectionViewModel(ICustomerController controller,
string stateFilter = "")
: this(controller, null, stateFilter)
{
}
public CustomerSelectionViewModel(ICustomerController controller,
IView view, string stateFilter = "")
: base(controller, view)
{
controller.Messenger.Register(MessageTypes.MSG_CUSTOMER_SAVED,
new Action<Message>(RefreshList));
RefreshList();
}
#endregion
#region Private Methods
private void RefreshList(Message message)
{
RefreshList();
message.HandledStatus = MessageHandledStatus.HandledContinue;
}
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
{
[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;
}
}
[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
{
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
{
public partial class CustomerController : BaseController, ICustomerController
{
private static ICustomerService CustomerService;
#region Constructors
private CustomerController()
{
}
public CustomerController(ICustomerService customerService)
{
CustomerService = customerService;
}
#endregion
#region Public Methods
public void Start()
{
ShowViewCustomerSelection();
}
public void EditCustomer(int customerId, BaseViewModel daddy = null)
{
}
public void CustomerSelectedForEdit(CustomerListItemViewData data,
BaseViewModel daddy = null)
{
if (data != null && data.CustomerId != null)
{
NotificationResult result = Messenger.NotifyColleagues
(MessageTypes.MSG_CUSTOMER_SELECTED_FOR_EDIT, data);
if (result == NotificationResult.MessageNotRegistered ||
result == NotificationResult.MessageRegisteredNotHandled)
{
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
{
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;
}
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
{
#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)
{
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 main window 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
{
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.