Saturday 8 November 2008

Model View View-Model (MVVM) in Silverlight

I watched an excellent screencast by Jason Dolinger recently, showing how to implement the Model View View-Model pattern in WPF. A lot of the documentation on the MVVM pattern seems unnecessarily complicated, but Jason's demonstration explains it very clearly.

The basic idea of MVVM is that we would like to use data binding to connect our View to our Model, but data binding is often tricky because our model doesn't have the right properties. So we create a View Model that is perfectly set up to have just the right properties for our View to bind to, and gets its actual data from the Model.

To try the technique out I decided to refactor a small piece of a Silverlight game I have written, called SilverNibbles, which is a port of the old QBasic Nibbles game, keeping the graphics fairly similar. At the top of the screen there is a scoreboard, whose role it is to keep track of the scores, number of lives, level, speed etc. This is the piece I will attempt to modify to use MVVM.

SilverNibbles Scoreboard

Let's have a look at the original code-behind for the Scoreboard user control. As you can see, there is a not lot going on here. Whenever a property on the Scoreboard is set, the appropriate changes are made to the graphical components. This is very similar to the typical code that would be written for a Windows Forms user control.

using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace SilverNibbles
{
    public partial class Scoreboard : UserControl
    {
        private int players;
        private int level;
        private int speed;
        private int jakeScore;
        private int sammyScore;

        public Scoreboard()
        {
            InitializeComponent();
        }

        public int Players
        {
            get 
            {
                return players; 
            }
            set 
            {
                players = value;
                jakeLives.Visibility = players == 2 ? Visibility.Visible : Visibility.Collapsed;
                jakeScoreLabel.Visibility = players == 2 ? Visibility.Visible : Visibility.Collapsed;
                jakeScoreTextBlock.Visibility = players == 2 ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        public int Level
        {
            get
            {
                return level;
            }
            set
            {
                level = value;
                levelTextBlock.Text = value.ToString();
            }
        }

        public int Speed
        {
            get
            {
                return speed;
            }
            set
            {
                speed = value;
                speedTextBlock.Text = value.ToString();
            }
        }

        public int SammyScore
        {
            get
            {
                return sammyScore;
            }
            set
            {
                sammyScore = value;
                sammyScoreTextBlock.Text = value.ToString();
            }
        }

        public int JakeScore
        {
            get
            {
                return jakeScore;
            }
            set
            {
                jakeScore = value;
                jakeScoreTextBlock.Text = value.ToString();
            }
        }

        public int JakeLives
        {
            get
            {
                return jakeLives.Lives;
            }
            set
            {
                jakeLives.Lives = value;
            }
        }

        public int SammyLives
        {
            get
            {
                return sammyLives.Lives;
            }
            set
            {
                sammyLives.Lives = value;
            }
        }
    }
}

Instead of having all these properties, the Scoreboard will become a very simple view. Now the code-behind of Scoreboard is completely minimalised:

namespace SilverNibbles
{
    public partial class Scoreboard : UserControl
    {
        public Scoreboard()
        {
            InitializeComponent();
        }
    }
}

Now it is the job of whoever creates Scoreboard to give it a ScoreboardViewModel as its DataContext. It doesn't even have to be a ScoreboardViewModel either, so long as it has the appropriate properties. This means that a graphic designer could use an XML file instead to create dummy data to help while designing the appearance. Here's my code elsewhere in the project that sets up the data context of the Scoreboard with its View Model.

scoreData = new ScoreboardViewModel();
scoreboard.DataContext = scoreData;

Now I simply modify the scoreData object's properties, and the Scoreboard will update its view automatically.

What has changed in the Scoreboard user control's XAML? Well first, it has Binding statements for every property that gets its value from the view model. I was also able to remove all the x:Name attributes from the markup, which is a sign that MVVM has been done right. I also needed to make my Lives property on my LivesControl user control into a dependency property to allow it to accept the binding syntax as a value.

<UserControl x:Class="SilverNibbles.Scoreboard"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:sn="clr-namespace:SilverNibbles"
     >
    <Grid x:Name="LayoutRoot" Background="LightYellow" ShowGridLines="False">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="20*" />
            <ColumnDefinition Width="35*" />
            <ColumnDefinition Width="25*" />
            <ColumnDefinition Width="10*" />
        </Grid.ColumnDefinitions>
        
        <!-- row 0 -->
        <TextBlock 
            Grid.Row="0" 
            Grid.Column="0"
            Text="High Score" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen" />
        <TextBlock 
            Grid.Row="0" 
            Grid.Column="1"
            Margin="5,0,5,0"
            Text="{Binding Record}" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen" />
        
        <TextBlock 
            Grid.Column="2" 
            Text="Level" 
            HorizontalAlignment="Right" 
            Margin="5,0,5,0"
            FontSize="18"
            FontFamily="teen bold.ttf#Teen" />
        <TextBlock 
            Grid.Column="3" 
            Margin="5,0,5,0"
            Text="{Binding Level}" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen" />
                
        <!-- row 1 -->        
        <TextBlock 
            Grid.Row="1"
            Text="Sammy" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen" />
       <sn:LivesControl
            Grid.Row="1"
            Grid.Column="1"
            HorizontalAlignment="Right"
           Fill="{StaticResource SammyBrush}"
           Lives="{Binding SammyLives}"
             />        
       <TextBlock 
            Grid.Row="1"
            Grid.Column="1"
            Margin="5,0,5,0"
            Text="{Binding SammyScore}" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen"
            />
        
       <TextBlock 
           Grid.Row="1" 
           Grid.Column="2" 
            Text="Speed" 
            HorizontalAlignment="Right" 
            Margin="5,0,5,0"
            FontSize="18"
            FontFamily="teen bold.ttf#Teen"
           />
        <TextBlock 
            Grid.Row="1"
            Grid.Column="3" 
            Margin="5,0,5,0"
            Text="{Binding Speed}" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen"
            />
        
        <!-- row 2 -->
        <TextBlock 
            Grid.Row="2"
            Grid.Column="0"
            Text="Jake" 
            FontSize="18"
            FontFamily="teen bold.ttf#Teen"
            Visibility="{Binding JakeVisible}"            
            />
        <sn:LivesControl
            Grid.Row="2"
            Grid.Column="1"
            HorizontalAlignment="Right"
            Fill="{StaticResource JakeBrush}"
            Visibility="{Binding JakeVisible}"
            Lives="{Binding JakeLives}"
             />
       <TextBlock 
            Grid.Row="2"
            Grid.Column="1"
            FontSize="18"
            Margin="5,0,5,0"
            Text="{Binding JakeScore}" 
            FontFamily="teen bold.ttf#Teen"
            Visibility="{Binding JakeVisible}"
           />
    </Grid>
</UserControl>

Here's what my ScoreboardViewModel looks like. The main thing to notice is that I have implemented the INotifyPropertyChanged interface. I rather lazily raise the changed event every time the setter is called irrespective of whether the value really changed. Notice also I have created a JakeVisible property. This highlights how the View Model can be used to create properties that are exactly what the View needs.

namespace SilverNibbles
{
    public class ScoreboardViewModel : INotifyPropertyChanged
    {
        private int players;
        private int level;
        private int speed;
        private int jakeScore;
        private int sammyScore;

        public int Players
        {
            get
            {
                return players;
            }
            set
            {
                players = value;
                RaisePropertyChanged("Players");
                RaisePropertyChanged("JakeVisible");
            }
        }

        public Visibility JakeVisible
        {
            get
            {
                return players == 2 ? Visibility.Visible : Visibility.Collapsed;
            }
        }

        public int Level
        {
            get
            {
                return level;
            }
            set
            {
                level = value;
                RaisePropertyChanged("Level");
            }
        }

        public int Speed
        {
            get
            {
                return speed;
            }
            set
            {
                speed = value;
                RaisePropertyChanged("Speed");
            }
        }

        public int SammyScore
        {
            get
            {
                return sammyScore;
            }
            set
            {
                sammyScore = value;
                RaisePropertyChanged("SammyScore");
            }
        }

        public int JakeScore
        {
            get
            {
                return jakeScore;
            }
            set
            {
                jakeScore = value;
                RaisePropertyChanged("JakeScore");
            }
        }

        int jakeLives;
        int sammyLives;

        public int JakeLives
        {
            get
            {
                return jakeLives;
            }
            set
            {
                jakeLives = value;
                RaisePropertyChanged("JakeLives");
            }
        }

        public int SammyLives
        {
            get
            {
                return sammyLives;
            }
            set
            {
                sammyLives = value;
                RaisePropertyChanged("SammyLives");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void RaisePropertyChanged(string property)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(property));
            }
        }
    }
}

I think there are alternatives to using INotifyPropertyChanged such as creating dependency properties or inheriting from DependencyObject. I don't know what the advantages or disadvantages of taking that approach would be. That is something for a future investigation.

There are lots of other parts of the SilverNibbles application that could be refactored to use this pattern, but that is also a task for another day. View the code at CodePlex, and play SilverNibbles here.

4 comments:

Donn Felker said...

I do agree, the explanations of MVVM are overly complicated for no reason. Its a very very simple pattern when you get down to it.

Anonymous said...
This comment has been removed by a blog administrator.
Anonymous said...

I have read many articles about MVVM, written systems incorporating it, and fully agree that to make it a code reviewable pattern that x:name should not be needed in the view or used in the code behind.

AlaskanRogue

nandarsync said...

A nice post to make the concept simply understandable rather explaining this and that and making things complex. Thanks Mark.NET.