Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

A nice approach for a LOB WPF application

Rate me:
Please Sign up or sign in to vote.
4.89/5 (39 votes)
22 Feb 2009CPOL13 min read 117K   2.1K   93   34
An implementation of the MVVM Patterns + CommandModel within a WPF LOB application.

Image 1

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:

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

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

C#
Customer c = new Customer();

And the setting of certain properties within the Customer object will be the same as well, like:

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

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

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

  1. List the already registered ones.
  2. 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:

Image 2

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:

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

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

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

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

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

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

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

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

Image003-CollectionView.jpg

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

C#
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
{
    /// <summary>
    /// Interaction logic for CustomerListView.xaml
    /// </summary>
    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:

Image004-WorkspaceView.jpg

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

Image005-TabControlProperties.jpg

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:

C#
public class DTSelector : DataTemplateSelector
{
    private DataTemplate _CollectionTemplate, _EditableTemplate;

    // NOT CLOSABLE HEADER AND HAS A CustomerListView AS CHILD
    public DataTemplate CollectionTemplate
    {
        get { return _CollectionTemplate; }
        set { _CollectionTemplate = value; }
    }

    // CLOSABLE HEADER AND HAS A CustomerWorkspaceView AS CHILD
    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:

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

Image007-FinalCollectionView.jpg

Note the disabled buttons Edit and Delete.

On pressing New, a new TabItem with the NewCommand is called:

Image008-NewExecuted.jpg

showing up a new TabItem like this:

Image009-NewCommandFinished.jpg

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:

Image004-WorkspaceView.jpg

When a customer is selected, the Delete and Edit buttons are enabled:

Image010-EditDeleteCanExecuteTrue.jpg

Once Edit is clicked, it shows the customer in edit mode:

Image011-EditCurrentClicked.jpg

This is what it looks like in edit mode:

Image012-InEditMode.jpg

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.

License

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


Written By
Architect
Brazil Brazil
Senior Software Architect from Brazil.

Comments and Discussions

 
QuestionGetting Error Pin
Nivas Maran23-Mar-15 19:42
Nivas Maran23-Mar-15 19:42 
GeneralMy vote of 5 Pin
Mendigolhes11-Nov-10 2:09
Mendigolhes11-Nov-10 2:09 
GeneralMy vote of 5 Pin
blackr2d6-Jul-10 2:58
blackr2d6-Jul-10 2:58 
GeneralProblem: CustomerView Model [modified] Pin
treesprite3-Dec-09 14:02
professionaltreesprite3-Dec-09 14:02 
GeneralError:1 Could not create an instance of type 'CustomerViewModel'. Pin
metnik1-Sep-09 23:33
metnik1-Sep-09 23:33 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
Raul Mainardi Neto2-Sep-09 2:27
Raul Mainardi Neto2-Sep-09 2:27 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
metnik2-Sep-09 20:18
metnik2-Sep-09 20:18 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
Raul Mainardi Neto3-Sep-09 2:47
Raul Mainardi Neto3-Sep-09 2:47 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
metnik4-Sep-09 15:21
metnik4-Sep-09 15:21 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
Raul Mainardi Neto5-Sep-09 11:08
Raul Mainardi Neto5-Sep-09 11:08 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
metnik6-Sep-09 3:47
metnik6-Sep-09 3:47 
GeneralRe: Error:1 Could not create an instance of type 'CustomerViewModel'. Pin
Raul Mainardi Neto6-Sep-09 10:46
Raul Mainardi Neto6-Sep-09 10:46 
GeneralFire Edit Command on DoubleClick CustomerListView Pin
GregorSchiller17-Aug-09 2:25
GregorSchiller17-Aug-09 2:25 
GeneralRe: Fire Edit Command on DoubleClick CustomerListView Pin
Raul Mainardi Neto17-Aug-09 3:00
Raul Mainardi Neto17-Aug-09 3:00 
QuestionVery very good, but who does it work with Odyssey RibbonWindow Pin
GregorSchiller15-Jul-09 10:06
GregorSchiller15-Jul-09 10:06 
Very, very great article. But I got a problem, if I try to use it with Odyssey RibbonWindow.

I modifyed the control

from

public partial class CustomerView : Window

to

public partial class CustomerView : RibbonWindow

and the XAML

to

<odc:RibbonWindow
x:Class="PasswordSafe.UserControls.CustomerView"


<odc:RibbonWindow.DataContext>
<ViewModel:CustomerViewModel />
</odc:RibbonWindow.DataContext>

<odc:RibbonWindow.Resources>

. . .

</odc:RibbonWindow.Resources>

</odc:RibbonWindow>

All works fine, but the CustomerListView shows the text

Mainardi.Model.ObjectMapping.Individual

instead of the database information.

Perhaps there is somebody who knows what I’m making wrong. I would be very glad for a tip.

Thanks Greg
AnswerRe: Very very good, but who does it work with Odyssey RibbonWindow Pin
Raul Mainardi Neto18-Jul-09 16:41
Raul Mainardi Neto18-Jul-09 16:41 
GeneralRe: Very very good, but who does it work with Odyssey RibbonWindow Pin
GregorSchiller17-Aug-09 2:18
GregorSchiller17-Aug-09 2:18 
GeneralWorking with Multiple ViewModels Pin
P P Vilsad8-Jun-09 0:24
P P Vilsad8-Jun-09 0:24 
GeneralRe: Working with Multiple ViewModels Pin
Raul Mainardi Neto8-Jun-09 17:06
Raul Mainardi Neto8-Jun-09 17:06 
GeneralThanks Pin
P P Vilsad31-May-09 19:25
P P Vilsad31-May-09 19:25 
QuestionReplace xmlns:ViewModel-URI Pin
ChrDressler5-Mar-09 10:54
ChrDressler5-Mar-09 10:54 
AnswerRe: Replace xmlns:ViewModel-URI Pin
Raul Mainardi Neto5-Mar-09 23:57
Raul Mainardi Neto5-Mar-09 23:57 
AnswerRe: Replace xmlns:ViewModel-URI Pin
ChrDressler6-Mar-09 6:01
ChrDressler6-Mar-09 6:01 
GeneralRe: Replace xmlns:ViewModel-URI Pin
Raul Mainardi Neto6-Mar-09 7:06
Raul Mainardi Neto6-Mar-09 7:06 
GeneralVery good... Pin
rudigrobler2-Mar-09 19:08
rudigrobler2-Mar-09 19:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.