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

MVVM # Episode 4

Rate me:
Please Sign up or sign in to vote.
4.89/5 (26 votes)
3 Dec 2013CPOL9 min read 97.2K   2.2K   68   45
Using an extended MVVM pattern for real world LOB applications: Part 4
Other Articles In This Series:
Part One
Part Two
Part Three
Part Four  

Introduction

In part one of this series of articles, I introduced my take on MVVM pattern, and discussed some of the 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 part three, I presented the code for enough of the application to get us up and running, displaying a Customer Selection View in a Form, containing data at both run time and design time.

In this part of the series, I will build upon this basic application to show real functionality.

Filtering

The specification for our filtering requirements is pretty simple. Allow the user to type in a State code, and filter the list to include only those customers in that state. (I'm an ex-pat pom living in Aus - so I'm using Australian state codes here - the full set is QLD, NSW, SA, WA, TAS, NT, ACT, VIC if you're interested.) (Actually, that's the full set whether you're interested or not)

As usual, a million and one ways to solve this, but my designer wanted to do this as a "Search As you Type". 

The first part to achieve this is actually already in the XAML of our CustomerSelectionView:

C#
Text="{Binding Path=StateFilter, UpdateSourceTrigger=PropertyChanged}"></TextBox>

This line binds the state text box to a StateFilter property, and the UpdateSourceTrigger attribute tells WPF to update the property whenever it is changed.

A reminder may be in order here; this property, while an Observable property, is part of the functionality rather than part of the data being acted upon - so the property resides in the ViewModel rather than the ViewData object.

So, what will happen is, the user types something into the TextBox, that action sets the property on the ViewModel, which will ask the Controller to provide a newly filtered set of data. As that data is bound to the View, the DataGrid in the View will update.

But wait! If we implement that, as soon as a single letter is typed, the list will blank, as no States have a single letter code. we could do the filtering as a 'starts-with' filter - but then typing 'N' for NT would bring up all the NSW customers as well. OK - it's not such a problem really, but there could be an issue with the time it takes to get the filtered results - so we don't want to keep refreshing results as the user presses a key.

So, what I want to do is introduce a delay. No filtering will take place for, say, half a second after the user has typed something - then after half a second, the list will be filtered using the contents of the TextBox, unless another key is pressed, in which case the half-second countdown starts again.

So, we need a Timer - I'm using a DispatcherTimer, so a reference to WindowsBase needs to be added to the ViewModels project.

Then, we can add a new private field:

C#
DispatcherTimer stateFilterTimer;

And we need to add our ObservableProperty StateFilter.

C#
private string stateFilter;
public string StateFilter
{
	get
	{
		return stateFilter;
	}
	set
	{
		if (value != stateFilter)
		{
			stateFilterTimer.Stop();
			stateFilter = value;
			RaisePropertyChanged("StateFilter");
			stateFilterTimer.Start();
		}
	}
}

In the constructor, we need to instantiate the timer, and set its timespan to half a second, and give it an event handler method so we can handle the event when the timer reaches zero.

C#
/// <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
	stateFilterTimer = new DispatcherTimer()
	{
		Interval = new TimeSpan(0, 0, 0, 0, 500)
	};
	stateFilterTimer.Tick += StateFilterTimerTick;
	StateFilter = stateFilter;
	RefreshList();
}

Of course, then we need to write that event handler...

C#
/// <summary>
/// Event handler for the timer used for 'filter as you type' on the State filter.
/// When the timer triggers, filter the list with the existing filter.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void StateFilterTimerTick(object sender, EventArgs e)
{
	stateFilterTimer.Stop();
	RefreshList();
}

Then all that is required is to change the RefreshList method so that we pass the StateFilter property, rather than an empty string.

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

That should do us - give it a run. Remembering in my test data I only used two states - Qld and NSW.

Filtering in action

Editing

Well, the whole point of this application is to be able to edit customer details, so let's get started by creating our CustomerEditViewModel.

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

namespace ViewModels
{
    /// <summary>
    /// A ViewModel for a view that allows a Customer to be modified
    /// </summary>
    public class CustomerEditViewModel : BaseViewModel
    {
        #region Private Fields
        #endregion
        #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
        #endregion
        #endregion
        #region Commands
        #region Command Relays
        private RelayCommand<IView> cancelledCommand;
        private RelayCommand<IView> saveCommand;
        public ICommand CancelledCommand
        {
            get
            {
                return cancelledCommand ?? (cancelledCommand =
                new RelayCommand<IView>(param => ObeyCancelledCommand(param),
                param => CanObeyCancelledCommand(param)));
            }
        }

        public ICommand SaveCommand
        {
            get
            {
                return saveCommand ?? (saveCommand = new RelayCommand<IView>
                (param => ObeySaveCommand(param), param => CanObeySaveCommand(param)));

            }
        }

        #endregion // Command Relays

        #region Command Handlers

        /// <summary>
        /// </summary>
        /// <returns></returns>
        private bool CanObeyCancelledCommand(IView view)
        {
            return true;
        }

        private void ObeyCancelledCommand(IView view)
        {
            CloseViewModel(false);
        }
        private bool CanObeySaveCommand(IView view)
        {
            return true;
        }

        private void ObeySaveCommand(IView view)
        {
            CustomerController.UpdateCustomer((CustomerEditViewData)ViewData);
            CloseViewModel(true);
        }
        #endregion // Command Handlers
        #endregion // Commands

        #region Constructor

        public CustomerEditViewModel(ICustomerController controller)
            : this(controller, null)
        {
        }


        public CustomerEditViewModel(ICustomerController controller, IView view)
            : base(controller, view)
        {
            controller.Messenger.Register(MessageTypes.MSG_CUSTOMER_SELECTED_FOR_EDIT,
            new Action<Message>(HandleCustomerSelectedForEditMessage));
        }

        #endregion

        /// <summary>
        /// If somewhere someone selects a customer for editing and this 
        /// Edit ViewModel is already
        /// Editing that customer, then abort the message, and make the View active
        /// </summary>
        /// <param name="message"></param>
        private void HandleCustomerSelectedForEditMessage(Message message)
        {
            CustomerListItemViewData customer = 
		message.Payload as CustomerListItemViewData;
            if (customer != null && customer.CustomerId == 
			((CustomerEditViewData)ViewData).CustomerId)
            {
                message.HandledStatus = MessageHandledStatus.HandledCompleted;
                ActivateViewModel();
            }
        }
    }
}

Let's spend a minute looking through this code to make sure we understand what's what.

Again, I've defined a private property of type ICustomerController to return the IController defined in the BaseViewModel, just to save me casting it every time I use it.

There's no ObservableProperties. The ObservableProperties for the Customer data that we are editing are in the CustomerViewData - the absense of ObservableProperties in the ViewModel tells us that there is no additional bound functionality in this View.

We've defined two RelayCommands of type IView (cancelledCommand and saveCommand) instantiated when needed by the associated property Getter.

The CanObeyCancelledCommand method always returns true - so the user can always cancel.

The CanObeySaveCommand also always returns true. In the real world, of course, you'd probably only want to return true if the current CustomerEditViewData was 'dirty' - that is, the user had made changes - but this series of articles is long enough without adding the additional complexity of change tracking!

The ObeyCancelledCommand method (which is the method that gets called when the user cancels) uses the CloseViewModel method with a False parameter. This parameter determines, if a view is shown as a dialog,  whether the dialogresult is true or false.

The ObeySaveCommand asks the CustomerController to save the data in our ViewData (which is bound to the controls that the user is using to make changes, and so reflects those changes). It then uses the CloseViewModel method, passing 'True' so that, if the view is shown as a dialog, the DialogResult will be true.

In the constructor, you will see that the CustomerEditViewModel registers to receive messages of the type 'MSG_CUSTOMER_SELECTED_FOR_EDIT'. The reason for this is so that we can, if we so desire, open several CustomerEditViewModels, each editing its own Customer - but we really don't want the same customer to be edited in two Views - so every time a Customer is selected for Edit, the CustomerEditViewModel inspects the message and, if the customer selected matches the customer it's currently editing, it will set the message status to HandledCompleted and 'Activate' itself, which really means it will Activate the View - so the effect to the user will be that the window containing the View will become the Active window.)

This is all achieved in the HandleCustomerSelectedForEditMessage method. The Message object has a Payload property, which in the case of this message type, is a CustomerListItemViewData. (As an aside, I have implemented nothing in this version to either enforce that this message type's payload object is of the correct type, or made any attempt to automate this casting. It's not that hard to do so, if you want to - but the more you move down that route, the more you move toward a complicated Framework - and that's what I wanted to avoid.)

Okay - that's the ViewModel - let's knock up a View to suit - then we can ship it off to the designer to make it look pretty.

CustomerEditView.xaml

XML
<views:BaseView x:Class="Views.CustomerEditView"
                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:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:views="clr-namespace:Views"
                mc:Ignorable="d"
                d:DesignHeight="243"
                d:DesignWidth="346"
                d:DataContext="{d:DesignInstance 
			Type=views:DesignTimeCustomerEditViewModel,
                IsDesignTimeCreatable=true}">
    <StackPanel Margin="10">
        <Grid >
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Name"
                       Grid.Column="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.Name}"
                     Grid.Column="1"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Address"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.Address}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Suburb"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.Suburb}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="State"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.State}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="PostCode"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.PostCode}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="Phone"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.Phone}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="100*" />
                <ColumnDefinition Width="200*" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="eMail"
                       Grid.Column="0"
                       Grid.Row="0"
                       Margin="8" />
            <TextBox Text="{Binding ViewData.Email}"
                     Grid.Column="1"
                     Grid.Row="0"
                     Margin="2" />
        </Grid>
        <StackPanel Orientation="Horizontal"
                    FlowDirection="RightToLeft"
                    Height="35">
            <Button Content="Save"
                    Command="{Binding Path=SaveCommand, Mode=OneTime}"
                    Height="23"
                    Width="75"
                    Margin="5,5,25,2" />
            <Button Content="Cancel"
                    Command="{Binding Path=CancelledCommand, Mode=OneTime}"
                    Height="23"
                    Width="75"
                    Margin="5,5,25,2" />
        </StackPanel>
    </StackPanel>
</views:BaseView>

Nothing much to see in the edit view. A bunch of Text Blocks in pairs with TextBoxes. The TextBoxes are bound to the properties of the CustomerEditViewData.

There's two buttons - one to Cancel and one to Save, each bound to the appropriate Command. And that's about it!

So if we head back to the CustomerController_ViewManagement.cs, the GetCustomerEditView can be un-commented as we now have a CustomerEditView so it will compile. Also in the CustomerController.cs source, the contents of the editCustomer method can be un-commented.

Before we go further, remember that part of our goal is Blendability - the ability to ship off our Views to be messed with by Designers to make them look pretty? In the CustomerEditView XAML, we have:

C#
d:DataContext="{d:DesignInstance Type=views:DesignTimeCustomerEditViewModel, 
	IsDesignTimeCreatable=true}">

which tells the designer to instantiate an instance of a DesignTimeCustomerEditViewModel so our designer can see some data. So, we better create one.

DesignTimeCustomerEidtViewModel.cs

C#
using ViewModels;

namespace Views
{
    class DesignTimeCustomerEditViewModel : CustomerEditViewModel
    {
        public DesignTimeCustomerEditViewModel()
        {
            ViewData = new CustomerEditViewData()
            {
                Address = "23 Netherington on Wallop Street",
                CustomerId = 123,
                Email = "Oldhag@GeeMail.Com",
                Name = "Betty Boop",
                Phone = "0414 4142424",
                PostCode = "4540",
                State = "QLD",
                Suburb = "Indooroopilly"
            };
        }
    }
}

Blendability - An Aside

Look at the three screenshots below. They're comparisons of what you see in VS2010, Expression Blend 4 and at Runtime with the customer form with a simple Drop Shadow effect added to the buttons (this effect is not included in the listings presented here, nor in the downloaded version.)

View editing in Blend

View editing in VS2010

View at Runtime

Interesting how Blend aligns the Buttons and the drop Shadow to the left, while VS2010 (and the runtime) aligns them both right! This sort of thing tends to upset Designers, but at least they can be mollified by the fact that they are looking at data rather than blank entry fields.

On With the Show

Now, run the program. You should be able to select a customer, click the buton to edit it, which should open a window in which you can make changes to the customer. The window isn't modal, so you can go back and select another customer, which opens another window.

Close the Selection window, and all of the Edit windows will be closed too.

Make a change to one of the fields shown in the Selection list, save the customer, and the selection list refreshes to show the modified details.

Make a change to one of the same fields and cancel, and no changes are shown in the selection list.

Select the same customer twice, and the same window you opened the first time takes focus.

I reckon that fits the specification nicely (if you can remember back that far to part 1).

Changes

Of course, we all know life ain't that simple! As soon as our project sponsor sees the application, he wants it changed. He doesn't like being able to have multiple edit windows open - far too confusing.

OK, let's make the change for him.

Pop into the CustomerController source, and find the EditCustomer method. This is where we do whatever we want to do when we want to edit a customer - so here's where we need to change that first parameter in the view.ShowInWindow from false to true.

Job done.

Conclusion

We've come a long way over four instalments, but I hope this has been of some help to somebody.

As I have stressed, this is not a Framework, but just an example of how I have put together workable components to make an MVVM WPF application that works, and improves, for me, on some of the shortcomings of other solutions.

This might not be for you. You may want to use one of the frameworks out there - Cinch or MVVM Light or one of the gazzilion others - or you may want to roll your own.  Or, like me, you may want to do it your own way, implementing the thing you find works best in your environment.

Whatever you do, I'd really appreciate your feedback. I'm sure I've made mistakes along the way, and am always happy to learn from others to improve my own stuff.

Once again, many thanks to the giants on whose shoulders I've stood - especially Pete O'Hanlon.

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

 
QuestionA question from a novice Pin
Carpi_19685-Feb-15 6:50
Carpi_19685-Feb-15 6:50 
AnswerRe: A question from a novice Pin
_Maxxx_29-Mar-17 2:27
professional_Maxxx_29-Mar-17 2:27 
GeneralGreat way to present a topic Pin
Chris Marassovich4-Dec-13 16:41
Chris Marassovich4-Dec-13 16:41 
GeneralRe: Great way to present a topic Pin
_Maxxx_4-Dec-13 17:02
professional_Maxxx_4-Dec-13 17:02 
SuggestionPlace a link to the other episodes Pin
Klaus Luedenscheidt2-Dec-13 19:11
Klaus Luedenscheidt2-Dec-13 19:11 
GeneralRe: Place a link to the other episodes Pin
_Maxxx_2-Dec-13 20:08
professional_Maxxx_2-Dec-13 20:08 
GeneralRe: Place a link to the other episodes Pin
_Maxxx_2-Dec-13 23:08
professional_Maxxx_2-Dec-13 23:08 
GeneralRe: Place a link to the other episodes Pin
Klaus Luedenscheidt3-Dec-13 18:39
Klaus Luedenscheidt3-Dec-13 18:39 
GeneralRe: Place a link to the other episodes Pin
_Maxxx_3-Dec-13 18:42
professional_Maxxx_3-Dec-13 18:42 
SuggestionBlend design issue Pin
Richard Deeming12-Feb-13 7:24
mveRichard Deeming12-Feb-13 7:24 
GeneralRe: Blend design issue Pin
_Maxxx_12-Apr-13 1:20
professional_Maxxx_12-Apr-13 1:20 
QuestionDoubl-click on the data grid to select for edit Pin
jgalak30-Jul-11 10:44
jgalak30-Jul-11 10:44 
AnswerRe: Doubl-click on the data grid to select for edit Pin
_Maxxx_31-Jul-11 2:24
professional_Maxxx_31-Jul-11 2:24 
QuestionWhere's the databinding "connection" between the View and the ViewData Pin
jgalak29-Jun-11 10:03
jgalak29-Jun-11 10:03 
AnswerRe: Where's the databinding "connection" between the View and the ViewData Pin
_Maxxx_29-Jun-11 14:14
professional_Maxxx_29-Jun-11 14:14 
GeneralRe: Where's the databinding "connection" between the View and the ViewData Pin
jgalak30-Jun-11 6:15
jgalak30-Jun-11 6:15 
GeneralRe: Where's the databinding "connection" between the View and the ViewData Pin
_Maxxx_30-Jun-11 12:43
professional_Maxxx_30-Jun-11 12:43 
QuestionValue of DialogResult Pin
Member 783241823-Jun-11 9:50
Member 783241823-Jun-11 9:50 
AnswerRe: Value of DialogResult Pin
_Maxxx_24-Jun-11 1:12
professional_Maxxx_24-Jun-11 1:12 
GeneralRe: Value of DialogResult Pin
Member 783241824-Jun-11 7:38
Member 783241824-Jun-11 7:38 
GeneralRe: Value of DialogResult Pin
_Maxxx_24-Jun-11 13:48
professional_Maxxx_24-Jun-11 13:48 
GeneralUsing comboboxes for lookup Pin
Frenzi26-May-11 10:51
Frenzi26-May-11 10:51 
GeneralRe: Using comboboxes for lookup Pin
_Maxxx_26-May-11 12:39
professional_Maxxx_26-May-11 12:39 
GeneralRe: Using comboboxes for lookup Pin
Frenzi27-May-11 1:09
Frenzi27-May-11 1:09 
GeneralRe: Using comboboxes for lookup Pin
_Maxxx_27-May-11 2:35
professional_Maxxx_27-May-11 2:35 

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.