Using Model – View – ViewModel with Silverlight
The View – Model – ViewModel design pattern, also known as MVVM, is getting more popular these days. I have found it extremely easy to use when developing very different applications and have used the design pattern recently in both ASP.NET, WPF and Silverlight applications. However easy as it might be is seems to confuse people as I have seen some terrible examples where people make a complete mess of things.
Josh Smith did an excellent screen cast for Pixel8 on using MVVM with WPF, you can find it here.
Even thought the UI technology used doesn't change the basic MVVM pattern there are some subtle differences, like not easily being able to use ICommand in Silverlight, so I decided to create a small Silverlight sample.
The basic structure goes like this:
- The user interacts with a View, implemented as a Silverlight user control.
- The View is data bound to a ViewModel. This is the most important step to remember. The ViewModel is just another class.
- The ViewModel is a wrapper for a Model. Think of the Model as the data and the business rules.
The Model
The Model I am using is very simple and has two read-write properties, FirstName and LastName, and a single read-only property, FullName. The FullName property represents some business rule on how a name should be formatted. I know it is a bit of a lame business rule but it is just a sample
. The Model also implements INotifyPropertyChanged so the UI can be updated whenever a value is updated. The complete model looks like this:
1: using System.ComponentModel;
2:
3: namespace SilverlightMVVMDemo.Model
4: {
5: public class Customer : INotifyPropertyChanged
6: {
7: private string _firstName;
8: private string _lastName;
9:
10: public string FirstName
11: {
12: get { return _firstName; }
13: set
14: {
15: _firstName = value;
16: OnPropertyChanged("FirstName");
17: OnPropertyChanged("FullName");
18: }
19: }
20: public string LastName
21: {
22: get { return _lastName; }
23: set
24: {
25: _lastName = value;
26: OnPropertyChanged("LastName");
27: OnPropertyChanged("FullName");
28: }
29: }
30:
31:
32: public string FullName
33: {
34: get { return FirstName + " " + LastName; }
35: }
36:
37: public event PropertyChangedEventHandler PropertyChanged;
38:
39: protected void OnPropertyChanged(string propertyName)
40: {
41: if (PropertyChanged != null)
42: {
43: var args = new PropertyChangedEventArgs(propertyName);
44: PropertyChanged(this, args);
45: }
46: }
47: }
48: }
The View
The View is just a Silverlight user control that makes things visible for the user. All controls use data binding to get to their values. There is one interesting twist here. Just like I had a business rule about how to format a FullName I have a rule about the font weight to use to display the FullName. This is just UI so not a business rule and as a result it is not stored in the Model. We do not want to code this in the UI as that makes things hard to test. So that leaves one place, the ViewModel
. And the View uses data binding to set the FontWeight property.
The complete View looks like this:
1: <UserControl
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:SilverlightMVVMDemo_ViewModel="clr-namespace:SilverlightMVVMDemo.ViewModel" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Class="SilverlightMVVMDemo.View.CustomerView"
5: Width="400" Height="300" mc:Ignorable="d">
6: <Grid x:Name="LayoutRoot"
7: Background="White">
8: <Grid.RowDefinitions>
9: <RowDefinition Height="0.133*"/>
10: <RowDefinition Height="0.133*"/>
11: <RowDefinition Height="0.133*"/>
12: <RowDefinition Height="0.18*"/>
13: <RowDefinition Height="0.42*"/>
14: </Grid.RowDefinitions>
15: <Grid.ColumnDefinitions>
16: <ColumnDefinition Width="0.3*"/>
17: <ColumnDefinition Width="0.7*"/>
18: </Grid.ColumnDefinitions>
19: <TextBlock Text="Firstname:"
20: Margin="8"/>
21: <TextBox Text="{Binding Path=FirstName, Mode=TwoWay}"
22: Grid.Column="1"
23: Margin="8,8,8,8"/>
24: <TextBlock Text="Lastname:"
25: Grid.Row="1"
26: Margin="8"/>
27: <TextBox Text="{Binding Path=LastName, Mode=TwoWay}"
28: Grid.Row="1"
29: Grid.Column="1"
30: Margin="8,8,8,8"/>
31: <TextBlock Text="Fullname:"
32: Grid.Row="2"
33: Margin="8"/>
34: <TextBox Text="{Binding Path=FullName}"
35: Grid.Row="2"
36: Grid.Column="1"
37: Margin="8,8,8,8"
38: FontWeight="{Binding Path=FullNameFontWeight}"
39: IsReadOnly="True"/>
40: <Button Content="Save"
41: Click="Button_Click"
42: Grid.Row="3"
43: Grid.Column="1" />
44: </Grid>
45: </UserControl>
The code behind is very simple and looks like this:
1: using System.Windows.Controls;
2: using SilverlightMVVMDemo.ViewModel;
3:
4: namespace SilverlightMVVMDemo.View
5: {
6: public partial class CustomerView : UserControl
7: {
8: public CustomerView()
9: {
10: InitializeComponent();
11: DataContext = new CustomerViewModel();
12: }
13:
14: private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
15: {
16: var viewModel = (CustomerViewModel)DataContext;
17: viewModel.Save();
18: }
19: }
20: }
In an ideal world there would be no code at all, or only that setting the DataContext but as Silverlight makes it a little harder to use commands there is a single line, okay two counting the cast, to pass the save command on to the ViewModel.
The ViewModel
The ViewModel class is the one that glues the View and the Model together, hence its name
. basically the ViewModel wraps each Model object, so if you want to use a collection of customer models you create a collection of CustomerViewModel objects and databind against them. The ViewModel catches all PropertyChanged events from the Model and passes them on to the user interface. Whenever the FullName property changes the FullNameFontWeight might also change so in that case an extra PropertyChanged event is raised. Note that calculation of the FontWeight to use is part of the ViewModel as the UI depends on this but it is not a business rule per se.
Another example of a property like this would be the CSS class to use in an ASP.NET application. basically if a UI property changes, create a property get to do so and databind it.
The complete ViewModel class looks like this:
1: using System.ComponentModel;
2: using System.Windows;
3: using SilverlightMVVMDemo.Model;
4:
5: namespace SilverlightMVVMDemo.ViewModel
6: {
7: public class CustomerViewModel : INotifyPropertyChanged
8: {
9: private Customer _model;
10:
11: public CustomerViewModel()
12: {
13: Model = new Customer() { FirstName = "Maurice", LastName = "de Beijer" };
14: }
15:
16: public CustomerViewModel(Customer model)
17: {
18: Model = model;
19: }
20:
21:
22: public Customer Model
23: {
24: get { return _model; }
25: private set
26: {
27: if (_model != null)
28: _model.PropertyChanged -= ModelPropertyChanged;
29:
30: _model = value;
31:
32: if (_model != null)
33: _model.PropertyChanged += ModelPropertyChanged;
34: }
35: }
36:
37: void ModelPropertyChanged(object sender, PropertyChangedEventArgs e)
38: {
39: OnPropertyChanged(e.PropertyName);
40:
41: if (e.PropertyName == "FullName")
42: OnPropertyChanged("FullNameFontWeight");
43: }
44:
45:
46: public string FirstName
47: {
48: get { return Model.FirstName; }
49: set { Model.FirstName = value; }
50: }
51: public string LastName
52: {
53: get { return Model.LastName; }
54: set { Model.LastName = value; }
55: }
56:
57: public string FullName
58: {
59: get { return Model.FullName; }
60: }
61:
62: public FontWeight FullNameFontWeight
63: {
64: get
65: {
66: if (FullName.Length > 20)
67: return FontWeights.ExtraBold;
68: else if (FullName.Length > 15)
69: return FontWeights.Bold;
70: else if (FullName.Length > 10)
71: return FontWeights.Normal;
72: else
73: return FontWeights.Light;
74: }
75: }
76:
77: public event PropertyChangedEventHandler PropertyChanged;
78:
79: protected void OnPropertyChanged(string propertyName)
80: {
81: if (PropertyChanged != null)
82: {
83: var args = new PropertyChangedEventArgs(propertyName);
84: PropertyChanged(this, args);
85: }
86: }
87:
88: internal void Save()
89: {
90: // Implement the logic to save the customer.
91: }
92: }
93: }
In this case I duplicated all properties from the Model in the ViewModel. This is not a must, some people prefer to expose and bind to the Model directly. It saves a few lines of code but I prefer not to do so. Of course there is no need to duplicate properties the UI is never going to bind to directly.
Model-View-ViewModel guidelines
Not very difficult at all as long as you keep to a few guidelines:
- Never set any UI properties from your CS code with the exception of the DataContext.
- If you need an event handler in your code behind, like the button click, make it a single line and pass the request onto the ViewModel.
- All UI control properties that need to change at runtime do so by data binding to a ViewModel property.
- Organize your ViewModel types per View and not per Model. It is perfectly fine to create a ViewModel that wraps two different Models. A View has only a single DataContext so you cannot bind it to multiple ViewModel objects.
www.TheProblemSolver.nl
Wiki.WindowsWorkflowFoundation.eu
And now on Twitter at http://twitter.com/mauricedb.