When starting out with WPF and the M-V-VM pattern I was looking for a simple example of this pattern. Most of the things I found were full of items that complimented the pattern but didn’t show just the pattern itself. I decided to walk through a simple example and will detail it below.
You can download the source code from here.
Basic M-V-VM OverView
| Layer |
Items |
| Model |
Business Objects, Business Logic |
|
View Model
|
UI Logic, Data / Services Interaction
|
|
View
|
UI elements (Styling, Data Binding, etc)
|
The Model
The model in the MVVM pattern will be the business object. For this example my model is a picture object. This object contains some basic properties a picture would have such as:
- Name
- File Path
- Height
- Width
It also contains a validation method to validate the Picture object. This was placed here for example only (notice there is no logic in the method). Any business logic for the model should be contained within this class or a model helper class.
namespace MVVMDemoApplication.Models
{
public class Picture : BaseModel
{
private string _pictureName;
private string _picturePath;
private int _height;
private int _width;
public Picture()
{
Width = 300;
Height = 200;
}
/// <summary>
/// Name of Picture
/// </summary>
public string PictureName
{
get { return _pictureName; }
set
{
if (_pictureName != value)
{
_pictureName = value;
NotifyPropertChanged("PictureName");
}
}
}
/// <summary>
/// Picture path
/// </summary>
public string PicturePath
{
get { return _picturePath; }
set
{
if (_picturePath != value)
{
_picturePath = value;
NotifyPropertChanged("PicturePath");
}
}
}
/// <summary>
/// Height of Picture
/// </summary>
public int Height
{
get { return _height; }
set
{
if (_height != value)
{
_height = value;
NotifyPropertChanged("Height");
}
}
}
/// <summary>
/// Width of Picture
/// </summary>
public int Width
{
get { return _width; }
set
{
if (_width != value)
{
_width = value;
NotifyPropertChanged("Width");
}
}
}
/// <summary>
/// Determine if Picture is Valid
/// This is a model based logic validation and does not belong in the viewmodel
/// </summary>
/// <returns></returns>
public bool IsPictureValid()
{
bool isValid = true;
// Add Code here to validate picture - whatever you want
return isValid;
}
}
}
Base Model Class
This class is used to provide functionality to each model class without the need to replicate code. This works perfectly when implementing the INotifyPropertyChanged interface for Data Bound Properties in WPF and Silverlight.
namespace MVVMDemoApplication.Models
{
public class BaseModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify of Property Changed event
/// </summary>
/// <param name="propertyName"></param>
public void NotifyPropertChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The View Model
The view model contains the UI logic for the view. Any logic, method calls, parameters, etc should always be placed in the View Model. This logic should not be placed in the view.
The view model contains a collection of picture objects. These objects have to be populated from a data source. After many hours of research I have to agree with some of the experts (a.k.a Josh Smith) and say that while the data operations belong in the View Model layer they should be extracted from the view model in a separate set of classes. This will simplify the view models and allow the reuse of the data access logic across multiple view models. This will be more apparent and useful when using Silverlight with WCF / RIA Services.
Properties
- Error Message – displayed when error occurs
- Pictures – observable collection of Picture Objects (Model). It is better to use an observable collection when items maybe added / removed from the collection so the data binding recognizes the changes automatically. This does not occur with list objects.
Methods
- MakePictureBigger – enlarge picture if valid (notice the call to the IsPictureValid object on the model itself)
- MakePictureSmaller – make picture smaller (notice the validation logic – since this is UI logic and not business logic it belongs in this class)
- CheckIfPictureSelected – ensure that an item is selected from the list box – if not modify the error message on the view model.
- LoadPictures – call to the services class to retrieve the data. In this case it is a list of image files. This can be anything from a WCF services call to a call out to a custom Data Access Layer (DAL).
- BuildPictureCollection – build the collection of picture objects – this is here versus the data services layer to reduce coupling.
- BuildPictureObject – load values on the object based on the file path given
namespace MVVMDemoApplication.ViewModels
{
public class PictureListBoxViewModel : BaseViewModel
{
ObservableCollection<Picture> _pictures;
PictureDataServices _services;
string _pictureDirectory;
string _errorMessage;
/// <summary>
/// Selected Picture in ListBox
/// </summary>
public Picture SelectedPicture { get; set; }
/// <summary>
/// Error Message
/// </summary>
public string ErrorMessage
{
get { return _errorMessage; }
set
{
if (_errorMessage != value)
{
_errorMessage = value;
NotifyPropertChanged("ErrorMessage");
}
}
}
/// <summary>
/// Observable Collection of Pictures
/// Uses INotifyPropertyChange when list changes
/// </summary>
public ObservableCollection<Picture> Pictures
{
get { return _pictures; }
set
{
if (_pictures != value)
{
_pictures = value;
NotifyPropertChanged("Pictures");
}
}
}
/// <summary>
/// Constructor
/// </summary>
public PictureListBoxViewModel()
{
// Initialize data services class for later use
// It is best to keep this isolated from the view model
_services = new PictureDataServices();
// Set Picture Directory
_pictureDirectory = Directory.GetCurrentDirectory() + "\\Images";
// Initialize here so we don't forget to later and end up with a null reference exception
Pictures = new ObservableCollection<Picture>();
LoadPictures();
}
/// <summary>
/// Overload to allow different images directory
/// Maybe one view has an option to select a directory
/// </summary>
/// <param name="imagespath"></param>
public PictureListBoxViewModel(string imagespath)
{
// Initialize data services class for later use
// It is best to keep this isolated from the view model
_services = new PictureDataServices();
// Set Picture Directory
_pictureDirectory = imagespath;
// Initialize here so we don't forget to later and end up with a null reference exception
Pictures = new ObservableCollection<Picture>();
LoadPictures();
}
/// <summary>
/// Make the selected picture bigger
/// </summary>
public void MakePictureBigger()
{
//Perform Error Check
if (!CheckifPictureIsSelected())
return;
Picture picture = SelectedPicture;
// Here is where we call business logic on the picture object itself
// The validation method on the object determines if object is valid
// This logic has nothing to do with the view or the view model so it
// Is placed on the model
if (picture.IsPictureValid())
{
picture.Height = picture.Height + 50;
picture.Width = picture.Width + 50;
}
}
/// <summary>
/// Make the selected picture Smaller
/// </summary>
public void MakePictureSmaller()
{
//Perform Error Check
if (!CheckifPictureIsSelected())
return;
Picture picture = SelectedPicture;
// Verify we don't make the picture to small. Since this is a UI
// check we place in the View Model versus the model. The model
// does not care how big the picture is displayed
if (picture.Height > 60)
{
picture.Height = picture.Height - 50;
picture.Width = picture.Width - 50;
}
}
/// <summary>
/// Check if user selected item
/// </summary>
/// <returns></returns>
private bool CheckifPictureIsSelected()
{
if (SelectedPicture == null)
{
ErrorMessage = "Please select a picture";
return false;
}
else
{
ErrorMessage = string.Empty;
return true;
}
}
/// <summary>
/// Load Pictures from FileSystem into Collection
/// </summary>
private void LoadPictures()
{
// Load picture collection from services (could be WCF or other Data Access Logic)
List<string> pictureFilePaths = _services.GetPicturesFromDirectory(_pictureDirectory);
// Build the Collection of Models here - not done in DataSevices to reduce coupling
BuildPictureCollection(pictureFilePaths);
}
/// <summary>
/// Build / Load Collection of Pictures
/// </summary>
/// <param name="pictureFilePaths"></param>
private void BuildPictureCollection(List<string> pictureFilePaths)
{
foreach (string file in pictureFilePaths)
{
// Build Picture Object
Picture pic = BuildPictureObject(file);
// Add object to collection if not null
if (pic != null)
Pictures.Add(pic);
}
}
/// <summary>
/// Load Picture object
/// </summary>
/// <param name="filepath"></param>
/// <returns></returns>
private Picture BuildPictureObject(string filepath)
{
Picture pic = new Picture();
FileInfo fi = new FileInfo(filepath);
pic.PictureName = fi.Name;
pic.PicturePath = filepath;
return pic;
}
}
}
Base View Model Class
This class is used to provide functionality to each view model class without the need to replicate code. This works perfectly when implementing the INotifyPropertyChanged interface for Data Bound Properties in WPF and Silverlight.
namespace MVVMDemoApplication.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notify of Property Changed event
/// </summary>
/// <param name="propertyName"></param>
public void NotifyPropertChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
}
The View
The view should contain the bindings to the view model only. This may include some event handling. In this example I used some event triggers for buttons to call methods on the view model directly. Command binding reduces the need for this extra code. For this example I did not want to introduce command binding as it does add an extra level of complexity that is best held for future example / post.
This solution has 2 views. The first one shows the initialization of the View and View Model via XAML. The second one performs the exact same functions in the code behind page.
View 1
namespace MVVMDemoApplication.Views
{
/// <summary>
/// Interaction logic for PictureView.xaml
///
/// NO Business / Application logic should be in this class
/// This class is for UI Logic ONLY!!!!
///
/// Also note that the events you see in this class can be removed completely when using
/// Command Binding.
///
/// Added to interface to enable easy UI switching
/// </summary>
public partial class PictureView : Page, PictureViewInterface
{
// Reference for events. This can be elimnated when using command binding. The
// ViewModel in this case was created via XAML through the Data Context method
PictureListBoxViewModel _plvm;
public PictureView()
{
InitializeComponent();
_plvm = (PictureListBoxViewModel)this.gdRoot.DataContext;
}
private void btnMakeSmaller_Click(object sender, RoutedEventArgs e)
{
_plvm.MakePictureSmaller();
}
private void btnMakeBigger_Click(object sender, RoutedEventArgs e)
{
_plvm.MakePictureBigger();
}
private void btnClose_Click(object sender, RoutedEventArgs e)
{
// Grabbed parent window to call the close event on the parent window. This
// can be wired up different but was done this way for simplicity
Window win = (Window)this.Parent;
win.Close();
}
/// <summary>
/// Added just to show the difference when using different views
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSwitchViews_Click(object sender, RoutedEventArgs e)
{
Window1 win = (Window1)this.Parent;
win.SwitchViews(this);
}
}
}
View 2 – Constructor only
public partial class PictureView2 : Page
{
PictureListBoxViewModel _plvm;
public PictureView2()
{
InitializeComponent();
// Initialize ViewModel in constructor
_plvm = new PictureListBoxViewModel();
// Set Data Context for Grid
gdRoot.DataContext = _plvm;
}
View 1 – XAML
Notice the data context and PictureViewModel initialization is done at the Grid.DataContext section instead of in the code behind page.
<Page x:Class="MVVMDemoApplication.Views.PictureView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModels="clr-namespace:MVVMDemoApplication.ViewModels"
Title="PictureView">
<Grid x:Name="gdRoot">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
</Style>
</Grid.Resources>
<Grid.DataContext>
<!--Set the Grid Data Context to the PictureListViewModel which initializes the class-->
<ViewModels:PictureListBoxViewModel/>
</Grid.DataContext>
<ListBox x:Name="lbPictures"
ItemsSource="{Binding Path=Pictures}"
HorizontalAlignment="Left"
BorderBrush="AliceBlue"
BorderThickness="2"
Margin="10"
SelectedItem="{Binding Path=SelectedPicture}">
<!--The list box template can be removed from this file and placed in a resource dictionary-->
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="5"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding Path=PictureName}"
/>
<Image
Margin="5"
Height="{Binding Path=Height}"
Width="{Binding Path=Width}"
Source="{Binding Path=PicturePath}"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Grid.Row="1" HorizontalAlignment="Center" Orientation="Horizontal">
<Label>PictureView</Label>
<Button Name="btnMakeSmaller" Click="btnMakeSmaller_Click" ToolTip="Make selected item smaller" >Shrink</Button>
<Button Name="btnMakeBigger" Click="btnMakeBigger_Click" ToolTip="Make selected item bigger" >Grow</Button>
<Button Name="btnSwitchViews" Click="btnSwitchViews_Click" ToolTip="Click to switch to other PictureView">Switch Views</Button>
<Button Name="btnClose" Click="btnClose_Click" ToolTip="Close the application" >Close</Button>
<TextBlock Name="tbErrorMessage" Text="{Binding Path=ErrorMessage}" Foreground="Red" />
</StackPanel>
</Grid>
</Page>
Testing
Another major benefit of M-V-VM when implemented correctly is the ability to test the view model. These can be in the form of Unit or Integration tests. I added a test project to this solution utilizing NUnit (DLL provided in solution) so it will build. You will need a test runner to be able to execute the tests.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using MVVMDemoApplication.ViewModels;
using System.IO;
namespace MVVMTestProject
{
[TestFixture]
public class TestClass
{
[Test]
public void Verify_PictureModel_HasPictures()
{
string imagesdirectory = Directory.GetCurrentDirectory() + "\\Images";
PictureListBoxViewModel vm = new PictureListBoxViewModel(imagesdirectory);
Assert.IsTrue(vm.Pictures.Count() > 0);
}
}
}
Summary
Using the M-V-VM pattern is a different mindset from traditional WinForms application development. It is similar to the ASP.NET MVC / Rails patterns but for WPF and Silverlight. This pattern makes it easy to test the application as the UI presentation layer has been extrapolated from the UI and business logic. It also helps reduce coupling and make changing the UI layer (Views) much easier.
You can download the source code from here.