Im früheren Artikel MVC Pattern mit WPF verwenden wurde bereits gezeigt, wie das MVC-Pattern zusammen mit WPF verwendet werden kann. Dieser Artikel möchte sich dem MVVM Pattern widmen und anhand eines Beispiels zeigen, wie dieses Pattern eingesetzt werden kann.
Das MVC-Pattern besteht aus einem Model, einer View und einem Controller. Das MVVM ist ähnlich aufgebaut, wobei jedoch der Controller durch ein ViewModel ersetzt wird:

Die View ist nach wie vor der Part, der für die Visualisierung zuständig ist. Das Model liefert die Daten. Das ViewModel besitzt nun Kenntnis über die View und dem Model, kann also Daten entsprechend weiterreichen. Im Normalfall wird das ViewModel per Data Binding an die View gebunden. Daraus entsteht zwar eine enge Kopplung, wobei diese nicht zwangsweise auch an das Model weitergegeben werden muss.
Auch durch dieses Pattern besteht eine verbesserte Unit-Testbarkeit gegenüber der herkömmlichen Code-Behind-Variante. Ebenfalls kann die Implementierung auf unterschiedliche Personen/Teams verteilt werden.
Wie dieses Pattern nun konkret implementiert werden kann, soll nachfolgendes Beispiel zeigen.
Beispielimplementierung
Eine kleine Beispielanwendung soll Personen in einer Liste darstellen. Zudem soll es ermöglicht werden, diese in einer Detailansicht zu öffnen und zu editieren. Zusätzlich sollen in der Detailansicht der Person zugewiesene Adressen in einer Liste angezeigt werden. Bevor wir zum Sourcecode übergehen, hier (rudimentäre) Screenshots der Anwendung, damit man sich darunter mehr vorstellen kann:


Hierfür werden die notwendigen Basisklassen (Model) benötigt:
Adressen-Klasse:
public class Address
{
public String Street { get; set; }
public String StreetNumber { get; set; }
public String Zip { get; set; }
public String City { get; set; }
public String Country { get; set; }
public Address(string street, string streetNumber, string zip, string city)
{
Street = street;
StreetNumber = streetNumber;
Zip = zip;
City = city;
}
public Address(string street, string streetNumber, string zip, string city, string country)
: this(street, streetNumber, zip, city)
{
Country = country;
}
}
Personen-Klasse:
public class Person
{
private List<Address> _addresses = new List<Address>();
public String FirstName { get; set; }
public String LastName { get; set; }
public DateTime Birthday { get; set; }
public IList<Address> Addresses
{
get { return _addresses; }
}
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
public Person(string firstName, string lastName, DateTime birthDay)
: this(firstName, lastName)
{
Birthday = birthDay;
}
}
Für dieses kleine Beispiel ist keine Datenbank-Anbindung vorgesehen. Stattdessen wird ein Mock-Objekt verwendet, welches Beispieldaten generiert und zur Verfügung stellt (also ein Model erstellt):
public static class Database
{
public static Address[ GetAddresses()
{
Address[ address = new Address[
{
new Address("Eine Strasse", "1", "10000", "Eine Stadt"),
new Address("Eine andere Straße", "2", "20000", "Eine andere Stadt")
};
return address;
}
public static Person[ GetPeople()
{
Person[ people = new Person[
{
new Person("Vorname 1", "Nachname 1"),
new Person("Vorname 2", "Nachname 2")
};
return people;
}
public static Person[ GetCompletePeople()
{
Person[ p = GetPeople();
Address[ a = GetAddresses();
foreach (Person tempPerson in p)
foreach (Address tempAddress in a)
tempPerson.Addresses.Add(tempAddress);
return p;
}
}
Damit wäre alles für unser notwendiges Model erledigt.
In weiterer Folge wird sowohl für die initiale Anzeige der Personen-Liste, als auch für die Detailansicht die jeweils notwendige View erstellt. Dabei handelt es sich um ein einfaches Steuerelement und um ein Fenster.
Personen-List-Control
<UserControl x:Class="DotNetGui.MVVMDemo.PeopleList.PeopleListControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:model="clr-namespace:DotNetGui.MVVMDemo.PeopleList.ViewModel"
Height="Auto" Width="Auto">
<UserControl.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</UserControl.Resources>
<DockPanel LastChildFill="True">
<ListView DataContext="{Binding Path=.}" ItemsSource="{Binding Path=People}">
<ListView.View>
<GridView>
<GridViewColumn Header="FirstName" DisplayMemberBinding="{Binding FirstName, Mode=TwoWay}"/>
<GridViewColumn Header="LastName" DisplayMemberBinding="{Binding LastName, Mode=TwoWay}"/>
<GridViewColumn Header="Birthday" DisplayMemberBinding="{Binding Birthday, Mode=TwoWay}"/>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</UserControl>
Personen-Editor
<Window x:Class="DotNetGui.MVVMDemo.PersonEditor.PersonEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Edit Person" Height="Auto" Width="Auto" SizeToContent="WidthAndHeight">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Labels -->
<TextBlock Text="First Name" Grid.Column="0" Grid.Row="0"/>
<TextBlock Text="last Name" Grid.Column="0" Grid.Row="1"/>
<TextBlock Text="Birthday" Grid.Column="0" Grid.Row="2"/>
<!-- Edit -->
<TextBox Text="{Binding Path=.FirstName}" Grid.Column="1" Grid.Row="0"/>
<TextBox Text="{Binding Path=.LastName}" Grid.Column="1" Grid.Row="1"/>
<TextBox Text="{Binding Path=.Birthday}" Grid.Column="1" Grid.Row="2"/>
<!-- Addresses -->
<ListView DataContext="{Binding Path=.}" ItemsSource="{Binding Path=Addresses}" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2">
<ListView.View>
<GridView>
<GridViewColumn Header="Street" DisplayMemberBinding="{Binding Street, Mode=TwoWay}"/>
<GridViewColumn Header="StreetNumber" DisplayMemberBinding="{Binding Path=StreetNumber, Mode=TwoWay}"/>
<GridViewColumn Header="Zip" DisplayMemberBinding="{Binding Path=Zip, Mode=TwoWay}"/>
<GridViewColumn Header="City" DisplayMemberBinding="{Binding Path=City, Mode=TwoWay}"/>
<GridViewColumn Header="Country" DisplayMemberBinding="{Binding Path=Country, Mode=TwoWay}"/>
</GridView>
</ListView.View>
</ListView>
<!-- Buttons -->
<Button Content="Ok" Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="2" Click="Ok_Click"/>
</Grid>
</Window>
Hierbei wird mit einfachen Mitteln (es werden keine Styles etc. verwendet) die Oberfläche designed und alle notwendigen Informationen für das Data Binding definiert.
Das Personen-List-Control besitzt keinen Sourcecode in der Code Behind Datei. Im Personen-Editor wird lediglich beim Button-Click das Fenster geschlossen. Weiterer Sourcecode ist nicht notwendig.
Im nächsten Schritt müssen die ViewModels definiert werden, da diese Aufgaben übernehmen und auch für das Data Binding von Relevanz sind. Insgesamt sind 3 ViewModels notwendig:
- AddressViewModel
- PersonViewModel
- PersonListViewModel
Die ersten beiden ViewModels werden für den Editor verwendet. Das dritte ViewModel dient dem Personen-List-Control für die Anzeige der Personen-Liste. Von Wichtigkeit für dieses Beispiel sind die beiden letzten ViewModels.
PersonViewModel
Das PersonViewModel kapselt das Model und implementiert zusätzlich das Interface INotifyPropertyChanged. Damit können Änderungen an den Daten festgestellt und für das Data Binding angezeigt werden. Ebenfalls werden in diesem Fall sämtliche Änderungen im ViewModel sofort an das Model weitergereicht. Dies kann gegebenenfalls entkoppelt gelöst werden.
public class PersonViewModel : INotifyPropertyChanged
{
private Person _person;
private ObservableCollection<AddressViewModel> _addresses = new ObservableCollection<AddressViewModel>();
public PersonViewModel(Person person)
{
_person = person;
foreach (Address tempAddress in _person.Addresses)
_addresses.Add(new AddressViewModel(tempAddress));
}
public String FirstName
{
get { return _person.FirstName; }
set
{
if (_person.FirstName != value)
{
_person.FirstName = value;
OnPropertyChanged("FirstName");
}
}
}
public string LastName
{
get { return _person.LastName; }
set
{
if (_person.LastName != value)
{
_person.LastName = value;
OnPropertyChanged("LastName");
}
}
}
public DateTime Birthday
{
get { return _person.Birthday; }
set
{
if (_person.Birthday != value)
{
_person.Birthday = value;
OnPropertyChanged("Birthday");
}
}
}
public ObservableCollection<AddressViewModel> Addresses
{
get { return _addresses; }
}
public bool IsSelected { get; set; }
#region INotifyPropertyChanged Members
protected virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
PersonListViewModel
Das PersonListViewModel arbeit mit einer ObservableCollection (notwendig für das Data Binding) und hält Objekte vom Type PersonViewModel. Wichtig ist hierbei, dass das ViewModel verwendet wird und nicht das Model selbst, da das ViewModel vom Model enkoppelt sein könnte und mit zusätzlicher Funktionalität ankommt (siehe INotifyPropertyChanged). Weiters stellt das PersonListViewModel einen Command für das Editieren des gewählten Datensatzes bereit. Dieser Command implementiert das Interface ICommand, wodruch ein Standard-WPF-Command implementiert wird. Dieser Command stellt die beiden Methoden CanExecute und Execute zur Verfügung. Erstere wird verwendet, um die Editier-Möglichkeit zu aktivieren bzw. zu deaktiveren - abhängig davon, ob ein Item ausgewählt wurde oder nicht. Innerhalb der Execute-Methode wird das ausgewählte Item in der Detailansicht geöffnet und kann somit bearbeitet werden.
public class PersonListViewModel
{
private ObservableCollection<PersonViewModel> _people = new ObservableCollection<PersonViewModel>();
private ICommand _editCommand;
public PersonListViewModel(Person[ people)
{
foreach (Person tempPerson in people)
_people.Add(new PersonViewModel(tempPerson));
_editCommand = new EditCommand(this);
}
public ObservableCollection<PersonViewModel> People
{
get { return _people; }
}
public ICommand Edit
{
get { return _editCommand; }
}
private class EditCommand : ICommand
{
PersonListViewModel _listViewModel;
public EditCommand(PersonListViewModel listViewModel)
{
_listViewModel = listViewModel;
}
#region ICommand Members
public bool CanExecute(object parameter)
{
foreach (PersonViewModel model in _listViewModel.People)
if (model.IsSelected)
return true;
return false;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
PersonViewModel selectedModel = null;
foreach (PersonViewModel model in _listViewModel.People)
{
if (model.IsSelected)
{
selectedModel = model;
break;
}
}
if (selectedModel != null)
{
PersonEditor.PersonEditor editor = new DotNetGui.MVVMDemo.PersonEditor.PersonEditor();
editor.DataContext = selectedModel;
editor.ShowDialog();
}
}
#endregion
}
}
Was nun noch fehlt ist das Hauptfenster für die WPF-Applikation selbst. Dieses ist ebenfalls recht einfach deklariert:
<Window x:Class="DotNetGui.MVVMDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:DotNetGui.MVVMDemo.PeopleList"
Title="WPF MVVM Demo" Height="Auto" Width="Auto" SizeToContent="WidthAndHeight">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ui:PeopleListControl DataContext="{Binding Path=.}" Grid.Row="0"/>
<StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Grid.Row="1">
<Button Content="Edit" Click="Edit_Click"/>
<Button Content="Close" Click="Close_Click"/>
</StackPanel>
</Grid>
</Window>
Zusätzlich befindet sich hier etwas Sourcecode in der Code Behind Datei:
public partial class MainWindow : Window
{
private PersonListViewModel _personListViewModel = null;
public MainWindow()
{
InitializeComponent();
Person[ people = Database.GetCompletePeople();
_personListViewModel = new PersonListViewModel(people);
this.DataContext = _personListViewModel;
}
private void Edit_Click(object sender, RoutedEventArgs e)
{
_personListViewModel.Edit.Execute(null);
}
private void Close_Click(object sender, RoutedEventArgs e)
{
Application.Current.Shutdown(0);
}
}
Was passiert hier? Es wird lediglich das Model vom Mock-Objekt bezogen, das notwendige PersonListViewModel instanziert und der Datenkontext für das Hauptfenster gesetzt. Auf diesen Kontext greifen die restlichen Views zu und können somit ihre Daten anzeigen. Fertig ist die Beispielanwendung.
Download Beispielanwendung