Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF

WPF/Silverlight: Step By Step Guide to MVVM

Rate me:
Please Sign up or sign in to vote.
4.66/5 (26 votes)
19 Jul 2011CPOL8 min read 110.1K   5.4K   103   27
This article aims to provide basic overview of MVVM design pattern which is very popular amongst WPF/Silverlight application developers. This is a very basic practical tutorial and aims at providing a step by step guide to people who are new to MVVM.

Introduction

MVVM is the short form for Model-View-ViewModel pattern widely used in WPF/Silverlight programming. This design pattern was introduced by John Gossman primarily for segregation and easy testability of View, ViewModel and Model.

Model - View - ViewModel

Let me first explain the three parts of MVVM.

Model

Model as we all know represents the data layer.

View

View represents the UI or the looks.

View Model

View Model is the middle man and its responsibility is to tweak the data from model in such a way that it can be consumed by the View. For some people, MVVM is a steep learning curve. Let me assure you that it's very easy if you keep four things in mind.

You can also call these four steps as the GURU MANTRA of MVVM.

mvvm.png

  1. Try to have minimum code behind. That is your View.xaml.cs that is supposed to have almost no code. Not even event handlers. This does not mean absolute zero code is a must, what I mean to say is that we should have minimum code behind. An exception would be cases like where the logic is pure View oriented (and is very specific to that view only) and has nothing to do with ViewModel or Model or even other Views of same ViewModel. For example, on mouse over you want to slide in the tooltip, you may opt to do it in xaml.cs (of course you could have also done it by using trigger in xaml itself, but just cooking it as an example). There is no harm in having such code behind as it does not have anything to do with ViewModel or Model.
    Having said this, I would like to mention a few exceptions to the above rule.
    1. Dependency properties will always be part of code behind, hence it does not mean that you should avoid dependency property in MVVM.
    2. Sometimes you have to use third party controls which are not MVVM compliant. In such cases too, you end up having some code behind.
  2. All Events or actions should be treated as Command. Your question would be how to do that, my control has click event behavior but no command. My answer is there are several ways to do that. All this will be explained in detail in the rest of the article.
  3. ViewModel is the DataContext of View. So neither View has instance ViewModel and nor does ViewModel have instance of View. It's incorrect and ugly to type cast View.DataContext to ViewModel. This will break your true MVVM model. 
  4. Design ViewModel and View independent of each other. This means that you must design the view by keeping in mind that if tomorrow the view is supposed to be replaced with another view (another look and feel), it should not require the ViewModel or model to change. Hence do not code anything in ViewModel that is specific to the view and also do not code anything in the view that is specific to ViewModel.

Homework Before Starting MVVM

  1. Learn and clear concepts on Databinding.
  2. Learn how to use Dependency Properties.
  3. Learn use of Converters.
  4. Last but not the least, INotifyPropertyChanged.

Let's start of with the basics first:

Data Binding: Data binding is the process that establishes a connection between the application UI and business logic. If the binding has the correct settings and the data provides the proper notifications, then, when the data changes its value, the elements that are bound to the data reflect changes automatically. Data binding can also mean that if an outer representation of the data in an element changes, then the underlying data can be automatically updated to reflect the change. For example, if the user edits the value in a TextBox element, the underlying data value is automatically updated to reflect that change.

Dependency Property: Dependency properties are just like normal CLR properties, but with a X-factor. The X-factor is that these properties have inbuilt support for data binding, animation, styling, value expressions, property validation, per-type default values. At the end of the day, we need dependency properties to be able to save state of UI Elements. It won't be incorrect to say that dependency property will always be part of View's code behind and normal (CLR) properties are more likely to be part of ViewModel or Model. Just like there is a syntax to define CLR properties (setter, getter), there is a syntax for dependency property as well. The syntax is:

C#
public class MySampleControl : Control
{
    // Step 1: Register the dependency property 
    public static readonly DependencyProperty SpecialBrushProperty =
            DependencyProperty.Register("SpecialBrush", typeof(Brush),
            typeof(MySampleControl));

    // Step 2: Provide set/get accessors for the property 
    public Brush SpecialBrush
    {
        get { return (Brush)base.GetValue(SpecialBrushProperty); }
        set { base.SetValue(SpecialBrushProperty, value); }
    }
}

Please note that there are other ways to declare setter and getter for dependency properties. Also note that there several ways and combination to register a dependency properties like registering it as attached property (which in turn makes the dependency property inheritable its value down the visual tree. Example if you change the font of the Grid control, all its children also start showing the font of the parent Grid), etc.

Converters: Data Conversion or Value Conversion is generally required when your view needs that data to be slightly tweaked from what it is currently. For example, in the view you want to show traffic light kind of signals based on the state of your project. That is green if your project is from 8 to 10, yellow if its rating is 5-7 and red if it’s less. Here there is no point keeping a variable corresponding to the color in your ViewModel, looks ugly. Here the showing project rating as color is a pure View requirement. Tomorrow, if this view is to be replaced with another view where you show project rating as number, your color variable will turn out to be a complete waste. So how do you do that, the solution is simple use Value converters, Value converters are purely part of View, it’s the view who wants the color to be green, yellow or red and ViewModel doesn’t care about the color. A value converter must derive from IValueConverter or IMultiValueConverter (in case you have multiple values to be used as input. Like in the traffic light example, let’s suppose your green color is to be shown when project is healthy and deadline is within acceptable limit. So we have two inputs, rating and deadline).

C#
[ValueConversion(typeof(Project), typeof(Brush))]
public class ColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        Project project = (Project)value;
        if (project.Rating >= 8)//its healthy if rating is 8 to 10
              return new SolidColorBrush(Colors.Green);
        else if (project.Rating >= 5) )//its ok if rating is 5 to 7
              return new SolidColorBrush(Colors.Yellow);
        else //less its unhealthy
              return new SolidColorBrush(Colors.Red);
    }    

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

INotifyPropertyChanged: INotifyPropertyChanged has been there since Winform arena. It’s not a new concept in WPF/Silverlight. It’s an interface that you would like to implement in your ViewModel to notify your View that something got changed. Observable Collection and Dependency Property are some which have inbuilt notification mechanism. For such properties, you may not require to raise the notification through code.

C#
public class SomeViewModel : ViewModelBase, INotifyPropertyChanged
{
     ….
     ….
    private Module _SelectedModule;
    public Module SelectedModule
    {
            get
            {
                return _SelectedModule;
            }
            set
            {
                _SelectedModule = value;
                RaisePropertyChanged("SelectedModule");
            }
    }
    …
    …
    #region INotifyPropertyChanged 
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName)
    {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
    }
    #endregion 

Coming back to the real thing, let's start of with the Model: Rest of the post, I shall explain by means of a demo application for displaying employee information.

Model: As the name suggests, it holds the data's model. Some people prefer to call model as data model. All your database select queries will first populate the data model which in turn will be used by ViewModel and View. As an example, I hereby demonstrate an Employee class. This class has properties corresponding to my data fields like name, notes, age, etc.

C#
public class Employee : IDataErrorInfo, INotifyPropertyChanged
{
    #region Constructor
    public Employee(string id = "", string name = "", uint age = 0, string notes = "")
    {
        _id = id;
        _name = name;
        _age = age;
        _notes = notes;
    }
    #endregion

    #region Properties
    private string _id = string.Empty;

    public string ID
    {
        get { return _id; }
        set
        {
            _id = value;
            RaisePropertyChanged("ID");
        }
    }

    private string _name = string.Empty;

    public string Name
    {
        get { return _name; }
        set
        {
            _name = value;
            RaisePropertyChanged("Name");
        }
    }

    private uint _age = 0;

    public uint Age
    {
        get { return _age; }
        set
        {
            _age = value;
            RaisePropertyChanged("Age");
        }
    }

    private string _notes = string.Empty;

    public string Notes
    {
        get { return _notes; }
        set
        {
            _notes = value;
            RaisePropertyChanged("Notes");
        }
    }
    #endregion

    #region IDataErrorInfo
    public string Error
    {
        get
        {
            return this[string.Empty];
        }
    }

    public string this[string propertyName]
    {
        get
        {
            string result = string.Empty;
            propertyName = propertyName ?? string.Empty;

            if (propertyName == string.Empty || propertyName == "ID")
            {
                if (string.IsNullOrEmpty(this.ID))
                {
                    result = "Employee ID is invalid. ID cannot be null or blank";
                }
                else if (System.Text.RegularExpressions.Regex.IsMatch
					(this.ID, "[/!@#?/}[}{ ]"))
                {
                    result = "Employee ID is invalid. 
				ID cannot have special characters";
                }
            }
            else if (propertyName == "Name")
            {
                if (string.IsNullOrEmpty(this.Name))
                {
                    result = "Name is invalid. ID cannot be null or blank";
                }
            }
            else if (propertyName == "Age")
            {
                if (Age > 150 || Age < 0)
                {
                    result = "Age is invalid. Age should be between 0 and 150";
                }
            }

            return result;
        }
    }
    #endregion

    #region INotifyPropertyChanged
    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}

In have inherited my model from IDataErrorInfo and INotifyPropertyChanged, but the same is not mandatory while designing your model. the same is done for data validation implementation and not MVVM. There is not much to explain in the model, let's proceed to the next level ViewModel.

ViewModel: In the beginning of this blog, I had mentioned that ViewModel is nothing but DataContext of View. Since ViewModel's responsibility is to play mediator between View and Model, it will always have an instance of Model class and to reflect change in model, it will notify View via notification (INotifyPropertyChanged, etc.) or databinding. All Commands of the View are processed/handled here. Keep in mind that a ViewModel may have multiple views, so never code anything which is specific to a given view here. Considering our example for Employee, in our view we have a list of employees on the left side and selected employees details on right side, so our viewModel which is supposed to be DataContext of View, must provide a list of employees and a property to represent selection. Hence we end up having two properties in our viewModel, Employees and SelectedEmployee.

C#
public class EmployeeListViewModel : INotifyPropertyChanged
{
    public ObservableCollection<Employee> Employees { get; private set; }

    public EmployeeListViewModel()
    {
        Employees = MVVMDemo.DataHelper.EmployeeDataHelper.CookEmployeesData();
    }

    private Employee _SelectedEmployee;
    public Employee SelectedEmployee
    {
        get
        {
            return _SelectedEmployee;
        }
        set
        {
            _SelectedEmployee = value;
            RaisePropertyChanged("SelectedEmployee");
        }
    }

    #region INotifyPropertyChanged
    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    #endregion
}

View: View is the look and feel. View is represented by xaml and its xaml.cs files. All your animations, decorations, themes, controls, etc. sit here. The view is to be coded keeping in mind that tomorrow if you want to display your data in a different way, it should not require you to touch the ViewModel or Model at all. For example, if you want to display your list of employees in a datagrid or listbox or tree or even tabs, it should not be a matter of concern for your ViewModel or Model, it’s purely the job of View to change the look and feel because at the end of the day it’s the same data that has to be presented in slightly different way. In our example of Employee list, View has a ListView of left side and selected employees details are displayed on the right side. So for this, we have an xaml and xaml.cs in place. But the xaml.cs hardly has anything significant; hence code for it can be ignored for now.

EmployeeListView.xaml
XML
 <Window x:Class="MVVMDemo.View.EmployeeListView"
        xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
        xmlns:x=http://schemas.microsoft.com/winfx/2006/xaml
        xmlns:VM="clr-namespace:MVVMDemo.ViewModel"
        xmlns:View="clr-namespace:MVVMDemo.View"
        Title="Employee View" Height="300" Width="720">
    <Window.DataContext>
        <VM:EmployeeListViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="152*" />
            <ColumnDefinition Width="568*" />
        </Grid.ColumnDefinitions>
        <ListView x:Name="listEmp" 
	ItemsSource="{Binding Employees, UpdateSourceTrigger=PropertyChanged}" 
	Grid.Column="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
	SelectedValue="{Binding SelectedEmployee, 
	UpdateSourceTrigger=PropertyChanged}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding ID}" 
			Header="ID" Width="40" />
                    <GridViewColumn DisplayMemberBinding="{Binding Name}" 
			Header="Name" Width="100"/>
                </GridView>
            </ListView.View>
        </ListView>
        <View:EmployeeView Grid.Column="1" 
		DataContext="{Binding SelectedEmployee, 
		UpdateSourceTrigger=PropertyChanged}"/>
    </Grid>
</Window> 
EmployeeView.xaml
XML
<UserControl x:Class="MVVMDemo.View.EmployeeView"
             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:wc="clr-namespace:System.Windows.Controls;
			assembly=PresentationFramework"
             xmlns:VM="clr-namespace:MVVMDemo.ViewModel"
             MinHeight="245" MinWidth="400"
>
    <Grid>
        <Grid.Resources>
            <Storyboard x:Key="FlashErrorIcon">
                <ObjectAnimationUsingKeyFrames BeginTime="00:00:00"
                                 Storyboard.TargetProperty="(UIElement.Visibility)">
                    <DiscreteObjectKeyFrame KeyTime="00:00:00" 
			Value="{x:Static Visibility.Hidden}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:00.4000000" 
			Value="{x:Static Visibility.Visible}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:00.8000000" 
			Value="{x:Static Visibility.Hidden}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:01.6000000" 
			Value="{x:Static Visibility.Visible}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:02.4000000" 
			Value="{x:Static Visibility.Hidden}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:03.2000000" 
			Value="{x:Static Visibility.Visible}"/>
                    <DiscreteObjectKeyFrame KeyTime="00:00:01" 
			Value="{x:Static Visibility.Visible}"/>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
            <Style TargetType="{x:Type TextBox}">
                <Style.Triggers>
                    <Trigger Property="Validation.HasError" Value="true">
                        <Setter Property="Background" Value="Pink"/>
                        <Setter Property="Foreground" Value="Black"/>
                    </Trigger>
                </Style.Triggers>
                <Setter Property="Validation.ErrorTemplate">
                    <Setter.Value>
                        <ControlTemplate>
                            <DockPanel LastChildFill="True"                   
                                       ToolTip="{Binding ElementName=controlWithError,
					Path=AdornedElement.(Validation.Errors)
						[0].ErrorContent}">
                                <Ellipse DockPanel.Dock="Right"
                                 ToolTip="{Binding ElementName=controlWithError,
                                     Path=AdornedElement.(Validation.Errors)
						[0].ErrorContent}"
                                 Width="15" Height="15"
                                 Margin="-25,0,0,0"
                                 StrokeThickness="1" Fill="IndianRed" >
                                    <Ellipse.Stroke>
                                        <LinearGradientBrush EndPoint="1,0.5" 
						StartPoint="0,0.5">
                                          <GradientStop Color="#FFFA0404" Offset="0"/>
                                          <GradientStop Color="#FFC9C7C7" Offset="1"/>
                                        </LinearGradientBrush>
                                    </Ellipse.Stroke>
                                    <Ellipse.Triggers>
                                        <EventTrigger RoutedEvent=
						"FrameworkElement.Loaded">
                                            <BeginStoryboard Storyboard=
					    "{StaticResource FlashErrorIcon}"/>
                                        </EventTrigger>
                                    </Ellipse.Triggers>
                                </Ellipse>
                                <TextBlock DockPanel.Dock="Right"
                                ToolTip="{Binding ElementName=controlWithError,
                                     Path=AdornedElement.(Validation.Errors)
						[0].ErrorContent}"
                                Foreground="White"
                                FontSize="10"
                                Margin="-15,5,0,0" FontWeight="Bold">!
                            <TextBlock.Triggers>
                                <EventTrigger RoutedEvent="FrameworkElement.Loaded">
                                    <BeginStoryboard Storyboard=
					"{StaticResource FlashErrorIcon}"/>
                                </EventTrigger>
                            </TextBlock.Triggers>
                                </TextBlock>
                                <Border BorderBrush="Red" BorderThickness="1">
                                    <AdornedElementPlaceholder 
					Name="controlWithError"/>
                                </Border>

                            </DockPanel>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Grid.Resources>
        <Grid.RowDefinitions>
            <RowDefinition Height="50*" />
            <RowDefinition Height="33*" />
            <RowDefinition Height="33*" />
            <RowDefinition Height="33*" />
            <RowDefinition Height="63*" />
            <RowDefinition Height="33*" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="75*" />
            <ColumnDefinition Width="425*" />
        </Grid.ColumnDefinitions>
        <TextBlock Grid.ColumnSpan="2" Text="Employee Details" 
		HorizontalAlignment="Center" VerticalAlignment="Center" 
		FontFamily="Tahoma" FontStyle="Italic" FontWeight="Bold" 
		Foreground="DodgerBlue" />
        <TextBlock Grid.Column="0" Grid.Row="1" Margin="5"  Text="ID :" 
		HorizontalAlignment="Right" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="1" Margin="5,5,15,5" 
		Text="{Binding ID,ValidatesOnDataErrors=true, 
		NotifyOnValidationError=true}" />
        <TextBlock Grid.Column="0" Grid.Row="2" Margin="5"  
	    	Text="Name :" HorizontalAlignment="Right" 
		VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="2" Margin="5,5,15,5" 
    	    	Text="{Binding Name,ValidatesOnDataErrors=true, 
		NotifyOnValidationError=true}" />
        <TextBlock Grid.Column="0" Grid.Row="3" Margin="5"  
	    	Text="Age :" HorizontalAlignment="Right" 
		VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="3" Margin="5,5,15,5" 
		Text="{Binding Age,ValidatesOnDataErrors=true, 
		NotifyOnValidationError=true}" />
        <TextBlock Grid.Column="0" Grid.Row="4" Margin="5"  
		Text="Notes :" HorizontalAlignment="Right" 
		VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="4" Margin="5,5,15,5" 
		Text="{Binding Notes}" />
    </Grid>
</UserControl>

Some Useful MVVM Frameworks

Also do not forget to check out a few handy tools/Frameworks for MVVM:

License

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


Written By
Technical Lead Barclays Capital
Singapore Singapore
Pradeep Dhawan:
Worked on Prism, Smart Client(SCSF CAB), WPF, WCF, WF, C#, VC++, C and Unix shell scripting.

Blog: http://programmingwpf.blogspot.com

Comments and Discussions

 
QuestionQuestion about mvvm Pin
elwatusi14-Jan-16 9:54
elwatusi14-Jan-16 9:54 
QuestionThis propagates a common misconception. Pin
Pete O'Hanlon16-Jan-14 8:47
subeditorPete O'Hanlon16-Jan-14 8:47 
GeneralMy vote of 5 Pin
hari111r15-Nov-12 22:24
hari111r15-Nov-12 22:24 
QuestionMy vote of 5 for simplicity and clarity Pin
annamalaisamy16-May-12 23:41
annamalaisamy16-May-12 23:41 
AnswerRe: My vote of 5 for simplicity and clarity Pin
PradeepDhawan2-Aug-12 21:34
PradeepDhawan2-Aug-12 21:34 
Questionmy 5 Pin
BITA Moin26-Apr-12 6:15
BITA Moin26-Apr-12 6:15 
GeneralMy vote of 4 Pin
Nivas Maran25-Apr-12 21:10
Nivas Maran25-Apr-12 21:10 
QuestionCatel Pin
Vincent Beek11-Aug-11 2:17
Vincent Beek11-Aug-11 2:17 
QuestionA tiny detail Pin
Nils Thorell25-Jul-11 4:25
Nils Thorell25-Jul-11 4:25 
Questionvote of 5 Pin
JamesBond007x19-Jul-11 15:29
JamesBond007x19-Jul-11 15:29 
AnswerRe: vote of 5 Pin
PradeepDhawan19-Jul-11 16:16
PradeepDhawan19-Jul-11 16:16 
QuestionWhat about Dialogs? Launching other views? Pin
pattyweb19-Jul-11 13:39
pattyweb19-Jul-11 13:39 
AnswerRe: What about Dialogs? Launching other views? Pin
PradeepDhawan19-Jul-11 14:14
PradeepDhawan19-Jul-11 14:14 
QuestionAdd a button to the Sample?? Pin
wim4you14-Jul-11 4:20
wim4you14-Jul-11 4:20 
AnswerRe: Add a button to the Sample?? Pin
PradeepDhawan14-Jul-11 15:24
PradeepDhawan14-Jul-11 15:24 
AnswerRe: Add a button to the Sample?? Pin
PradeepDhawan18-Jul-11 18:15
PradeepDhawan18-Jul-11 18:15 
GeneralRe: Add a button to the Sample?? Pin
wim4you19-Jul-11 1:40
wim4you19-Jul-11 1:40 
QuestionI think.... Pin
Pritesh Aryan9-Jul-11 1:47
Pritesh Aryan9-Jul-11 1:47 
AnswerRe: I think.... Pin
PradeepDhawan10-Jul-11 15:46
PradeepDhawan10-Jul-11 15:46 
AnswerRe: I think.... Pin
Pete O'Hanlon19-Jul-11 0:22
subeditorPete O'Hanlon19-Jul-11 0:22 
GeneralRe: I think.... Pin
PradeepDhawan19-Jul-11 2:10
PradeepDhawan19-Jul-11 2:10 
QuestionVote 4 Pin
Kevin Marois6-Jul-11 4:37
professionalKevin Marois6-Jul-11 4:37 
AnswerRe: Vote 4 Pin
aamironline6-Jul-11 5:35
aamironline6-Jul-11 5:35 
AnswerRe: Vote 4 [modified] Pin
PradeepDhawan6-Jul-11 15:13
PradeepDhawan6-Jul-11 15:13 
GeneralRe: Vote 4 Pin
_Maxxx_6-Jul-11 18:37
professional_Maxxx_6-Jul-11 18:37 

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.