Click here to Skip to main content
15,879,535 members
Articles / Desktop Programming / WPF

MVVM # Episode 3

Rate me:
Please Sign up or sign in to vote.
4.89/5 (19 votes)
13 Dec 2013CPOL12 min read 61.3K   1K   65   10
Using an extended MVVM pattern for real world LOB applications: Part 3

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 Customers 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

C#
namespace Model
{
    /// <summary>
    /// Summary information for a Customer
    /// As a 'cut down' version of Customer information, this class is used
    /// for lists of Customers, for example, to avoid having to get a complete
    /// Customer object
    /// </summary>
    public class CustomerListData
    {
        /// <summary>
        /// The unique Id assigned to this Customer in the Data Store
        /// </summary>
        public int? Id
        {
            get;
            set;
        }
        /// <summary>
        /// The Business name of the Customer
        /// </summary>
        public string Name
        {
            get;
            set;
        }

        /// <summary>
        /// Which State the Customer is in
        /// </summary>
        public string State
        {
            get;
            set;
        }
    }
}

Customer.cs

C#
namespace Model
{
    /// <summary>
    /// A Customer
    /// This inherits from the CustomerSummary class, which contains the basic Customer
    /// information provided in lists.
    /// In real implementations this class may use lazy loading to get transactions
    /// </summary>
    public class Customer : CustomerListData
    {
        /// <summary>
        /// The address of the customer.
        /// </summary>
        public string Address
        {
            get;
            set;
        }
        public string Suburb
        {
            get;
            set;
        }
        public string PostCode
        {
            get;
            set;
        }
        public string Phone
        {
            get;
            set;
        }
        public string Email
        {
            get;
            set;
        }
    }
}

I've stripped my Model class down to the bare bones for this article.

Services

Now we have some data objects, we need some way to retrieve and store them into our data store (be that a database, a text file, some XML files, a web service or whatever). So in the Services project, we need to create our Service Interface ...

IcustomerService.cs

C#
using System.Collections.Generic;
using Model;

namespace Service
{
    public interface ICustomerService
    {
        /// <summary>
        /// Return the Customer for the given id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        Customer GetCustomer(int id);
        /// <summary>
        /// Return a list of Customers' List Data filtered by State
        /// </summary>
        /// <returns></returns>
        List<CustomerListData> GetListOfCustomers(string stateFilter);

        /// <summary>
        /// Update a customer in the data store
        /// </summary>
        /// <param name="?"></param>
        void UpdateCustomer(Customer data);
    }
}

That will give us what we need for this application. So let's implement the Interface...

CustomerService.cs

C#
using System.Collections.Generic;
using Model;

namespace Service
{
    /// <summary>
    /// Provide services for retrieving and storing Customer information
    /// </summary>
    public class CustomerService : ICustomerService
    {
        /// <summary>
        /// A fake database implementation so we can store and retrieve customers
        /// </summary>
        private List<Customer> fakeDatabaseOfCustomers;

        public CustomerService()
        {
            // Add some data to our database
            fakeDatabaseOfCustomers = new List<Customer>();
            fakeDatabaseOfCustomers.Add(DummyCustomerData(1));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(2));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(3));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(4));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(5));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(6));
            fakeDatabaseOfCustomers.Add(DummyCustomerData(7));
        }
        /// <summary>
        /// Make a fake customer
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        private Customer DummyCustomerData(int id)
        {
            Customer customer = new Customer()
            {
                Id = id,
                Address = id.ToString() + " High Street",
                Suburb = "Nether Wallop",
                State = (id % 2) == 0 ? "Qld" : "NSW",
                Email = "Customer" + id.ToString() + "@BigFoot.Com",
                Phone = "07 3333 4444",
                Name = "Customer Number " + id.ToString()
            };

            return customer;
        }

        #region ICustomerService

        public Customer GetCustomer(int id)
        {
            return fakeDatabaseOfCustomers[id - 1];
        }

        public List<CustomerListData> GetListOfCustomers(string stateFilter)
        {
            List<CustomerListData> list = new List<CustomerListData>();
            foreach (var item in fakeDatabaseOfCustomers)
            {
                if (string.IsNullOrEmpty(stateFilter) || 
            item.State.ToUpper() == stateFilter.ToUpper())
                {
                    list.Add(new CustomerListData()
                    {
                        Id = item.Id,
                        Name = item.Name,
                        State = item.State
                    });
                }
            }
            return list;
        }

        public void UpdateCustomer(Customer data)
        {
            fakeDatabaseOfCustomers[(int)data.Id - 1] = data;
        }

        #endregion
    }
}

You'll see that the CustomerService class creates a 'fake' collection (fakeDatabaseOfCustomer) - no saving to any repository - but it serves the purposes for this demonstration. It's just there to help us get an application running with some data without having to populate a database - don't confuse it with design-time data which will be discussed later.

Don't forget to add a reference to the Models project!

ViewData

So, we have our data objects (in Models) and we have some services to store and retrieve the data. Now we need to think about the actual presentation. Remember that our ViewData need to have Observable properties for each of the properties the user needs to see.

We'll think first about what data is going to be used.

  • We'll need a class containing all of the editable properties of the Customer (CustomerEditViewData)
  • We'll need a class containing a minimal set of data for displaying lists of Customer information (CustomerListItemViewData)
  • We'll need a class containing a collection of CustomerListItemViewData so we can show a list (CustomerSelectionViewData)

These classes have a more or less 1-1 relationship with the ViewModels (and thus the Views) we'll be creating. In this case, there's also a (more or less)1-1 relationship between the ViewData and the Model objects - but that's not necessarily going to be the case in larger more complex applications.

I say 'more or less' because while the CustomerEditViewData maps to the CustomerEditViewModel, and the CustomerSelectionViewData maps to the CustomerSelectionViewModel, CustomerSelectionViewData is really just a collection of CustomerListItemViewData - which doesn't have its own ViewModel at all.

These classes all live in the ViewModel's project, in their own sub folder, ViewData.

They all inherit from BaseViewData, and use their base's RaisePropertyChanged method to notify the View(s) of any changes.

So, let's start off with the CustomerListItemViewData:

C#
using System.Windows;

namespace ViewModels
{
    /// <summary>
    /// A minimalist view of a Customer - for displaying in lists
    /// </summary>
    public class CustomerListItemViewData : BaseViewData
    {
        #region Private Fields
        private string customerName;
        private int? customerId;
        private string state;
        #endregion
        #region Observable Properties
        /// <summary>
        /// The Id of the Customer represented by this item.
        /// </summary>
        public int? CustomerId
        {
            get
            {
                return customerId;
            }
            set
            {
                if (value != customerId)
                {
                    customerId = value;
                    base.RaisePropertyChanged("CustomerId");
                }
            }
        }
        public string CustomerName
        {
            get
            {
                return customerName;
            }
            set
            {
                if (value != customerName)
                {
                    customerName = value;
                    base.RaisePropertyChanged("CustomerName");
                }
            }
        }
        public string State
        {
            get
            {
                return state;
            }
            set
            {
                if (value != state)
                {
                    state = value;
                    base.RaisePropertyChanged("State");
                }
            }
        }

        #endregion
        #region Constructor
        #endregion
    }
}

That's all fairly simple - so let's move on to the CustomerSelectionViewData, which, as we've said, is just a collection of CustomerListItemViewData using System.Collections.ObjectModel.

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

C#
namespace ViewModels
{
    /// <summary>
    /// Editable Customer Info
    /// </summary>
    public class CustomerEditViewData : BaseViewData
    {
        #region Private Fields
        private string name;
        private int? customerId;
        private string address;
        private string suburb;
        private string email;
        private string postCode;
        private string phone;
        private string state;
        #endregion

        #region Observable Properties
        public int? CustomerId
        {
            get
            {
                return customerId;
            }
            set
            {
                if (value != customerId)
                {
                    customerId = value;
                    base.RaisePropertyChanged("CustomerId");
                }
            }
        }
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                if (value != name)
                {
                    name = value;
                    base.RaisePropertyChanged("Name");
                }
            }
        }
        public string Address
        {
            get
            {
                return address;
            }
            set
            {
                if (value != address)
                {
                    address = value;
                    base.RaisePropertyChanged("Address");
                }
            }
        }
        public string Suburb
        {
            get
            {
                return suburb;
            }
            set
            {
                if (suburb != value)
                {
                    suburb = value;
                    base.RaisePropertyChanged("Suburb");
                }
            }
        }
        public string Email
        {
            get
            {
                return email;
            }
            set
            {
                if (email != value)
                {
                    email = value;
                    base.RaisePropertyChanged("Email");
                }
            }
        }
        public string PostCode
        {
            get
            {
                return postCode;
            }
            set
            {
                if (postCode != value)
                {
                    postCode = value;
                    base.RaisePropertyChanged("PostCode");
                }
            }
        }
        public string Phone
        {
            get
            {
                return phone;
            }
            set
            {
                if (phone != value)
                {
                    phone = value;
                    base.RaisePropertyChanged("Phone");
                }
            }
        }
        public string State
        {
            get
            {
                return state;
            }
            set
            {
                if (state != value)
                {
                    state = value;
                    base.RaisePropertyChanged("State");
                }
            }
        }
        #endregion

        #region Constructor

        #endregion
    }
}

Let's finish there with the ViewData and move over to our Controller.

ICustomerController

We need now to look at our CustomerController. What functionality do we need it to perform?

  1. Provide a CustomerSelectionViewData object to be shown to the user
  2. Handle the selection of a Customer
  3. Handle the request to edit a Customer
  4. 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!

C#
namespace ViewModels
{
    public interface ICustomerController : IController
    {
        /// <summary>
        /// Return a collection of Customer information to be displayed in a list
        /// </summary>
        /// <returns>A collection of Customers</returns>
        CustomerListViewData GetCustomerSelectionViewData(string stateFilter);
        /// <summary>
        /// Do whatever needs to be done when a Customer is selected (i.e. edit it)
        /// </summary>
        /// <param name="customerId"></param>
        void CustomerSelectedForEdit(CustomerListItemViewData data, BaseViewModel daddy);
        /// <summary>
        /// Edit this customer Id
        /// </summary>
        /// <param name="customerId"></param>
        void EditCustomer(int customerId, BaseViewModel daddy);
        /// <summary>
        /// Update Customer data in the repository
        /// </summary>
        /// <param name="data"></param>
        void UpdateCustomer(CustomerEditViewData data);
    }
}

ViewModel

Well, now we have the basics, let's start thinking about our first ViewModel. At last!

The CustomerSelectionViewModel needs to display a list of customers, and allow the user to select one. Initially, that's it, so let's write the CustomerSelectionViewModel class.

CustomerSelectionViewModel

C#
using System;
using System.Windows.Input;
using Messengers;

namespace ViewModels
{
    /// <summary>
    /// This view model expects the user to be able to select from a list of
    /// Customers, sending a message when one is selected.
    /// On selection, the Controller will be asked to show 
    /// the details of the selected Customer
    /// </summary>
    public class CustomerSelectionViewModel : BaseViewModel
    {

        #region Properties

        /// <summary>
        /// Just to save us casting the base class's 
        /// IController to ICustomerController all the time...
        /// </summary>
        private ICustomerController CustomerController
        {
            get
            {
                return (ICustomerController)Controller;
            }
        }

        #region Observable Properties

        private CustomerListItemViewData selectedItem;
        public CustomerListItemViewData SelectedItem
        {
            get
            {
                return selectedItem;
            }
            set
            {
                if (value != selectedItem)
                {
                    selectedItem = value;
                    RaisePropertyChanged("SelectedItem");
                }
            }
        }
        #endregion
        #endregion

        #region Commands
        #region Command Relays
        private RelayCommand userSelectedItemCommand;
        public ICommand UserSelectedItemCommand
        {
            get
            {
                return userSelectedItemCommand ?? 
                (userSelectedItemCommand = new RelayCommand(() => 
                    ObeyUserSelectedItemCommand()));
            }
        }
        #endregion
        #region Command Handlers
        private void ObeyUserSelectedItemCommand()
        {
            CustomerController.CustomerSelectedForEdit
                (this.SelectedItem, this);
        }
        #endregion
        #endregion

        #region Constructors
        /// <summary>
        /// Required to allow our DesignTime version to be instantiated
        /// </summary>
        protected CustomerSelectionViewModel()
        {
        }

        public CustomerSelectionViewModel(ICustomerController controller, 
            string stateFilter = "")
            : this(controller, null, stateFilter)
        {

        }

        /// <summary>
        /// Use the base class to store the controller 
        /// and set the Data Context of the view (view)
        /// Initialise any data that needs initialising
        /// </summary>
        /// <param name="controller"></param>
        /// <param name="view"></param>
        public CustomerSelectionViewModel(ICustomerController controller, 
            IView view, string stateFilter = "")
            : base(controller, view)
        {
            controller.Messenger.Register(MessageTypes.MSG_CUSTOMER_SAVED, 
                new Action<Message>(RefreshList));
            // Leave it for half a second before filtering on State
            RefreshList();
        }
        #endregion

        #region Private Methods

        private void RefreshList(Message message)
        {
            RefreshList();
            message.HandledStatus = MessageHandledStatus.HandledContinue;
        }

        /// <summary>
        /// Ask for an updated list of customers based on the filter
        /// </summary>
        private void RefreshList()
        {
            ViewData = 
            CustomerController.GetCustomerSelectionViewData("");
        }

        #endregion
    }
}

A few things to note in the CustomerSelectionViewModel...

First, to save me having to cast the BaseViewModel's Controller property to ICustomerController all the time, I've added a private property CustomerController. Much like the flushable toilet, it's just a convenience thing.

We have an Observable property of SelectedItem. This is the CustomerListItemViewData that is currently selected from the list presented to the user - so whatever binds to this property needs to tell us via that binding what is currently selected.

We have a UserSelectedItemCommand. As its name suggests, this is the Command that our View will send when the user has selected an item. It is up to the designer whether this is on the press of a button, or as each row on a grid list is clicked, or via some quirky user interface dreamed up over several pints of Guinness.

There is a parameterless constructor. This is required because I want to be able to provide design-time support for data - and design time support demands a parameterless constructor. Every ViewModel requires a Controller, so the other constructors require an ICustomerController parameter. I'm also allowing the constructor to (optionally) provide a State Filter. This isn't implemented in the listing above, but the aim is to allow a CustomerSelectionViewModel to be created, filtering the customers to only show those from a particular state - perhaps the state the operator is in.

The other constructor allows us to create the ViewModel without an injected View. But what good is a ViewModel without a View? Well, no good at all - but good question, it shows you're paying attention! The non-view variant of our constructor will allow us to instantiate a ViewModel for a View that is created at design time - for example, if the designer decides that the Customer selection and edit should both appear together on a 'parent' view, she can design it like that, and we'll need to create a parent ViewModel that instantiates the CustomerSelectionViewModel and assigns it to the DataContext of the design-time created view.

Notice that the constructor also registers our ViewModel to receive messages of type MSG_CUSTOMER_SAVED and, when it does so, it uses the RefreshList method to ask the Controller to provide an updated list of Customers. This way, whenever a customer is updated somewhere, our list will reflect any changes.

When it's instantiated, our ViewModel also calls its Refresh() method to get the initial data for display. I sometimes struggle with the "right" way to do this - should the ViewModel get the data when it's instantiated, or should the Controller feed in the data? There's pros and cons for each school of thought, and in this case I chose to use the 'pull' method - where the ViewModel pulls the data from the Controller - rather than the 'push' method - where the Controller pushes the data into the ViewModel.

View

We really should think about creating a View now - so we not only have something to see, but also so our highly paid designer can have something to do!

Create a new WPF UserControl in the Views project, called CustomerSelectionView. You'll need to change the base class in the .cs file to BaseView (from UserControl). Then, do your design. Here's my XAML. (I'm not a designer!)

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

C#
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace Views
{
    /*
     * This source file contains all the converters used.
     */

    /// <summary>
    /// Returns false if the object is null, true otherwise.
    /// handy for using when something needs to be enabled or disabled depending on
    /// whether a value has been selected from a list.
    /// </summary>
    [ValueConversion(typeof(object), typeof(bool))]
    public class NullToFalseBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
            object parameter, CultureInfo culture)
        {
            return (value != null);
        }

        public object ConvertBack(object value, Type targetType, 
            object parameter, CultureInfo culture)
        {
            return null;
        }
    }

    /// <summary>
    /// Returns false if the object is null, true otherwise.
    /// handy for using when something needs to be enabled or disabled depending on
    /// whether a value has been selected from a list.
    /// </summary>
    [ValueConversion(typeof(object), typeof(Visibility))]
    public class NullToHiddenVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, 
            object parameter, CultureInfo culture)
        {
            if (value == null)
            {
                return Visibility.Hidden;
            }
            else
            {
                return Visibility.Visible;
            }
        }

        public object ConvertBack(object value, Type targetType, 
            object parameter, CultureInfo culture)
        {
            return null;
        }
    }
}

When these two converters are written, we're left with a single compile error. The line:

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

C#
using System.Collections.ObjectModel;
using ViewModels;

namespace Views
{
    /// <summary>
    /// This class allows us to see design time customers. Blendability R Us
    /// </summary>
    public class DesignTimeCustomerSelectionViewModel : CustomerSelectionViewModel
    {
        public DesignTimeCustomerSelectionViewModel()
        {
            ViewData = new CustomerListViewData();
            var customers = new ObservableCollection<CustomerListItemViewData>();

            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 1,
                CustomerName = "First Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 2,
                CustomerName = "2nd Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 3,
                CustomerName = "Third Customer",
                State = "NSW"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 4,
                CustomerName = "Fourth Customer",
                State = "SA"
            });

            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 1,
                CustomerName = "First Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 2,
                CustomerName = "2nd Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 3,
                CustomerName = "Third Customer",
                State = "NSW"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 4,
                CustomerName = "Fourth Customer",
                State = "SA"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 1,
                CustomerName = "First Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 2,
                CustomerName = "2nd Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 3,
                CustomerName = "Third Customer",
                State = "NSW"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 4,
                CustomerName = "Fourth Customer",
                State = "SA"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 1,
                CustomerName = "First Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 2,
                CustomerName = "2nd Customer",
                State = "Qld"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 3,
                CustomerName = "Third Customer",
                State = "NSW"
            });
            customers.Add(new CustomerListItemViewData()
            {
                CustomerId = 4,
                CustomerName = "Fourth Customer",
                State = "SA"
            });

            ((CustomerListViewData)ViewData).Customers = customers;
        }
    }
}

The class itself inherits from the 'real' CustomerSelectionViewModel, and just has a constructor that creates some dummy data for the designer to use.

Once this is written, rebuild and you should see the design time data, at design time! As Designed!

Design time data shown at design time

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. Wink | ;)

CustomerController.cs

C#
using Messengers;
using Service;
using ViewModels;
using Views;

namespace Controllers
{
    /// <summary>
    /// The controller 'is' the application.
    /// Everything is controlled by this :
    /// it instantiates Views and ViewModels
    /// it retrieves and stores customers via services
    ///
    /// But it does all this only in response to requests
    /// made by the ViewModels.
    ///
    /// e.g. a ViewModel may request a list of customers
    /// e.g. a ViewModel may want to save changes to a customer
    ///
    /// set up as a partial class for convenience
    /// </summary>
    public partial class CustomerController : BaseController, ICustomerController
    {
        private static ICustomerService CustomerService;

        #region Constructors
        /// <summary>
        /// Private constructor - we must pass a service to the constructor
        /// </summary>
        private CustomerController()
        {
        }

        /// <summary>
        /// The controller needs a reference to the service layer to enable it 
        /// to make service calls
        /// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService)
        {
            CustomerService = customerService;
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Main entry point of the Controller.
        /// Called once (from App.xaml.cs) this will initialise the application
        /// </summary>
        public void Start()
        {
            ShowViewCustomerSelection();
        }
        /// <summary>
        /// Edit the customer with the Id passed
        /// </summary>
        /// <param name="customerId">Id of the customer to be edited</param>
        /// <param name="daddy">The 'parent' ViewModel who will own the
        /// ViewModel that controls the Customer Edit</param>
        public void EditCustomer(int customerId, BaseViewModel daddy = null)
        {
            //BaseView view = GetCustomerEditView(customerId, daddy);
            //view.ShowInWindow(false, "Edit Customer");
        }

        /// <summary>
        /// A Customer has been selected to be edited
        /// </summary>
        /// <param name="data">The CustomerListItemViewData of the selected customer
        /// </param>
        /// <param name="daddy">The parent ViewModel</param>
        public void CustomerSelectedForEdit(CustomerListItemViewData data, 
        BaseViewModel daddy = null)
        {
            // Check in case we get a null sent to us
            if (data != null && data.CustomerId != null)
            {
                NotificationResult result = Messenger.NotifyColleagues
                (MessageTypes.MSG_CUSTOMER_SELECTED_FOR_EDIT, data);
                if (result == NotificationResult.MessageNotRegistered ||
                result == NotificationResult.MessageRegisteredNotHandled)
                {
                    // Nothing was out there that handled our message, 
          // so we'll do it ourselves!
                    EditCustomer((int)data.CustomerId, daddy);
                }
            }
        }
        #endregion
    }
}

The main CustomerController source will show a couple of build errors until we complete the other partial classes. notice also here I've commented out code in the EditCustomer method - as we haven't yet created the ViewModel or View to perform this function.

CustomerController_Dataretrieval.cs

C#
using System.Collections.ObjectModel;
using Messengers;
using Model;
using Service;
using ViewModels;

namespace Controllers
{
    public partial class CustomerController
    {
        /// <summary>
        /// Get a collection of Customers and return an Observable 
        /// collection of CustomerListItemViewData
        /// for display in a list.
        /// You could bypass this conversion if you wanted to present a 
        /// list of Customers by binding directly to
        /// the Customer object.
        /// </summary>
        /// <returns></returns>
        public CustomerListViewData GetCustomerSelectionViewData(string stateFilter)
        {
            CustomerListViewData vd = new CustomerListViewData();
            vd.Customers = new ObservableCollection<CustomerListItemViewData>();

            foreach (var customer in CustomerService.GetListOfCustomers(stateFilter))
            {
                vd.Customers.Add(new CustomerListItemViewData()
                {
                    CustomerId = (int)customer.Id,
                    CustomerName = customer.Name,
                    State = customer.State
                });
            }
            return vd;
        }

        /// <summary>
        /// Get the Edit View Data for the Customer Id specified
        /// </summary>
        /// <param name="customerId"></param>
        /// <returns></returns>
        public CustomerEditViewData GetCustomerEditViewData(int customerId)
        {
            var customer = CustomerService.GetCustomer(customerId);
            return new CustomerEditViewData()
            {
                CustomerId = customer.Id,
                Name = customer.Name,
                Address = customer.Address,
                Suburb = customer.Suburb,
                PostCode = customer.PostCode,
                State = customer.State,
                Phone = customer.Phone,
                Email = customer.Email
            };
        }

        public void UpdateCustomer(CustomerEditViewData data)
        {
            Customer item = new Customer()
            {
                Id = data.CustomerId,
                Address = data.Address,
                Name = data.Name,
                Suburb = data.Suburb,
                PostCode = data.PostCode,
                Email = data.Email,
                Phone = data.Phone,
                State = data.State

            };
            CustomerService.UpdateCustomer(item);
            Messenger.NotifyColleagues(MessageTypes.MSG_CUSTOMER_SAVED, data);
        }
    }
}

CustomerController_ViewManagement.cs

C#
using ViewModels;
using Views;
namespace Controllers
{
    public partial class CustomerController : ICustomerController
    {
        /// <summary>
        /// The ShowView methods are private. 
        /// A ViewModel may request some action to take place,
        /// but the Controller will decide whether this action will result 
        /// in some view being shown.
        /// e.g. clicking a 'Search' button on a form may result 
        /// in a Command being sent from the
        /// View (via binding) to the ViewModel; the Command handler 
        /// then asks the Controller to
        /// Search for whatever.
        /// The controller may (for example) use a service to 
        /// return a collection of objects. if there
        /// is only a single object, then it may return a single object 
        /// rather than popping up a search
        /// view only to have the User be presented with a single action 
        /// from which to select.
        /// </summary>

        #region Show Views

        private void ShowViewCustomerSelection()
        {
            CustomerSelectionView v = GetCustomerSelectionView();
            v.ShowInWindow(false);
        }
        #endregion

        #region Get Views
        private CustomerSelectionView GetCustomerSelectionView(BaseViewModel daddy = null)
        {
            CustomerSelectionView v = new CustomerSelectionView();
            CustomerSelectionViewModel vm = new CustomerSelectionViewModel(this, v);

            if (daddy != null)
            {
                daddy.ChildViewModels.Add(vm);
            }

            return v;
        }

        private BaseView GetCustomerEditView(int customerId, BaseViewModel daddy)
        {
            //CustomerEditView v = new CustomerEditView();
            //CustomerEditViewModel vm = new CustomerEditViewModel(this, v);

            //vm.ViewData = GetCustomerEditViewData(customerId);

            //if (daddy != null)
            //{
            //    daddy.ChildViewModels.Add(vm);
            //}

            //return v;
            return new BaseView();
        }

        #endregion
    }
}

And again, I've fiddled the GetCustomerEditView method as we've not written the View or ViewModel yet.

Give that a build and it should be clean. Try to run it, though, and you will see an unhandled IO exception "cannot locate resource 'mainwindow.xaml'".

Fear not - this is expected. Remember we created a WPF application which expected us to use a 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'.

Change the App.Xaml property Build Action to be Page

Open up your App.xaml file in the Customermaintenance project, and change it to look like this...

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

C#
using System.Windows;
using Controllers;
using Service;
using System;

namespace CustomerMaintenance
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            CustomerController controller = new CustomerController(new CustomerService());
            controller.Start();
        }
    }
}

Well - what are you waiting for? Press F5!

The program runs, a form appears with the Customer selection on it, showing a list of customers.

Astute WPF programmers always check the Output window when they run an application. Just in case you are one of them, I will point out that, in fact, there is an error:

System.Windows.Data Error: 40 : BindingExpression path error: 'StateFilter' 
property not found on 'object' ''CustomerSelectionViewModel' (HashCode=13304725)'. 
BindingExpression:Path=StateFilter; DataItem='CustomerSelectionViewModel' 
(HashCode=13304725); target element is 'TextBox' (Name=''); 
target property is 'Text' (type 'String') 

That's just because I've left the StateFilter TextBox on the View but omitted any property in the ViewModel to actually handle it.

But let's not dwell on the negatives, put on your party frock and celebrate - we've a working MVVM# application!

Next time, we'll add the filtering, and create CustomerEditViewModel and associated View so we'll end up with a small, but functional, application.

License

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


Written By
Software Developer (Senior) Devo
Australia Australia
Software developer par excellence,sometime artist, teacher, musician, husband, father and half-life 2 player (in no particular order either of preference or ability)
Started programming aged about 16 on a Commodore Pet.
Self-taught 6500 assembler - wrote Missile Command on the Pet (impressive, if I say so myself, on a text-only screen!)
Progressed to BBC Micro - wrote a number of prize-winning programs - including the best graphics application in one line of basic (it drew 6 multicoloured spheres viewed in perspective)
Trained with the MET Police as a COBOL programmer
Wrote platform game PooperPig which was top of the Ceefax Charts for a while in the UK
Did a number of software dev roles in COBOL
Progressed to Atari ST - learned 68000 assembler & write masked sprite engine.
Worked at Atari ST User magazine as Technical Editor - and was editor of Atari ST World for a while.
Moved on to IBM Mid range for work - working as team leader then project manager
Emigrated to Aus.
Learned RPG programming on the job (by having frequent coffee breaks with the wife!!)
Moved around a few RPG sites
Wrote for PC User magazine - was Shareware Magazine editor for a while.
Organised the first large-scale usage of the Internet in Australia through PC User magazine.
Moved from RPG to Delphi 1
Developed large applications in Delphi before moving on to VB .Net and C#
Became I.T. Manager - realised how boring paper pushing can be
And now I pretty much do .Net development in the daytime, while redeveloping PooperPig for the mobile market at night.

Comments and Discussions

 
QuestionController, View and ViewModel Pin
tab87vn5-Jan-16 2:37
tab87vn5-Jan-16 2:37 
AnswerRe: Controller, View and ViewModel Pin
_Maxxx_29-Mar-17 2:25
professional_Maxxx_29-Mar-17 2:25 
QuestionA quick question Pin
DataDude26-Apr-13 17:49
DataDude26-Apr-13 17:49 
AnswerRe: A quick question Pin
_Maxxx_26-Apr-13 19:35
professional_Maxxx_26-Apr-13 19:35 
QuestionDocking a view in another window Pin
BRShroyer9-Jan-13 5:15
BRShroyer9-Jan-13 5:15 
AnswerRe: Docking a view in another window Pin
_Maxxx_14-Jan-13 20:08
professional_Maxxx_14-Jan-13 20:08 
GeneralRe: Docking a view in another window Pin
BRShroyer15-Jan-13 1:58
BRShroyer15-Jan-13 1:58 
GeneralRe: Docking a view in another window Pin
_Maxxx_16-Jan-13 18:51
professional_Maxxx_16-Jan-13 18:51 
GeneralMy vote of 3 Pin
shiggin30-Mar-11 22:31
shiggin30-Mar-11 22:31 
GeneralRe: My vote of 3 Pin
_Maxxx_31-Mar-11 12:42
professional_Maxxx_31-Mar-11 12:42 

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.