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.
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.