ENTERPRISE

Application Patterns and Tips : Use Model-View-ViewModel in WPF

8/6/2012 5:53:56 PM
Scenario/Problem:You want to separate UI from underlying data and behavior in WPF.
Solution:As WPF has increased in popularity, the Model-View-ViewModel pattern has emerged as a variation of Model-View-Presenter, which works very well with the WPF binding system.

The ViewModel solves the problem of trying to associate WPF controls with data objects that don’t have any knowledge of UI. It maps plain data objects to data that WPF can bind to. For example, a color code in a database could be translated to a Brush for the view to use. The following sections tackle each part of this, piece by piece.

Figure 1 shows the final sample application which has two views of the data: a list of all widgets, and a view of a single widget.

Figure 1. Very simple pieces can be combined to form elegant WPF applications that are easy to extend and maintain.

Define the Model

In this case, we’ll just use objects in memory, but you could just as easily connect to a database, a web server, or a file.

enum WidgetType
{
    TypeA,
    TypeB
};

class Widget
{
    public int Id { get; set; }
    public string Name { get; set; }
    public WidgetType WidgetType { get; set; }

    public Widget(int id, string name, WidgetType type)
    {
        this.Id = id;
        this.Name = name;
        this.WidgetType = type;
    }
}
class WidgetRepository
{
    public event EventHandler<EventArgs> WidgetAdded;
    protected void OnWidgetAdded()
    {
        if (WidgetAdded != null)
        {
            WidgetAdded(this, EventArgs.Empty);
        }
    }
    private List<Widget> _widgets = new List<Widget>();

    public ICollection<Widget> Widgets
    {
        get
        {
            return _widgets;
        }
    }

    public Widget this[int index]
    {
        get
        {
            return _widgets[index];
        }
    }

    public WidgetRepository()
    {
        CreateDefaultWidgets();
    }

    public void AddWidget(Widget widget)
    {
        _widgets.Add(widget);
        OnWidgetAdded();
    }

    private void CreateDefaultWidgets()
    {
        AddWidget(new Widget(1, "Awesome widget", WidgetType.TypeA));
        AddWidget(new Widget(2, "Okay widget", WidgetType.TypeA));
        AddWidget(new Widget(3, "So-so widget", WidgetType.TypeB));
        AddWidget(new Widget(4, "Horrible widget", WidgetType.TypeB));
    }
}

					  

As you can see, this data model has no notion of anything related to WPF.

Define the ViewModel

Because we’ll have multiple ViewModel classes in this app, and they have some common functionality, let’s define a base class:

abstract class BaseViewModel : INotifyPropertyChanged
{
    private string _displayName="Unknown";

    public string DisplayName
    {
        get
        {
            return _displayName;
        }
        set
        {
            _displayName = value;
            OnPropertyChanged("DisplayName");
        }
    }

    protected BaseViewModel(string displayName)
    {
        this.DisplayName = displayName;
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    #endregion

    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

					  

WPF uses the INotifyPropertyChanged interface to know when to update views that are bound to these ViewModel objects.

The first concrete ViewModel is the WidgetViewModel:

class WidgetViewModel : BaseViewModel
{
    private Widget _widget;

    public int Id { get { return _widget.Id; } }
    public string Name { get { return _widget.Name; } }
    public string WidgetType
    {
        get
        {
            return _widget.WidgetType.ToString();
        }
    }

    public WidgetViewModel(Widget widget)
        :base("Widget")
    {
        _widget = widget;
    }
}

The other ViewModel is for the view that will show a list of all Widget objects:

class AllWidgetsViewModel : BaseViewModel
{
    private WidgetRepository _widgets;
    //this collection of view models is available to
    //the view to display however it wants
    public ObservableCollection<WidgetViewModel>
                     WidgetViewModels { get; private set; }

    public AllWidgetsViewModel(WidgetRepository widgets)
        :base("All Widgets")
    {
        _widgets = widgets;
         //the ViewModel watches the model for changes and
         //uses OnPropertyChanged to notify the view of changes
        _widgets.WidgetAdded +=
           new EventHandler<EventArgs>(_widgets_WidgetAdded);

        CreateViewModels();
    }

    void _widgets_WidgetAdded(object sender, EventArgs e)
    {
        CreateViewModels();
    }

    private void CreateViewModels()
    {
        WidgetViewModels = new ObservableCollection<WidgetViewModel>();
        foreach (Widget w in _widgets.Widgets)
        {
            WidgetViewModels.Add(new WidgetViewModel(w));
        }
        OnPropertyChanged("WidgetViewModels");
    }
}

					  

Define the View

The view involves mostly just setting up the UI and bindings to the ViewModel (see Listing 1). As you’ll see in the next section, the DataContext property for this control will be set to the ViewModel.

Listing 1. AllWidgetsView.xaml
<UserControl x:Class="MVVMDemo.AllWidgetsView"
             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"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <ListView x:Name="ListViewWidgets"
                  ItemsSource="{Binding WidgetViewModels}"
                  >
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="ID"
                                    DisplayMemberBinding="{Binding
                                                Path=Id}"/>
                    <GridViewColumn Header="Name"
                                    DisplayMemberBinding="{Binding
                                                Path=Name}" />
                    <GridViewColumn Header="Type"
                                    DisplayMemberBinding="{Binding
                                                Path=WidgetType}" />
                </GridView>
            </ListView.View>
        </ListView>
    </Grid>
</UserControl>				  

The Widget-specific view displays a single widget in a graphical way (see Listing 2).

Listing 2. WidgetGraphicView.xaml
<UserControl x:Class="MVVMDemo.WidgetGraphicView"
             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"
             mc:Ignorable="d"
             d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        <Border BorderThickness="1" BorderBrush="Black"
                VerticalAlignment="Top" HorizontalAlignment="Left">
            <StackPanel HorizontalAlignment="Left"
                        VerticalAlignment="Top">
                <Border BorderThickness="1" BorderBrush="DarkGray">
                    <TextBlock>ID: <TextBlock Text="{Binding Path=Id}"/>
                    </TextBlock>
                </Border>
                <Border BorderThickness="1" BorderBrush="DarkGray">
                    <TextBlock Foreground="Blue">Name:
                      <TextBlock Text="{Binding Path=Name}"/>
                    </TextBlock>

                </Border>
                <Border BorderThickness="1" BorderBrush="DarkGray">
                    <TextBlock Foreground="Red">Type:
                       <TextBlock Text="{Binding Path=WidgetType}"/>
                    </TextBlock>                </Border>
            </StackPanel>
        </Border>
    </Grid>
</UserControl>					  

Put Commands into the ViewModel

Now we just need to hook everything up with the MainWindow and MainWindowViewModel. The MainWindow needs to execute some commands, which should be done in the ViewModel. To do this, you can’t use the standard WPF RoutedUIEvent, but you can easily develop your own command classes. A common way to do this is to create a command object that calls a delegate you specify:

//this is a common type of class, also known as RelayCommand
class DelegateCommand : ICommand
{
    //delegates to control command
    private Action<object> _execute;
    private Predicate<object> _canExecute;

    public DelegateCommand(Action<object> executeDelegate)
        :this(executeDelegate, null)
    {
    }
    public DelegateCommand(Action<object> executeDelegate, Predicate<object> canExecuteDelegate)
    {
        _execute = executeDelegate;
        _canExecute = canExecuteDelegate;
    }
    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        if (_canExecute == null)
        {
            return true;
        }
        return _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion
}

					  

Now we can define the MainWindowViewModel:

class MainWindowViewModel : BaseViewModel
{
    private WidgetRepository _widgets = new WidgetRepository();
    private int _nextId = 5;

    //this is better than RoutedUICommand when using MVVM
    public DelegateCommand ExitCommand {get;private set;}
    public DelegateCommand OpenAllWidgetsListCommand
                                { get; private set; }
    public DelegateCommand ViewWidgetCommand { get; private set; }
    public DelegateCommand AddWidgetCommand { get; private set; }

    public ObservableCollection<BaseViewModel> OpenViews
                                               { get; private set; }

    public MainWindowViewModel()
        :base("MVVM Demo")
    {
        ExitCommand = new DelegateCommand(executeDelegate => OnClose());
        OpenAllWidgetsListCommand =
                 new DelegateCommand(executeDelegate => OpenAllWidgetsList());
        ViewWidgetCommand =
           new DelegateCommand(executeDelegate => ViewWidget());
        AddWidgetCommand =
           new DelegateCommand(executeDelegate => AddNewWidget());

        OpenViews = new ObservableCollection<BaseViewModel>();
    }

    public event EventHandler<EventArgs> Close;

    protected void OnClose()
    {
        if (Close != null)
        {
            Close(this, EventArgs.Empty);
        }
    }

    private void OpenAllWidgetsList()
    {
        OpenViews.Add(new AllWidgetsViewModel(_widgets));
    }
    private void ViewWidget()
    {
        OpenViews.Add(new WidgetViewModel(_widgets[0]));
    }

    private void AddNewWidget()
    {
        _widgets.AddWidget(new Widget(_nextId++,
                                      "New Widget",
                                      WidgetType.TypeA));
    }
}

					  

Now it’s just a matter of binding the MainView parts to properties of the ViewModel (see Listing 3).

Listing 3. Mainwindow.xaml
<Window x:Class="MVVMDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MVVMDemo"
        Title="{Binding Path=DisplayName}" Height="377" Width="627">
    <Window.Resources>
        <DataTemplate x:Key="TabControlTemplate">
            <TextBlock Text="{Binding Path=DisplayName}"/>
        </DataTemplate>
        <!-- These templates tell WPF now to
             display our ViewModel classes-->
        <DataTemplate DataType="{x:Type local:AllWidgetsViewModel}">
            <local:AllWidgetsView/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:WidgetViewModel}">
            <local:WidgetGraphicView/>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="150"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <StackPanel Grid.Column="0" >
            <Button x:Name="buttonViewAllGrid"
                    Margin="5" Command="{Binding
               Path=OpenAllWidgetsListCommand}">View All (details)
            </Button>
            <Button x:Name="buttonViewSingle"
                    Margin="5" Command="{Binding
               Path=ViewWidgetCommand}">View a widget</Button>
            <Button x:Name="buttonAddWidget"
                    Margin="5" Command="{Binding
               Path=AddWidgetCommand}">Add new Widget</Button>
            <Button x:Name="buttonExit" Margin="5" Command="{Binding
               Path=ExitCommand}">Exit</Button>
        </StackPanel>
        <TabControl HorizontalAlignment="Stretch"  Name="tabControl1" VerticalAlignment="Stretch" Grid.Column="1"
                    ItemsSource="{Binding Path=OpenViews}"
                    ItemTemplate="{StaticResource TabControlTemplate}">
        </TabControl>
    </Grid>
</Window>

					  

The line: xmlns:local="clr-namespace:MVVMDemo" brings the .NET namespace into the XML namespace local so that it can be used to refer to the controls in the XAML.

To see it all in action, look at the MVVMDemo project in the accompanying source code.

Note

The key point to MVVM is to make the view completely concerned with how data looks, never about behavior. Ideally, a view should be completely plug-and-play, with the only work being to hook up the bindings to the ViewModel.

In addition, separating all the behavior from the GUI allows you to be far more complete in unit testing. The ViewModel doesn’t care what type of view uses it—it could easily be a programmatic “view” that tests its functionality.

Other  
  •  Liquid-Metal
  •  Vigor 2850n
  •  Visual Studio 2010 : Introducing the Visual Studio Extensibility - Extending the Code Editor
  •  Visual Studio 2010 : Managing Extensions with the Extension Manager, Managing Add-Ins with the Add-In Manager
  •  Intel Xeon Phi: Coprocessor speeding at 1 teraflops in a PCIe Card
  •  Visual Studio Team System 2008 : Working with Test Results (part 2) - Build report and test result
  •  Visual Studio Team System 2008 : Working with Test Results (part 1) - Test as part of Team Foundation Server build
  •  Finance - Apple Versus Google
  •  Oracle Coherence 3.5 : Testing and debugging Coherence applications
  •  Oracle Coherence 3.5 : Accessing the data grid (part 6) - Using the Coherence API - Implementing CoherenceTarget, Testing the Cache loader
  •  Oracle Coherence 3.5 : Accessing the data grid (part 5) - Using the Coherence API - Loader design, Implementing CsvSource
  •  Oracle Coherence 3.5 : Accessing the data grid (part 4) - Using the Coherence API - The basics: NamedCache and CacheFactory
  •  Oracle Coherence 3.5 : Accessing the data grid (part 3) - Configuring Coherence
  •  Oracle Coherence 3.5 : Accessing the data grid (part 2) - Configuring the development environment
  •  Oracle Coherence 3.5 : Accessing the data grid (part 1) - Coherence console
  •  Oracle Coherence 3.5 : Installing Coherence, Starting up the Coherence cluster
  •  The Go-To Reference Design Map For The Cloud?
  •  Separating BPM and SOA Processes : SOA-Oriented Disputes with BEA
  •  Science! – Spintronics
  •  Linux - The Operating System With A Pure Heart (Part 2)
  •  
    Top 10
    Nikon 1 J2 With Stylish Design And Dependable Image And Video Quality
    Canon Powershot D20 - Super-Durable Waterproof Camera
    Fujifilm Finepix F800EXR – Another Excellent EXR
    Sony NEX-6 – The Best Compact Camera
    Teufel Cubycon 2 – An Excellent All-In-One For Films
    Dell S2740L - A Beautifully Crafted 27-inch IPS Monitor
    Philips 55PFL6007T With Fantastic Picture Quality
    Philips Gioco 278G4 – An Excellent 27-inch Screen
    Sony VPL-HW50ES – Sony’s Best Home Cinema Projector
    Windows Vista : Installing and Running Applications - Launching Applications
    Most View
    Bamboo Splash - Powerful Specs And Friendly Interface
    Powered By Windows (Part 2) - Toshiba Satellite U840 Series, Philips E248C3 MODA Lightframe Monitor & HP Envy Spectre 14
    MSI X79A-GD65 8D - Power without the Cost
    Canon EOS M With Wonderful Touchscreen Interface (Part 1)
    Windows Server 2003 : Building an Active Directory Structure (part 1) - The First Domain
    Personalize Your iPhone Case
    Speed ​​up browsing with a faster DNS
    Using and Configuring Public Folder Sharing
    Extending the Real-Time Communications Functionality of Exchange Server 2007 : Installing OCS 2007 (part 1)
    Google, privacy & you (Part 1)
    iPhone Application Development : Making Multivalue Choices with Pickers - Understanding Pickers
    Microsoft Surface With Windows RT - Truly A Unique Tablet
    Network Configuration & Troubleshooting (Part 1)
    Panasonic Lumix GH3 – The Fastest Touchscreen-Camera (Part 2)
    Programming Microsoft SQL Server 2005 : FOR XML Commands (part 3) - OPENXML Enhancements in SQL Server 2005
    Exchange Server 2010 : Track Exchange Performance (part 2) - Test the Performance Limitations in a Lab
    Extra Network Hardware Round-Up (Part 2) - NAS Drives, Media Center Extenders & Games Consoles
    Windows Server 2003 : Planning a Host Name Resolution Strategy - Understanding Name Resolution Requirements
    Google’s Data Liberation Front (Part 2)
    Datacolor SpyderLensCal (Part 1)