Introduction
I have been wanting to rite an article of my own for quite some time now, and I was working on a WPF LOB application on a Silverlight Team Management System in my non-working hours. I had a couple of doubts regarding my implementations, and asked Sacha Barber, a CodeProject WPF author about what to do, and you know what? He actually congratulated me because it was well made. I was stunned and pleased by his complement, and thought “if he thinks it is good, maybe I can make an article of it…”, so there you go. My first article is about a way to use the MVVM + Command Model within a WPF LOB application.
So I like to thank Sacha for his aid on my quest for a better application, and also the legion of great developers like him, and some others that I will reference in this article.
Where to start
Let’s start by defining what we are aiming to do, with a simple to do list.
Some of you may say: “Hey I already know how to do that. I don’t need an article to learn about that.” I ask for your patience because it is not about the application, rather how it is built.
Defining the Model
The Model, or DataModel as it’s called by some, is responsible for exposing data in ways that can be consumable by the View. For me, the DataModel is not the data itself, it’s not the database, XML, or anything that contains the data, but a wrapper for it, so we can expose it for the application use. I know this is pretty much common knowledge for most of the readers, but since I had some problems at my own place of work with other programmers who do not know about this approach, I feel I need to explain. Let’s analyze the following scenario:
We have an application that uses SQL Server 2005 Enterprise, and we have a new customer, but this customer has the license for Oracle 10i. Will you gently ask your new customer to acquire a SQL Server 2005 Enterprise license? Or will you redo your application because your concept of Model included the data itself? Neither. So the model contains only the mapping of the data, such as the entities from LINQ to SQL, or the tables of a Lightspeed Model. Using these two examples, we can understand it better. LINQ to SQL has its entities, so in our case, we may have something like this:
[Table(Name="Sales.Customer")]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
{
private static PropertyChangingEventArgs emptyChangingEventArgs =
new PropertyChangingEventArgs(String.Empty);
private int _CustomerID;
private System.Nullable<int> _TerritoryID;
private string _AccountNumber;
private char _CustomerType;
private System.Guid _rowguid;
private System.DateTime _ModifiedDate;
private EntitySet<customeraddress> _CustomerAddresses;
private EntityRef<individual> _Individual;
private EntityRef<salesterritory> _SalesTerritory;
#region Extensibility Method Definitions
partial void OnLoaded();
partial void OnValidate(System.Data.Linq.ChangeAction action);
partial void OnCreated();
partial void OnCustomerIDChanging(int value);
partial void OnCustomerIDChanged();
partial void OnTerritoryIDChanging(System.Nullable<int> value);
partial void OnTerritoryIDChanged();
partial void OnAccountNumberChanging(string value);
partial void OnAccountNumberChanged();
partial void OnCustomerTypeChanging(char value);
partial void OnCustomerTypeChanged();
partial void OnrowguidChanging(System.Guid value);
partial void OnrowguidChanged();
partial void OnModifiedDateChanging(System.DateTime value);
partial void OnModifiedDateChanged();
#endregion
public Customer()
{
this._CustomerAddresses = new EntitySet<customeraddress>(
new Action<customeraddress>(this.attach_CustomerAddresses),
new Action<customeraddress>(this.detach_CustomerAddresses));
this._Individual = default(EntityRef<individual>);
this._SalesTerritory = default(EntityRef<salesterritory>);
OnCreated();
}
[Column(Storage="_CustomerID", AutoSync=AutoSync.OnInsert,
DbType="Int NOT NULL IDENTITY",
IsPrimaryKey=true, IsDbGenerated=true)]
public int CustomerID
{
get
{
return this._CustomerID;
}
set
{
if ((this._CustomerID != value))
{
this.OnCustomerIDChanging(value);
this.SendPropertyChanging();
this._CustomerID = value;
this.SendPropertyChanged("CustomerID");
this.OnCustomerIDChanged();
}
}
}
[Column(Storage="_TerritoryID", DbType="Int")]
public System.Nullable<int> TerritoryID
{
get
{
return this._TerritoryID;
}
set
{
if ((this._TerritoryID != value))
{
if (this._SalesTerritory.HasLoadedOrAssignedValue)
{
throw new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException();
}
this.OnTerritoryIDChanging(value);
this.SendPropertyChanging();
this._TerritoryID = value;
this.SendPropertyChanged("TerritoryID");
this.OnTerritoryIDChanged();
}
}
}
[Column(Storage="_AccountNumber", AutoSync=AutoSync.Always,
DbType="VarChar(10) NOT NULL", CanBeNull=false,
IsDbGenerated=true, UpdateCheck=UpdateCheck.Never)]
public string AccountNumber
{
get
{
return this._AccountNumber;
}
set
{
if ((this._AccountNumber != value))
{
this.OnAccountNumberChanging(value);
this.SendPropertyChanging();
this._AccountNumber = value;
this.SendPropertyChanged("AccountNumber");
this.OnAccountNumberChanged();
}
}
}
[Column(Storage="_CustomerType", DbType="NChar(1) NOT NULL")]
public char CustomerType
{
get
{
return this._CustomerType;
}
set
{
if ((this._CustomerType != value))
{
this.OnCustomerTypeChanging(value);
this.SendPropertyChanging();
this._CustomerType = value;
this.SendPropertyChanged("CustomerType");
this.OnCustomerTypeChanged();
}
}
}
[Column(Storage="_rowguid", DbType="UniqueIdentifier NOT NULL")]
public System.Guid rowguid
{
get
{
return this._rowguid;
}
set
{
if ((this._rowguid != value))
{
this.OnrowguidChanging(value);
this.SendPropertyChanging();
this._rowguid = value;
this.SendPropertyChanged("rowguid");
this.OnrowguidChanged();
}
}
}
[Column(Storage="_ModifiedDate", DbType="DateTime NOT NULL")]
public System.DateTime ModifiedDate
{
get
{
return this._ModifiedDate;
}
set
{
if ((this._ModifiedDate != value))
{
this.OnModifiedDateChanging(value);
this.SendPropertyChanging();
this._ModifiedDate = value;
this.SendPropertyChanged("ModifiedDate");
this.OnModifiedDateChanged();
}
}
}
[Association(Name="Customer_CustomerAddress",
Storage="_CustomerAddresses",
ThisKey="CustomerID", OtherKey="CustomerID")]
public EntitySet<customeraddress> CustomerAddresses
{
get
{
return this._CustomerAddresses;
}
set
{
this._CustomerAddresses.Assign(value);
}
}
[Association(Name="Customer_Individual", Storage="_Individual",
ThisKey="CustomerID",
OtherKey="CustomerID", IsUnique=true, IsForeignKey=false)]
public Individual Individual
{
get
{
return this._Individual.Entity;
}
set
{
Individual previousValue = this._Individual.Entity;
if (((previousValue != value) ||
(this._Individual.HasLoadedOrAssignedValue == false)))
{
this.SendPropertyChanging();
if ((previousValue != null))
{
this._Individual.Entity = null;
previousValue.Customer = null;
}
this._Individual.Entity = value;
if ((value != null))
{
value.Customer = this;
}
this.SendPropertyChanged("Individual");
}
}
}
[Association(Name="SalesTerritory_Customer", Storage="_SalesTerritory",
ThisKey="TerritoryID", OtherKey="TerritoryID", IsForeignKey=true)]
public SalesTerritory SalesTerritory
{
get
{
return this._SalesTerritory.Entity;
}
set
{
SalesTerritory previousValue = this._SalesTerritory.Entity;
if (((previousValue != value) ||
(this._SalesTerritory.HasLoadedOrAssignedValue == false)))
{
this.SendPropertyChanging();
if ((previousValue != null))
{
this._SalesTerritory.Entity = null;
previousValue.Customers.Remove(this);
}
this._SalesTerritory.Entity = value;
if ((value != null))
{
value.Customers.Add(this);
this._TerritoryID = value.TerritoryID;
}
else
{
this._TerritoryID = default(Nullable<int>);
}
this.SendPropertyChanged("SalesTerritory");
}
}
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void SendPropertyChanging()
{
if ((this.PropertyChanging != null))
{
this.PropertyChanging(this, emptyChangingEventArgs);
}
}
protected virtual void SendPropertyChanged(String propertyName)
{
if ((this.PropertyChanged != null))
{
this.PropertyChanged(this,
new PropertyChangedEventArgs(propertyName));
}
}
private void attach_CustomerAddresses(CustomerAddress entity)
{
this.SendPropertyChanging();
entity.Customer = this;
}
private void detach_CustomerAddresses(CustomerAddress entity)
{
this.SendPropertyChanging();
entity.Customer = null;
}
}
This is a pretty common mapping, haven’t changed anything from the code generated by LINQ to SQL. The Lightspeed Model from Mindscape would be something like this:
[Serializable]
[System.CodeDom.Compiler.GeneratedCode("LightSpeedModelGenerator", "1.0.0.0")]
[Table(IdColumnName="CustomerID", Schema="Sales")]
public partial class Customer : Entity<int>
{
#region Fields
[ValidatePresence]
[ValidateLength(0, 10)]
private string _accountNumber;
[ValidatePresence]
[ValidateLength(0, 1)]
private string _customerType;
private System.DateTime _modifiedDate;
private System.Guid _rowguid;
private System.Nullable<int> _territoryId;
#endregion
#region Field attribute names
public const string AccountNumberField = "AccountNumber";
public const string CustomerTypeField = "CustomerType";
public const string ModifiedDateField = "ModifiedDate";
public const string RowguidField = "Rowguid";
public const string TerritoryIdField = "TerritoryId";
#endregion
#region Relationships
private readonly EntityCollection<customeraddress>
_customerAddresses = new EntityCollection<customeraddress>();
[ReverseAssociation("Customers")]
private readonly EntityHolder<salesterritory>
_territory = new EntityHolder<salesterritory>();
#endregion
#region Properties
public EntityCollection<customeraddress> CustomerAddresses
{
get { return Get(_customerAddresses); }
}
public SalesTerritory Territory
{
get { return Get(_territory); }
set { Set(_territory, value); }
}
public string AccountNumber
{
get { return Get(ref _accountNumber); }
set { Set(ref _accountNumber, value, "AccountNumber"); }
}
public string CustomerType
{
get { return Get(ref _customerType); }
set { Set(ref _customerType, value, "CustomerType"); }
}
public System.DateTime ModifiedDate
{
get { return Get(ref _modifiedDate); }
set { Set(ref _modifiedDate, value, "ModifiedDate"); }
}
public System.Guid Rowguid
{
get { return Get(ref _rowguid); }
set { Set(ref _rowguid, value, "Rowguid"); }
}
public System.Nullable<int> TerritoryId
{
get { return Get(ref _territoryId); }
set { Set(ref _territoryId, value, "TerritoryId"); }
}
#endregion
}
As you can see, this is the mapping of a set of CLR properties, so the way for creating a new instance of it, using LINQ to SQL or Lightspeed, is the same:
Customer c = new Customer();
And the setting of certain properties within the Customer
object will be the same as well, like:
c.ModifiedDate = System.DateTime.Now;
With the implementation of the INotifyPropertyChanged
and the INotifyPropertyChanging
interfaces, it is possible to track any changes to the entity, which allows TwoWay binding of the View. So, all we need to do to have a Model that does not need to be rebuilt for each data storage scenario, is create a BAL class for each of the entities, such as the CustomerBAL
class that may look like this:
For LINQ to SQL:
public class CustomerBAL
{
public CustomerBAL() { }
[MethodImpl(MethodImplOptions.Synchronized)]
public List<salesterritory> GetSalesTerritories()
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
return db.SalesTerritories.ToList();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public List<individual> GetAllCustomers()
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
return db.Individuals.ToList();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public List<individual> GetCustomersByTerritory(int TerritoryId)
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
return (from s in db.Individuals where
s.Customer.TerritoryID == TerritoryId select s).ToList();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public Individual GetCustomerById(int CustomerId)
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
return db.Individuals.Single(c => c.CustomerID == CustomerId);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool InsertCustomer(Individual c)
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
if (c.Contact.PasswordHash == null)
c.Contact.PasswordHash = "NOTHING YET";
if (c.Contact.PasswordSalt == null)
c.Contact.PasswordSalt = "NOTHING";
c.ModifiedDate = DateTime.Now;
c.Contact.ModifiedDate = DateTime.Now;
c.Customer.ModifiedDate = DateTime.Now;
db.Individuals.InsertOnSubmit(c);
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool UpdateCustomer(Individual c)
{
c.ModifiedDate = DateTime.Now;
c.Contact.ModifiedDate = DateTime.Now;
c.Customer.ModifiedDate = DateTime.Now;
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool DeleteCustomer(Individual c)
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
db.Individuals.DeleteOnSubmit(c);
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool DeleteCustomer(int id)
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
return DeleteCustomer(db.Individuals.Single(c => c.CustomerID == id));
}
[MethodImpl(MethodImplOptions.Synchronized)]
private bool SubmitChanges()
{
AdventureWorksDataContext db = AdventureWorksDataContext.Instance;
db.SubmitChanges(System.Data.Linq.ConflictMode.FailOnFirstConflict);
return true;
}
}
And for Mindscape Lightspeed:
public class CustomerBAL
{
public CustomerBAL() { }
[MethodImpl(MethodImplOptions.Synchronized)]
public List<customer> GetAllCustomers()
{
AdventureWorksUnitOfWork db = Repository.Instance;
return db.Customers.ToList();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public List<customer> GetCustomersByTerritory(int TerritoryId)
{
AdventureWorksUnitOfWork db = Repository.Instance;
return (from s in db.Customers where
s.TerritoryId == TerritoryId select s).ToList();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public Customer GetCustomerById(int CustomerId)
{
AdventureWorksUnitOfWork db = Repository.Instance;
return db.Customers.Single(c => c.Id == CustomerId);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool InsertCustomee(Customer c)
{
AdventureWorksUnitOfWork db = Repository.Instance;
db.Add(c);
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool UpdateCustomer(Customer c)
{
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool DeleteCustomer(Customer c)
{
AdventureWorksUnitOfWork db = Repository.Instance;
db.Remove(c);
return this.SubmitChanges();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool DeleteCustomer(int id)
{
AdventureWorksUnitOfWork db = Repository.Instance;
return DeleteCustomer(db.Customers.Single(c => c.Id == id));
}
[MethodImpl(MethodImplOptions.Synchronized)]
private bool SubmitChanges()
{
AdventureWorksUnitOfWork db = Repository.Instance;
db.SaveChanges();
return true;
}
}
As you can see, they are very similar, with some minor changes, and these changes will only be made in the BAL class, because the rest of the application will work the same.
For the purposes of this article, we will stick with using LINQ to SQL, despite my love for the TraceLogger of Lightspeed. I will not use third party components in this article.
What next?
This is where we split the team since the data model is now ready. If you know that a customer has a phone number for example, then you can build the UI, or you can work on the ViewModels. I usually go for the ViewModels, because they will have the commands that will be used on the windows, to not only authorize the operations that the user submits, but also check the permissions needed to perform the given action. For example, the user can register a customer, but can’t update the customer line-of-credit data, so he can not perform this action.
Since I am running the show here, we will go for the ViewModels.
ViewModel
Ah… the ViewModel, one of the best and trickier things ever crafted for the developer. Let’s start by defining it. A ViewModel is an abstraction of the View. The ViewModel is where you define the behavior that will be executed by the application, so everything that you want your application to do is going to be defined within a ViewModel. In this application, the expected behavior is that we can perform CRUD operations for the Customer table of the AdventureWorks database. For us to understand the ViewModel behavior, let’s go in parts:
ViewModel: Abstracts
Before reading Josh Smith's MSDN Magazine article, that can be read here, I always ended up using the ViewModel as a proxy class for the methods within the BAL objects on my models, but this article opened my eyes. There are a lot of things to do with the ViewModel that I didn’t even dream of. So special thanks goes out to Josh Smith for his amazing work.
The first thing I do now is to look at my model and see what is needed (duh... everyone does that), so let’s begin with the Customer. There are two things we need to do with our customer.
- List the already registered ones.
- Save a Customer and delete a Customer. I usually don’t put the cancel operation on this list because it’s needed everywhere when the data is changed. The Save a Customer operation can be either an Insert or an Update.
Here is our Class Diagram:
As you can see, we have four abstract classes, a ViewModelBase
class, and extending from that we have a BaseWorkspaceViewModel
(that is where we will handle the edit and insert operations), a BaseCollectionViewModel
that we will use to expose the list of existing object(s) and perform the call to the BaseWorkspaceViewModel
so we can edit an instance of the object (to be a little more precise, an individual customer), a CommandModel
class (more on this later), and a CustomerViewModel
that will be used as the DataContext of the window.
Now that we have the base types defined, it’s time for the real fun to start, let’s make this baby work for us.
ViewModel: the real deal
Our aim for this article is to have a TabControl
with its ItemsSource
bound to a Collection
of ViewModels (this will represent the list of Customer(s)), and that collection must have a CollectionViewModel
and a WorkspaceViewModel
. For this, we put an ObservableCollection
of ViewModelBase
within the CustomerViewModel
. More explanation about this later. Now, we want to centralize the operations on the CustomerViewModel
, so we will just put all the commands and the methods that we will use to execute the needed operation for the commands on this class, and add a reference to the CustomerViewModel
to the Workspace and the Collection ViewModels.
For us to have a more accurate data entry validation, we follow the foot steps of Josh Smith once more. If you haven’t read his work on meaningful validation error messages, I strongly recommend that you do. Here is the link: http://joshsmithonwpf.wordpress.com/2008/11/14/using-a-viewmodel-to-provide-meaningful-validation-error-messages/.
For this, we put this in our WorkspaceViewModel
:
public override string Error
{
get
{
return
CanSave ?
null :
"This form contains errors and must be fixed before recording.";
}
}
public override string this[string ColumnName]
{
get
{
string result = null;
if (_error == null)
_error = new string[6];
switch (ColumnName)
{
case "Territory":
if (Territory == null)
result = "A Sales Territory must be selected.";
_error[0] = result;
break;
case "EmailAddress":
result =
(DataContext as Mainardi.Model.ObjectMapping.Individual).Contact[ColumnName];
if (result == null)
if (!Validations.Validator.isEmail(EmailAddress))
result = "This is not a valid email address.";
_error[1] = result;
break;
case "FirstName":
result =
(DataContext as Mainardi.Model.ObjectMapping.Individual).Contact[ColumnName];
_error[2] = result;
break;
case "LastName":
result =
(DataContext as Mainardi.Model.ObjectMapping.Individual).Contact[ColumnName];
_error[3] = result;
break;
case "Phone":
result =
(DataContext as Mainardi.Model.ObjectMapping.Individual).Contact[ColumnName];
_error[4] = result;
break;
case "CustomerType":
int i = CustomerType;
if (i < 0 || i > 1)
result = "Customer type must be selected";
_error[5] = result;
break;
}
CanSave = Validations.Validator.ValidateFields(_error);
return result;
}
}
This validation is accomplished by using the IDataErrorInfo
interface. In this case, we are validating the Customer object using the validation rules you see above. For a better email validation idea, check the article Effective Email Address Validation by Vasudevan Deepak Kumar or any other validation that is too complex to be a part of the Model.
As I have said before, we will centralize the commands and operations in the CustomerViewModel
. For this, we have these properties:
private CommandModel _CancelCommand,
_EditCommand,
_NewCommand,
_SaveCommand,
_DeleteCommand;
public CommandModel NewCommand { get { return _NewCommand; } }
public CommandModel EditCommand { get { return _EditCommand; } }
public CommandModel CancelCommand { get { return _CancelCommand; } }
public CommandModel SaveCommand { get { return _SaveCommand; } }
public CommandModel DeleteCommand { get { return _DeleteCommand; } }
These properties have only the get
method, so we have to add to the constructor the creation of a new instance of every CommandModel
, like this:
public CustomerViewModel()
{
... REMOVED FOR CLARITY ...
_CancelCommand = new CustomerCancelCommand(this);
_DeleteCommand = new CustomerDeleteCommand(this);
_EditCommand = new CustomerEditCommand(this);
_NewCommand = new CustomerNewCommand(this);
_SaveCommand = new CustomerSaveCommand(this);
}
And the methods that will be called from the Command.Executed
methods:
internal void New()
{
... REMOVED FOR CLARITY ...
}
internal void Delete(object p)
{
... REMOVED FOR CLARITY ...
}
internal void Save(Mainardi.ViewModels.CustomerViewModels.
InternalViewModels.CustomerWorkspaceViewModel cvm)
{
... REMOVED FOR CLARITY ...
}
internal void Cancel(Mainardi.ViewModels.CustomerViewModels.
InternalViewModels.CustomerWorkspaceViewModel cvm)
{
... REMOVED FOR CLARITY ...
}
internal void Edit(Mainardi.Model.ObjectMapping.Individual c)
{
... REMOVED FOR CLARITY ...
}
For those who have paid attention to the code, there are classes there that we didn’t explain yet. This leads us to the next topic, and a very important one: Command Models.
Command Models
Dan Crevier, another genius, has made a nice implementation to encapsulate and consume Commands on the ViewModel, so you don’t actually need any CanExecute
or Executed
method on your view. This is important to separate the View from the logic, and make it easier for the application designers to do their stuff to make your application look nicer without messing with the code that controls the logic operations. You can read more about this on his blog: Dan Crevier’s Blog.
Since it was so well explained by Dan on his blog, I will just explain the basics.
The CommandModel class:
This is where we put the actual commands to work. It is an abstract class, it also has a RoutedCommand
CLR property and two methods - one virtual because we don’t want to implement the CanExecute
every time since there are situations where it has the e.CanExecute
(e
is an CanExecuteRoutedEventArgs
) with the value always true
.
The entire class is shown here:
namespace Mainardi.ViewModels.CommandBase
{
public abstract class CommandModel
{
private RoutedCommand _routedCommand;
public CommandModel()
{
_routedCommand = new RoutedCommand();
}
public RoutedCommand Command
{
get { return _routedCommand; }
}
[DebuggerStepThrough]
public virtual void CanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
e.Handled = true;
}
public abstract void Executed(object sender,
ExecutedRoutedEventArgs e);
}
}
Pretty simple, right? Just for the record, the [DebuggerStepThrough]
attribute is to allow the debug to not stop every time the CanExecute
method is called.
The CreateCommandBinding.Command AttachedProperty
This is a very nice part, and I particularly love it. For us to use the CommandBinding
without having to actually declare it on our View, we have this nice trick: this attached property has a PropertyChangedCallback
that calls a method:
private static void OnCommandInvalidated(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement)dependencyObject;
element.CommandBindings.Clear();
CommandModel commandModel = e.NewValue as CommandModel;
if (commandModel != null)
{
element.CommandBindings.Add(
new CommandBinding(commandModel.Command,
commandModel.Executed,
commandModel.CanExecute));
}
CommandManager.InvalidateRequerySuggested();
}
This simple method clears the CommandBinding
for the UIElement
and adds a new CommandBinding
(which has a Command (RoutedCommand
), the Executed
method (the abstract method on the CommandModel
class) and the CanExecute
method (the virtual method)) into the CommandBindings
object.
This allows us to bind the command to the UIElement.CommandBindings
and expose it so the View can consume it. All we have to do for this is add the CreateCommandBinding.Command
on the element that we want to use the command in, like ViewModels:CreateCommandBinding.Command="{Binding NewCommand}”
. Then, we set the CommandProperty
of the UIElement
like: Command="{Binding NewCommand.Command}"
and, if it is needed, we pass the CommandParameterProperty
.
And so, the use of this technique will result in a Button
XAML declaration like this:
<Button Content="New" Margin="3" Command="{Binding NewCommand.Command}"
ViewModels:CreateCommandBinding.Command="{Binding NewCommand}"/>
Now that we know how this works, we have to create the operations that will be bound for our window:
public class CustomerNewCommand : CommandBase.CommandModel
{
... REMOVED FOR CLARITY ...
}
public class CustomerEditCommand : CommandBase.CommandModel
{
... REMOVED FOR CLARITY ...
}
public class CustomerSaveCommand : CommandBase.CommandModel
{
... REMOVED FOR CLARITY ...
}
public class CustomerDeleteCommand : CommandBase.CommandModel
{
... REMOVED FOR CLARITY ...
}
public class CustomerCancelCommand : CommandBase.CommandModel
{
... REMOVED FOR CLARITY ...
}
This set of operations will cover all we need in this sample application. Of course, there could be a lot more commands depending on your scenario.
Let’s review what we did so far, so we can keep track of things:
- We have constructed the Model;
- Crafted the ViewModel for the View;
- Crafted the ViewModel for the Workspace;
- Crafted the ViewModel for the Collections;
- Crafted the needed Commands for the ViewModel.
Yeah, we have covered everything for the customer, except for the View. Let's head up to that.
View
Finally the View. All our previous work is now about to pay off as we craft a Window and a set of UserControls that will use bindings to bind to our ViewModels (which are an abstraction of a View after all). I don’t know about you, but my designers love Bindings, as they don’t have to mess around with so much code-behind.
The view is actually pretty simple:
For the CustomerCollectionViewModel
(which represents a list of Customer(s)), we have the following UserControl:
<UserControl
x:Class="MVVMArticle.Views.UserControls.CustomerListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="http://schemas.mainardi.com/WPF/MVVMArticle/PresentationLogic"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="New" Margin="3"
Command="{Binding NewCommand.Command}"
ViewModels:CreateCommandBinding.Command="{Binding NewCommand}"/>
<Button Content="Edit" Margin="3"
Command="{Binding EditCommand.Command}"
CommandParameter="{Binding ElementName=List, Path=SelectedItem, Mode=TwoWay}"
ViewModels:CreateCommandBinding.Command="{Binding EditCommand}"/>
<Button Content="Delete" Margin="3"
Command="{Binding DeleteCommand.Command}"
CommandParameter="{Binding ElementName=List, Path=SelectedItem, Mode=TwoWay}"
ViewModels:CreateCommandBinding.Command="{Binding DeleteCommand}"/>
</StackPanel>
<ListView Grid.Row="1" ItemsSource="{Binding List}" x:Name="List">
<ListView.View>
<GridView>
<GridViewColumn Header="Last Name"
DisplayMemberBinding="{Binding Contact.LastName}"/>
<GridViewColumn Header="First Name"
DisplayMemberBinding="{Binding Contact.FirstName}"/>
<GridViewColumn Header="E-mail"
DisplayMemberBinding="{Binding Contact.EmailAddress}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
And the following code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMArticle.Views.UserControls
{
public partial class CustomerListView : UserControl
{
public CustomerListView()
{
InitializeComponent();
}
}
}
As you can see, we have a clean code-behind, and since that was our target on implementing the MVVM + CommandModel, we pretty much did everything to make this work properly.
But let’s continue this for the CustomerWorkspaceViewModel
, which displays a single Customer and has buttons for CRUD operations. We have the following UserControl:
<UserControl
x:Class="MVVMArticle.Views.UserControls.CustomerWorkspaceView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="http://schemas.mainardi.com/WPF/MVVMArticle/PresentationLogic"
>
<Grid>
...
Removed for Clarity
...
<TextBlock
Grid.Column="0" Grid.Row="0"
Text="Title :" VerticalAlignment="Center"
HorizontalAlignment="Right" Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="0"
Margin="2" HorizontalAlignment="Left"
Width="60"
Text="{Binding Path=Title, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
Foreground="#FF000000"
VerticalContentAlignment="Center"/>
<TextBlock
Grid.Column="0" Grid.Row="1"
Text="First Name :"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="1"
Margin="2"
Text="{Binding Path=FirstName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
Foreground="#FF000000"
VerticalContentAlignment="Center"/>
<TextBlock
Grid.Column="0" Grid.Row="2" Text="Middle Name :"
VerticalAlignment="Center"
HorizontalAlignment="Right" Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="2" Margin="2"
Text="{Binding Path=MiddleName, Mode=TwoWay}"
Foreground="#FF000000" VerticalContentAlignment="Center"/>
<TextBlock
Grid.Column="0" Grid.Row="3" Text="Last Name :"
VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="3" Margin="2"
Text="{Binding Path=LastName, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
Foreground="#FF000000" VerticalContentAlignment="Center"/>
<TextBlock
Grid.Column="0"
Grid.Row="4" Text="Customer Type :"
VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="#FFFFFFFF"/>
<ComboBox
Grid.Column="1" Grid.Row="4" Margin="2"
SelectedIndex="{Binding Path=CustomerType,
Mode=TwoWay, ValidatesOnDataErrors=True}"
Foreground="#FF000000">
<ComboBoxItem
Content="Individual"/>
<ComboBoxItem
Content="Store"/>
</ComboBox>
<TextBlock
Grid.Column="0" Grid.Row="5" Text="Sales Territory :"
VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="#FFFFFFFF"/>
<ComboBox
Grid.Column="1" Grid.Row="5" Margin="2"
SelectedItem="{Binding Path=Territory, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
ItemsSource="{Binding Path=Territories}" Foreground="#FF000000">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Path=Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock
Grid.Column="0" Grid.Row="6" Text="Phone number :"
VerticalAlignment="Center" HorizontalAlignment="Right"
Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="6"
Margin="2" HorizontalAlignment="Left"
Width="120"
Text="{Binding Path=Phone, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}"
Foreground="#FF000000" VerticalContentAlignment="Center"/>
<TextBlock
Grid.Column="0" Grid.Row="7"
Text="E-mail :" VerticalAlignment="Center"
HorizontalAlignment="Right" Foreground="#FFFFFFFF"/>
<TextBox
Grid.Column="1" Grid.Row="7" Margin="2"
Text="{Binding Path=EmailAddress, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
Foreground="#FF000000" VerticalContentAlignment="Center"/>
</Grid>
</ScrollViewer>
<StackPanel Grid.Row="1" Orientation="Horizontal">
<Button Content="Save" Margin="3"
Command="{Binding Path=SaveCommand.Command}"
CommandParameter="{Binding}"
ViewModel:CreateCommandBinding.Command="{Binding Path=SaveCommand}"
Foreground="#FFFFFFFF" Width="60"/>
<Button Content="Cancel" Margin="3"
Command="{Binding Path=CancelCommand.Command}"
CommandParameter="{Binding}"
ViewModel:CreateCommandBinding.Command="{Binding Path=CancelCommand}"
Foreground="#FFFFFFFF" Width="60"/>
<Button Content="Delete" Margin="3"
Command="{Binding Path=DeleteCommand.Command}"
CommandParameter="{Binding Path=DataContext}"
ViewModel:CreateCommandBinding.Command="{Binding Path=DeleteCommand}"
Foreground="#FFFFFFFF" Width="60"/>
</StackPanel>
</Grid>
</UserControl>
Like the other user controls, it does not have anything in its code-behind file besides the constructor, and within it, the call to the InitializeComponent
method.
Article bonus: TabControl with a dynamic child using DataTemplateSelectors
Remember where I told that we will come back to the binding to the TabControl
ItemsSource
later? Here it is.
In my first attempt to build the Window, I used Google to try and find a closable TabItem
. I did find one, which looked good, worked well, but did not work when the TabItem
s had to be created dynamically within a TabControl
. I struggled with this issue for two days, and... didn’t find a solution. After this, I tried to look at the TabControl
class on http://msdn.microsoft.com/en-us/library/system.windows.controls.tabcontrol.aspx and found this:
Good. That’s what I needed. And, I recall reading an article about DataTemplateSelector
before. After Googling for it, I found this amazing article on the Dr.WPF blog. Unfortunately, he was using a list box, but there were things that I could use from his article. The DataTemplateSelector
is very well explained, so thanks for that Dr.WPF. After learning a little bit more about the TabControl
, I learned that it works a little differently from other controls, because it has two parts: the header and the content. We have to define the selector for both the header and the content. Since the logic for the selector is the same for both we use the same. The code for the selector is this:
public class DTSelector : DataTemplateSelector
{
private DataTemplate _CollectionTemplate, _EditableTemplate;
public DataTemplate CollectionTemplate
{
get { return _CollectionTemplate; }
set { _CollectionTemplate = value; }
}
public DataTemplate EditableTemplate
{
get { return _EditableTemplate; }
set { _EditableTemplate = value; }
}
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
if (item != null && item is Mainardi.ViewModels.VMBase.ViewModelBase)
{
if (item is Mainardi.ViewModels.VMBase.BaseCollectionViewModel)
{
return CollectionTemplate;
}
else if (item is Mainardi.ViewModels.VMBase.BaseWorkspaceViewModel)
{
return EditableTemplate;
}
}
throw new
NullReferenceException("Object is not an valid " +
"ViewModel for this implementation.");
}
}
Because of this, we need a different ViewModel for each type of TabItem
template, so I created CollectionViewModel
and the WorkspaceViewModel
. We can declare the template for the items within the TabControl
and let the DataTemplateSelector
decide what is needed in the View for the proper visualization of the desired feature. And for this to happen, all we have to do is create a two DataTemplateSelectors
, like:
...
Removed for Clarity
...
<DataTemplate x:Key="CollectionHeaderTemplate">
<TextBlock Text="{Binding DisplayName}"/>
</DataTemplate>
<DataTemplate x:Key="WorkspaceHeaderTemplate">
...
Removed for Clarity
...
</DataTemplate>
<DataTemplate x:Key="CollectionTemplate">
<MVVMArticle:CustomerListView />
</DataTemplate>
<DataTemplate x:Key="WorkspaceTemplate">
<MVVMArticle:CustomerWorkspaceView />
</DataTemplate>
<ViewModel:DTSelector x:Key="HeaderDataTemplateSelector"
CollectionTemplate="{StaticResource CollectionHeaderTemplate}"
EditableTemplate="{StaticResource WorkspaceHeaderTemplate}"/>
<ViewModel:DTSelector x:Key="ContentDataTemplateSelector"
CollectionTemplate="{StaticResource CollectionTemplate}"
EditableTemplate="{StaticResource WorkspaceTemplate}"/>
...
Removed for Clarity
...
<TabControl ItemsSource="{Binding Path=Collection}"
ItemTemplateSelector="{StaticResource HeaderDataTemplateSelector}"
ContentTemplateSelector="{DynamicResource ContentDataTemplateSelector}"/>
And again, nothing in the code-behind files besides the constructor and the InitializeComponent
method, that’s something pretty cool.
Here is how the application looks like when it is running. This is the CollectionViewModel
within the TabControl
:
Note the disabled buttons Edit and Delete.
On pressing New, a new TabItem
with the NewCommand
is called:
showing up a new TabItem
like this:
With its default values, the form is invalid. Thanks to the amazing IDataErrorInfo
, the red decorator is there to show which are the invalid fields on the form, and because of them the form cannot be saved:
When a customer is selected, the Delete and Edit buttons are enabled:
Once Edit is clicked, it shows the customer in edit mode:
This is what it looks like in edit mode:
I would also like to say a special thanks to Rudi Grobler for his GlassEffect
AttachedProperty trick that is used within this application. Rudi's work can be found here.
Points of concern
As you can see, the ViewModel can totally separate logic from visualization, so the design team and the development team can work together in a more efficient way.
Well, this is it for now. Hope you guys liked this and can put this to use.