MVVM und DataBinding
Motivation
Wenn man Anwendungen unter Windows Forms entwickelt, ist man es gewohnt, Code-Behind-Dateien für die Benutzeroberfläche zu erstellen, welche mit zunehmendem Funktionsumfang der Anwendung immer weniger lesbar und testbar sind. Dieser Code, bestehend aus Ereignis-Handlern, Update- und Initialisierungs-Methoden, ist sowohl abhängig von der Benutzeroberfläche, als auch von den Klassen der eigentlichen Anwendung. Er muss jedesmal angepasst werden, sobald eine Änderung an der Anwendung als auch an der Oberfläche vorgenommen wird. Und nicht zuletzt kann dieser Code auch nicht mit UnitTests automatisiert getestet werden, da er abhängig von der Benutzeroberfläche ist.
Als Neueinsteiger in die Anwendungsentwicklung mit WPF tendiert man oft dazu, weiterhin diesen Programmierstil zu pflegen. Immerhin funktioniert es ja auch, und man ist es nicht anders gewohnt. Dabei verzichtet man allerdings auf die großartigen Errungenschaften moderner Benutzeroberflächen, wie das DataBinding. WPF ist schließlich von Microsoft unter anderem dafür entwickelt worden, um die Steuerung der Oberfläche von der Anwendung zu trennen und damit die Entwicklung zu vereinfachen. Um diese Vorteile nutzen zu können, wird ein Verständnis des MVVM-Entwurfsmusters und die konsequente Verwendung von DataBinding vorausgesetzt. In diesem Artikel wird daher das Konzept hinter DataBinding und MVVM anhand von einigen Beispielen erläutert.
Die Vorteile des MVVM-Entwurfsmusters sind unter anderem: * Die Anwendungslogik wird unabhängig von der Darstellung in der Benutzeroberfläche entwickelt. Dadurch ist die Benutzeroberfläche leicht austauschbar und kann von einem Designer erstellt werden, der den Code der Anwendung nicht kennen muss.
Die gesamte Anwendungslogik kann mit Unit-Tests automatisiert getestet werden, da die View dafür nicht instanziiert werden muss. Mit der Code-Behind-Lösung sind dagegen manuelle UI-Tests notwendig, die viel mehr Zeit in Anspruch nehmen.
Die ViewModels können für unterschiedliche Views wiederverwendet werden. Zusammen mit dem ohnehin schon reduzierten Code gegenüber der Code-Behind-Lösung spart das enorm viel Zeit beim Entwickeln.
Der entstehende Code ist wesentlich besser lesbar, da alle Funktionalität im ViewModel gekapselt und nicht auf mehrere Stellen im Code-Behind verteilt ist. Das (Wieder-)Einarbeiten in einen bestehenden Code wird dadurch wesentlich erleichtert.
Die Einarbeitung in MVVM-Frameworks anderer Plattformen, wie z.B. Java, JavaScript oder Android, fällt leichter
Gliederung * 1. DataBinding
Weeks of programming can save you hours of planning
1. DataBinding
Wenn man Benutzeroberflächen unter Windows Forms ohne DataBinding erstellt, ist es erforderlich, sich um die Darstellung und Aktualisierung der Oberfläche selbst zu kümmern. Dazu muss man Programmcode schreiben, der * die Steuerelemente mit den voreingestellten Werten initialisiert,
die Steuerelemente aktualisiert, wenn sich diese Werte geändert haben, und
die Werte in der Anwendung aktualisiert, wenn sie vom Benutzer in der Oberfläche geändert wurden.
Der Code, um einen Namen in einer TextBox darzustellen, würde dann ungefähr so aussehen:
public MainWindow()
{
InitializeComponent();
textBoxUserName.Name = "Max Mustermann"; // Voreingestellten Wert setzen
}
// Benutzeroberfläche aktualisieren, wenn sich der Name in der Anwendung geändert hat
public void UpdateName(string newName)
{
textBoxUserName.Name = newName;
}
// Anwendung darüber informieren, dass der Benutzer einen neuen Namen eingegeben hat
private void textBoxUserName_Changed(object sender, EventArgs e)
{
if (NameChanged != null)
NameChanged.Invoke(this, new EventArgs());
}
public event EventHandler NameChanged;
Das muss für jeden einzelnen Wert getan werden, der in der Benutzeroberfläche dargestellt wird - aber auch für alle Auflistungen von Objekten, bei denen sich beispielsweise die Reihenfolge oder die Anzahl der Objekte ändern kann. Das führt sehr schnell zu unüberschaubaren Code, der eine schnelle Anpassung fast unmöglich macht. Ein weiteres Problem ist, dass der Code sowohl die Anwendungslogik als auch die Benutzeroberfläche kennen muss. Dadurch ist dieser Code schwer zu testen.
Die gleiche Funktionalität erreicht man in WPF mittels DataBinding auf einfacherem Weg:
<TextBox Text="{Binding Name}" />
DataBinding übernimmt diese Aufgaben, indem es im Hintergrund automatisch die Benutzeroberfläche aktualisiert. Alles, was man dafür tun muss, ist, die Benutzeroberfläche darüber zu informieren, wenn sich ein Wert in der Anwendung geändert hat. Für diesen Zweck gibt es im Framework bereits die INotifyPropertyChanged- und INotifyCollectionChanged-Schnittstellen. Diese Schnittstellen werden dann von den Klassen des sogenannten ViewModels implementiert.
Weeks of programming can save you hours of planning
2. Model - View - ViewModel (MVVM)
Das MVVM-Pattern bietet die Möglichkeit, die Benutzeroberfläche ("View") von den Anwendungsdaten ("Model") zu trennen, und zwar mit Hilfe des sogenannten "ViewModel". Etwas technischer ausgedrückt ist das ViewModel in der Software-Architektur also eine zusätzliche Abstraktionsschicht zwischen der eigentlichen Anwendungslogik und der Benutzeroberfläche.
Das ViewModel hat dabei folgende Aufgaben: * Alle erforderlichen Daten für die Anzeige bereitstellen: Das ViewModel muss der Benutzeroberfläche alle Daten zur Verfügung stellen, die sie zur Anzeige benötigt. Dazu gehören neben den Daten, die dem Benutzer angezeigt werden sollen, auch die Aktionen, die der Benutzer ausführen kann (siehe Abschnitt Commands), sowie zusätzliche Daten für Auswahllisten. Will man beispielsweise eine ComboBox für eine Länderauswahl darstellen, muss dafür über das ViewModel eine Liste mit allen auswählbaren Ländern bereitgestellt werden.
Die Benutzeroberfläche über geänderte Werte informieren: Das ViewModel löst die jeweiligen Events aus den INotifyPropertyChanged- und INotifyCollectionChanged-Schnittstellen aus, damit die Anzeige automatisch aktualisiert werden kann.
Kapselung der Logik für die Benutzeroberfläche: Das ViewModel enthält sämtlichen Code, der für die korrekte Darstellung der Benutzeroberfläche notwendig ist, beispielsweise die Aktivierung und Deaktivierung von Steuerelementen oder ganzen Bereichen der Oberfläche sowie Benutzeraktionen (Commands). Damit befindet sich die gesamte Logik der Anwendungsoberfläche im ViewModel, und kann dort unabhängig von anderen Teilen der Anwendung mit Hilfe von Unit-Tests getestet werde.
Eine Anwendung hat typischerweise ein MainViewModel, das an das Hauptfenster gebunden ist. Je nach Komplexität der Benutzeroberfläche gibt es dann weitere (Unter-)ViewModels für spezielle Views, die im MainViewModel enthalten sein können.
2.1 Implementierung des ViewModels
Um ein ViewModel zu erstellen, benötigt man zunächst eine Basisklasse, welche die INotifyPropertyChanged-Schnittstelle implementiert. Die Implementierung kann man selbst einmal vornehmen, oder man verwendet ein MVVM-Framework, das solche vordefinierten Klassen bereits zur Verfügung stellt. Eine solche Basisklasse sieht dann folgendermaßen aus:
public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Um ein ViewModel zu erstellen, leitet man einfach von dieser Basisklasse ab, und ruft jedesmal die OnPropertyChanged-Methode auf, wenn sich eine Eigenschaft geändert hat. Dieser Methode muss der Name der Eigenschaft übergeben werden, damit die Benutzeroberfläche weiß, welches Steuerelement aktualisiert werden muss.
public class EmployeeViewModel : BaseViewModel
{
private string firstName;
private string lastName;
public string FirstName
{
get { return firstName; }
set
{
if (value != firstName)
{
firstName = value;
OnPropertyChanged("FirstName");
OnPropertyChanged("FullName");
}
}
}
public string LastName
{
get { return lastName; }
set
{
if (value != lastName)
{
lastName = value;
OnPropertyChanged("LastName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get { return string.Format("{0} {1}", FirstName, LastName); }
}
}
Es gibt auch Implementierungen, welche eine Überladung der OnPropertyChanged-Methode anbieten, die als Parameter anstatt des Eigenschafts-Namens eine Lambda-Expression erwarten. Anstatt OnPropertyChanged("LastName")
schreibt man dort OnPropertyChanged(() => LastName)
. Diese Schreibweise ist gegenüber der Variante mit dem Namen als Zeichenkette vorzuziehen, da man so auf "Magic Strings" verzichtet und sicherstellen kann, dass man bei einem Refactoring nicht eine Stelle vergessen hat umzubenennen. Benennt man nämlich im Beispiel oben die Eigenschaft LastName in FamilyName um, dann wird OnPropertyChanged(() => LastName)
einen Compilerfehler erzeugen, OnPropertyChanged("LastName")
dagegen eine lauffähige, aber nicht funktionierende Anwendung ohne einen Hinweis auf die Fehlerursache. Ab C# Version 5 gibt es auch die nameof-Expression, welche die gleichen Vorteile bietet und folgendermaßen verwendet werden kann: OnPropertyChanged(nameof(LastName))
. Auch das ab Framwork-Version 4.5 zur Verfügung stehende CallerMemberName-Attribut kann den Aufruf in vielen Fällen noch weiter vereinfachen, wie in diesem Beispiel demonstriert wird.
2.2 Instanziierung des ViewModels
Das ViewModel wird im Konstruktor der View instanziiert oder per Konstruktor-Parameter übergeben. Auch in XAML ist die Instanziierung möglich. Wichtig dabei ist, die DataContext-Eigenschaft der View auf die Instanz des ViewModels zu setzen:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.DataContext = new MainViewModel();
}
}
Oder alternativ mit Hilfe von XAML:
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
__
2.3 Verwendung des ViewModels
Das ViewModel aus dem letzten Beispiel lässt sich dann wie folgt verwenden, um Vor- und Nachnamen zu bearbeiten:
<TextBox Text="{Binding FirstName}" />
<TextBox Text="{Binding LastName}" />
<Label Content="{Binding FullName}" />
Neben der Anzeige und der Bearbeitung von Daten lässt sich das ViewModel auch verwenden, um die Darstellung der Oberfläche zu verändern. So kann man beispielsweise eine Schaltfläche deaktivieren oder ausblenden, solange noch kein Name eingegeben wurde. Folgendes Beispiel geht davon aus, dass es im ViewModel eine Eigenschaft HasFirstAndLastName vom Typ bool
existiert, die angibt, ob bereits ein Vor- und Nachname eingegeben wurde:
<Button Content="Speichern" IsEnabled="{Binding HasFirstAndLastName}" />
Will man die Schaltfläche komplett ausblenden anstatt sie nur zu deaktivieren, dann kann man die gleiche ViewModel-Eigenschaft an die Visibility-Eigenschaft der Schaltfläche binden. Dazu benötigt man jedoch einen Converter, der zwischen den Datentypen bool
und Visibility konvertiert:
<Button Content="Speichern" Visibility="{Binding HasFirstAndLastName, Converter={StaticResource BoolToVisibilityConverter}" />
So vermeidet man, dass im C#-Code die Anzeige und das Aussehen der Steuerelemente geändert werden müssen. Und nur indem man diese Abhängigkeit vermeidet, erhält man ein ViewModel und eine View, die beide unabhängig voneinander entwickelt und getestet werden können. Weitere Möglichkeiten, die Darstellung der Benutzeroberfläche an den jeweiligen Programmzustand anzupassen, finden sich in den Abschnitten Styles und Trigger sowie Templates.
2.4 ViewModel-Auflistungen
Eine eigene Implementierung der INotifyCollectionChanged-Schnittstelle kann man sich übrigens sparen, denn der Framework enthält für diesen Zweck bereits die ObservableCollection-Klasse. Verwendet man im ViewModel für Auflistungen eine ObservableCollection anstatt einer Liste oder eines Arrays, dann werden alle daran gebundenen Steuerelemente automatisch aktualisiert, wenn Elemente entfernt, hinzugefügt oder verschoben werden.
Hier ein Beispiel mit einer Auflistung von ViewModels:
public class CompanyViewModel : BaseViewModel
{
private ObservableCollection<EmployeeViewModel> employees = new ObservableCollection<EmployeeViewModel>();
public ObservableCollection<EmployeeViewModel> Employees
{
get { return employees; }
set
{
if (value != employees)
{
employees = value;
OnPropertyChanged("Employees");
}
}
}
}
Diese Eigenschaft lässt sich dann an ein ListView-Control binden, um den Vor- und Nachnamen aller Mitarbeiter anzuzeigen und eine Auswahl zu ermöglichen:
<ListView ItemsSource="{Binding Employees}" DisplayMemberPath="FullName" SelectedItem="{Binding SelectedEmployee}" />
Weeks of programming can save you hours of planning
3. Commands
Eine Benutzeroberfläche muss nicht nur Daten darstellen und eine Bearbeitungsmöglichkeit anbieten, sondern auch Aktionen wie "Speichern", "Löschen" oder "Neuen Benutzer hinzufügen" auslösen können. Während man unter Windows Forms für diesem Zweck einen EventHandler für das Click-Event verwendet hat, bietet WPF die Möglichkeit, auch solche Aktionen direkt mittels DataBinding an die jeweiligen Steuerelemente zu binden. Dafür gibt es bei Steuerelementen wie Buttons oder MenuItems bereits die Command-Eigenschaft. Der eigentliche Code zum Ausführen der Aktion wird vom ViewModel bereitgestellt, mit Hilfe einer Klasse, welche die ICommand-Schnittstelle implementiert. Diese Schnittstelle enthält weiterhin einen Mechanismus, der angibt, ob die Aktion momentan zur Verfügung steht, um die damit verbundene Schaltfläche automatisch zu aktivieren oder zu deaktivieren. Fertige Implementierung der ICommand-Schnittstelle bietet jeder MVVM-Framework an, ist aber auch schnell selbst implementiert:
public class RelayCommand : ICommand
{
private Action<object> execute;
private Func<object, bool> canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return this.canExecute == null || this.canExecute(parameter);
}
public void Execute(object parameter)
{
this.execute(parameter);
}
}
In diesem Beispiel wird das CompanyViewModel um ein Command erweitert, welche die Funktion zum Hinzufügen eines neuen Mitarbeiters zur Verfügung stellt:
public class CompanyViewModel : BaseViewModel
{
public ObservableCollection<EmployeeViewModel> Employees
{
// ...
}
public ICommand AddNewEmployeeCommand { get; set; }
public CompanyViewModel()
{
AddNewEmployeeCommand = new RelayCommand(parameter => Employees.Add(new EmployeeViewModel()), parameter => true);
}
}
Im XML-Code erstellt man dann die Schaltfläche und bindet das AddNewEmployeeCommand an deren Command-Eigenschaft:
<Button Content="Mitarbeiter hinzufügen" Command="{Binding AddNewEmployeeCommand}" />
Für Aktionen, die einen Parameter mit zusätzlichen Informationen benötigen, steht die CommandParameter-Eigenschaft zur Verfügung. Mit diesem Code wird einem RemoveEmployeeCommand als Parameter das ViewModel des Mitarbeiters übergeben, welches aus der Liste entfernt werden soll:
<ItemsControl ItemsSource="{Binding Employees}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Label Content="{Binding FullName}" />
<Button Content="Mitarbeiter entfernen"
Command="{Binding DataContext.RemoveEmployeeCommand, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
In diesem Beispiel wird von verschiedenen DataBinding-Möglichkeiten Gebrauch gemacht, um die Liste an ein ItemsControl, den Mitarbeiternamen an eine Eigenschaft eines Listenelements, den CommandParameter an das Listenelement selbst, sowie das Command an eine ViewModel-Eigenschaft des beinhaltenden Steuerelements zu binden. Hier eine (unvollständige) Übersicht über die verschiedenen Möglichkeiten: * Path: Gibt den Pfad einer Eigenschaft an. Die Angabe ist optional, d.h. "{Binding Path=Content}"
ist gleichbedeutend mit "{Binding Content}"
.
ElementName: Bindet an ein benanntes Steuerelement der View anstatt an den DataContext.
RelativeSource: Bindet an ein übergeordnetes Steuerelement, welches das aktuelle Element beinhaltet.
StaticResource, DynamicResource: Verweist auf eine Resource in der Anwendung.
TemplateBinding: Bindet innerhalb eines Templates (siehe Abschnitt Templates) an eine Eigenschaft des Steuerelements.
Converter: Verwendet einen Converter, um zwischen dem Datentyp der ViewModel- und der Steuerelement-Eigenschaft zu konvertieren.
StringFormat: Stellt einen Wert in einem bestimmten Format dar, ähnlich wie die String.Format-Methode.
Weitere Möglichkeiten: Bindung als Markuperweiterung
Weeks of programming can save you hours of planning
4. Styles und Trigger
Während man unter Windows Forms das Aussehen (Größe, Hintergrundfarbe, Textfarbe, Textgröße usw.) jedes Steuerelements einzeln per Code anpassen musste, gibt es in WPF Styles und Templates. Vom Konzept her vergleichbar mit HTML-Templates und CSS-Stilen, bieten sie jedoch wesentlich mehr Flexibilität, wirken daher allerdings auf den ersten Blick auch etwas komplizierter.
Jedes Steuerelement besitzt ein Template, das die einzelnen Unterelemente für die Darstellung definiert. Ein Template für ein MenuItem hat beispielsweise einen Rahmen für die Hintergrundfarbe, einen Text, ein Icon und evtl. einen Tastatur-Shortcut. WPF bietet hier die Freiheit, jedes dieser Templates durch eigene Varianten zu überschreiben.
Jedes Steuerelement besitzt weiterhin einen Style, mit dem die Darstellungseigenschaften des verwendeten Templates eingestellt werden können. Dazu gehören üblicherweise die oben schon genannten Eigenschaften Größe, Hintergrundfarbe, Textfarbe, Textgröße usw.
Will man beispielsweise eine Schaltfläche mit grünem Hintergrund erstellen, dann kann der Style folgendermaßen gesetzt werden:
<Button Content="Alles im grünen Bereich">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
</Style>
</Button.Style>
</Button>
Jeder Style kann mehrere Setter enthalten, von denen jeder eine bestimmte Eigenschaft (hier: Background
) auf einen bestimmten Wert setzt (hier: Colors.Green
). Damit man den Style nicht bei jedem Steuerelement neu definieren muss, gibt es die Möglichkeit, den Style in den Resourcen zu hinterlegen und dann auf beliebig viele Steuerelemente anzuwenden:
<Window.Resources>
<Style x:Key="GreenButton" TargetType="{x:Type Button}">
<Setter Property="Background" Value="Green" />
</Style>
</Window.Resources>
<Button Content="Alles im grünen Bereich" Style="{StaticResource GreenButton}" />
Styles unterstützen wie fast alle XAML-Sprachelemente DataBinding. So kann man die Werte der Setter auch an ViewModel-Eigenschaften binden. Selbst die eigentliche Darstellung der Steuerelemente lässt sich über ViewModels steuern, indem per DataBinding bestimmte Templates (siehe nächster Abschnitt) zugewiesen werden.
Um bestimmte Eigenschaften im Style abhängig vom aktuellen Zustand der Anwendung zu setzen, verwendet man Trigger. In diesem Beispiel wird eine Schaltfläche per Trigger ausgegraut, solange sie deaktiviert ist:
<Style TargetType="{x:Type Button}">
<Setter Property="Foreground" Value="White" />
<Setter Property="Background" Value="Blue" />
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="DarkGray" />
<Setter Property="Background" Value="LightGray" />
</Trigger>
</Style.Triggers>
</Style>
Daneben gibt es auch Trigger, die mehrere Bedingungen (MultiTrigger) sowie DataBinding unterstützen (DataTrigger und MultiDataTrigger).
Um die Änderungen der Styles mit einer Animation oder Überblendung zu versehen, kann man Storyboards verwenden. Damit lassen sich beispielsweise Elemente weich ein- und ausblenden, Menüs animiert ein- und ausklappen oder Farben zwischen zwei Werten überblenden.
Weeks of programming can save you hours of planning
5. Templates
Wenn man das Aussehen eines Steuerelements selbst bestimmen will, und die Möglichkeiten der Anpassung per Styles nicht mehr ausreichen, dann kann man ein eigenes Template zuweisen. Folgendes Beispiel weist einer Schaltfläche den Style aus dem vorherigen Beispiel zu und verwendet gleichzeitig ein ControlTemplate für eine runde Schaltfläche:
<Button Content="OK" Style="{StaticResource GreenButton}" Height="30" Width="30">
<Button.Template>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Ellipse Height="{TemplateBinding Height}"
Width="{TemplateBinding Width}"
Fill="{TemplateBinding Background}"
Stroke="{TemplateBinding Foreground}"
StrokeThickness="1" />
<Label Content="{TemplateBinding Content}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
Neben den ControlTemplates gibt es DataTemplates mit denen sich Steuerelemente für eine bestimmte ViewModel-Klasse definieren lassen. Je nachdem, welchen Typ das per DataBinding zugewiesene Objekt hat, wird das Steuerelement dann automatisch an das jeweilige Template angepasst. Im folgenden Beispiel wird eine Liste von Nachrichten ausgegeben, wobei je nach Typ der Nachricht ein anderes Template mit anderen Symbolen und Textfarben verwendet wird:
<ItemsControl ItemsSource="{Binding Messages}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type NotificationMessage}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource NotificationIcon}" />
<Label Content="Notification: " Foreground="Green" />
<Label Content="{Binding MessageText}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type WarningMessage}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource WarningIcon}" />
<Label Content="Warning: " Foreground="Orange" />
<Label Content="{Binding MessageText}" />
</StackPanel>
</DataTemplate>
<DataTemplate DataType="{x:Type ErrorMessage}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource ErrorIcon}" />
<Label Content="Error: " Foreground="Red" />
<Label Content="{Binding MessageText}" />
</StackPanel>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Für hierarchische Daten, d.h. Baumstrukturen wie Dateisysteme, gibt es das HierarchicalDataTemplate. Dies kommt beispielsweise dann zum Einsatz, wenn man ein hierarchisches ViewModel an einen TreeView oder ein Menü mit mehreren Ebenen binden möchte.
Weeks of programming can save you hours of planning
6. Debugging
Das ViewModell lässt sich wie jeder andere Code debuggen. Da das DataBinding selbst erst zur Laufzeit stattfindet, gibt es allerdings keine Compiler-Fehlermeldungen, wenn man beispielsweise an einer Stelle im XAML-Code Content="{Binding Contnet}"
getippt hat anstatt Content="{Binding Content}"
. In solchen Fällen wird während der Programmausführung ein Hinweis im Ausgabe-Fenster von Visual Studio angezeigt. Falls also ein per DataBinding gesetzter Wert in der Oberfläche nicht den Erwartungen entspricht, sollte man zuerst einmal im Ausgabefenster nachschauen, ob es irgendwelche DataBinding-Fehler gab.
Weiterhin gibt es auch den Live Visual Tree im VisualStudio, mit dem man jedes Element in der Benutzeroberfläche mit seinen Daten und gebundenen Werten anschauen und editieren kann.
Falls das nicht ausreicht, gibt es mit Hilfe eines Converters die Möglichkeit, mit dem Debugger den eigentlichen DataBinding-Prozess zu unterbrechen. Damit kann man sich die vom ViewModel übergebenen Werte anschauen, und Schritt für Schritt herausfinden, ob und wann ein falscher Wert übergeben wurde. Der Converter ruft zu diesem Zweck lediglich Debugger.Break()
auf, was den gleichen Effekt wie ein Haltepunkt hat:
public class DataBindingDebugConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Debugger.Break();
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
Debugger.Break();
return value;
}
}
Verwendet wird er, indem er als Resource instanziiert wird und dem fraglichen DataBinding hinzugefügt wird:
<App.Resources>
<local:DataBindingDebugConverter x:Key="DataBindingDebugConverter" />
<App.Resources>
<TextBox Text="{Binding Caption, Converter={StaticResource DataBindingDebugConverter}}" />
Weeks of programming can save you hours of planning
7. Fazit
Databinding und das MVVM-Pattern sind nicht nur die einfachste und schnellste Variante, WPF-Anwendungen zu entwickeln, sondern gleichzeitig die beste Möglichkeit einen gut wartbaren und testbaren Code zu schreiben. In der Software-Entwicklung hat sich diese Technologie nicht nur unter WPF durchgesetzt. Auch für Windows Forms, JavaScript und andere Sprachen gibt es Implementierungen, die auf MVVM oder verwandte Entwurfsmuster wie MVC setzen. Es ist daher empfehlenswert, sich als WPF-Neueinsteiger möglichst früh mit den Möglichkeiten und Vorteilen von DataBinding zu beschäftigen.
Weeks of programming can save you hours of planning
Beispiel-Projekt
Im Anhang befindet sich ein Beispielprojekt, welches die Verwendung des MVVM-Entwurfsmusters demonstriert. Es zeigt, wie man ein ViewModel implementiert, die Steuerelemente eines Fensters an das ViewModel bindet, und veranschaulicht die Verwendung von Styles, Trigger, Templates und Converter.
Weeks of programming can save you hours of planning
Weeks of programming can save you hours of planning
Kritik, Berichtigungen, Ergänzungen, Fragen und anderes Feedback zum Artikel könnt ihr hier im Thread oder per PM an den Autor hinterlassen.
Hallo MrSparkle,
Erstmal danke für den guten Artikel.
Habe mir auch gleich das Projekt gezogen und angeschaut.
Dabei viel mir auf, dass Du alle Models mit ViewModels ummantelt hast, was einen Spezialfall von z.B. vorgegebenen Models darstellt. (Wrapper)
Models dürfen INotifyPropertyChanged sehr wohl implementieren.
Müsste das ViewModel demzufolge nicht irgendwie UserEntriesViewModel heissen und die Models Team und Employee zur Verfügung stellen...bzw. die Liste der Einträge?
Gruss Lhyn
Hi lhyn,
Dabei viel mir auf, dass Du alle Models mit ViewModels ummantelt hast, was einen Spezialfall von z.B. vorgegebenen Models darstellt. (Wrapper)
Das ViewModel ist nicht nur ein Wrapper um die Models, der die INotifyPropertyChanged-Schnittstelle implementiert, sondern hat noch zusätzliche Aufgaben, die unter Punkt 2 aufgezählt sind.
Models dürfen INotifyPropertyChanged sehr wohl implementieren.
Ja, aber warum? Wenn du im Model einen Benachrichtigungs-Mechanismus benötigst, dann wäre doch ein eigenes Event viel einfacher zu implementieren, als eine Schnittstelle, die lediglich auf den Namen der geänderten Eigenschaften basiert. Notwendig ist die INotifyPropertyChanged-Implementierung nur für Klassen, die an Steuerelemente gebunden werden sollen.
Müsste das ViewModel demzufolge nicht irgendwie UserEntriesViewModel heissen und die Models Team und Employee zur Verfügung stellen...bzw. die Liste der Einträge?
Welches ViewModel sollte so heißen? Letztendlich ist es egal, wie du deine ViewModels nennst. Für das Beispiel mit Mitarbeitern und Teams einer Firma fand ich das so ganz passend. Und ein Benutzer (User) einer Software ist auch etwas anderes als ein Mitarbeiter (Employee) einer Firma, jedenfalls aus Sicht der Anwendung betrachtet.
Weeks of programming can save you hours of planning
Ja, aber warum? Wenn du im Model einen Benachrichtigungs-Mechanismus benötigst, dann wäre doch ein eigenes Event viel einfacher zu implementieren, als eine Schnittstelle, die lediglich auf den Namen der geänderten Eigenschaften basiert. Notwendig ist die INotifyPropertyChanged-Implementierung nur für Klassen, die an Steuerelemente gebunden werden sollen.
An der Stelle wäre es dann natürlich einfacher, das Databinding eben auf dieses (bereits vorhandene) Event umzustellen, anstatt das Model erneut zu wrappen, nur um diese eine Benachrichtigungsmechanik (INPC) abzubilden. Wäre aber an dieser Stelle absolut übertrieben gewesen, insofern ist dein Beispiel nur eine Möglichkeit der Implementierung. (Ich denke auch mal, mehr sollte es auch nie sein). @Ihyn, du wirst an jedem Beispiel Dinge finden, die man anders machen könnte. Es gibt da kein richtig oder falsch, solange die Rahmenbedingungen des MVVM-Musters erfüllt sind.
LaTino
"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)
Nur weil es einfacher ist muss es nicht auch gut sein.
Bei der Vermischung von Darstellungs-Schicht (View<->ViewModel) und der Logik/Infrastruktur (Model<->Service) habe ich doch genau dann die Torte im Auge, wenn es Änderungen an der View oder am Service gibt, denn durch diese Vermischung muss ich beide Seiten gleichzeitig anfassen.
Bei einer Trennung können solche Änderungen wesentlich schmerzfreier durchgeführt werden.
Und wenn es nur um einfach geht, warum dann überhaupt mit MVVM hantieren. Einfach ist einen Doppelklick auf so einen Button in der View und im Click-EventHandler alles reinklatschen und fertig (so wie damals).
Also wenn man sich für so ein Konzept der Trennung schon entschieden hat, dann auch konsequent durchziehen.
Du verstehst mich miss 😉. Man kann in WPF ein Databinding implementieren, dass NICHT an INPC hängt. INPC ist nur der Standardweg. Das hier ist auch ein gültiges VM, das du ohne weitere Änderungen an einem View nutzen kannst:
class Example
{
private string _name;
public string Name { get { return _name; } private set { _name = value; NameChanged?.Invoke(); }
public event EventHandler NameChanged;
}
Zusätzlich dazu existieren mit UpdateTarget und UpdateSource zwei weitere Möglichkeiten, Änderungsbenachrichtigungen ohne INPC zu verschicken. In manchen Szenarien ist das weniger Aufwand, als erneut in ein IPNC-Objekt zu wrappen. Und es ist genauso MVVM. Wenn das Model bereits eine Mechanik zur Änderungsbenachrichtigung hat, kann man die benutzen. Dadurch wird an keiner Stelle die Trennung zwischen den Schichten durchbrochen.
LaTino
"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)
Dann verstehst du mich miss 😁
Wenn eine Klasse die Aufgabe hat Daten für eine View bereitzustellen, dann ist es ein ViewModel.
Wenn eine Klasse die Aufgabe hat Daten von A nach B zu transportieren, dann ist es ein DTO.
Wenn ...
Wenn eine Klasse mehr als eine Aufgabe hat, dann hat es sich gerade mit der Trennung erledigt. Dann bekommt man solche ViewModel-Dto-Service-Repository-Persistenz-Klassen oder EiWoSa-Klassen.
Verboten ist das nicht (der Compiler schmeisst weder einen Fehler noch eine Warnung), aber ist es gut? IMHO nein.
Glaub ich hab dich schon richtig verstanden. Mein Punkt war: wenn die Model-Klasse - aus welchen Gründen auch immer - bereits eine Änderungsbenachrichtigung hat, kann man die benutzen. (Wenn es diese Fähigkeit hat, um an eine View gebunden werden zu können, DANN ist die Trennung im Eimer und es muss refaktoriert werden. Aber es kann aus anderen Gründen vorkommen.)
Wenn meine Modelklasse bereits eine Berechnung von 1+1 hat, muss ich sie nicht wrappen, nur um eine andere Implementierung von 1+1 bereitzustellen. Aber gut, solche Fälle sind vermutlich selten. Dass das M in MVVM für ein DTO steht, würde ich btw abstreiten. Aber die Diskussion gab's schon mal.
LaTino
"Furlow, is it always about money?"
"Is there anything else? I mean, how much sex can you have?"
"Don't know. I haven't maxed out yet."
(Furlow & Crichton, Farscape)
Wann habe ich behauptet das M in MVVM steht für DTO? 🤔
Nur weil ich beispielhaft ein DTO angeführt habe, das man an einen Service reicht bzw. von dem zurückbekommt um das Thema Trennung anschaulicher zu gestalten?
Ich bin ein verfechter des one View <-> one ViewModel Prinzips.
Das hat sich mittlerweile sehr bewährt, da die Trennung imho klarer erfolgt. Es ist so auch relativ einfach die, ViewModels in anderen Projekten wieder zu verwenden, wenn man die Schichten in View-ViewModel-Model-DTO-IrgendEinServiceDerDatenLiefert aufbricht.
Ich versuche mich gerade in das MVVM Prinzip einzuarbeiten.
Was ich nicht ganz verstehe (ich habe mir die Dateien runtergeladen), ist, dass ich in deinem Beispiel erst die Mitarbeiter in ein Model lade und dann in das Viewmodel. Wenn ich die Daten zum Beispiel aus einer Datenbank lade, könnt eich sie doch direkt in das Viewmodel laden oder?
Hallo resper,
ViewModels kümmern sich nur um die View. Also um die Daten, die in der View angezeigt werden sollen. Die Models in dem Beispiel sind im Prinzip Datenbankmodels. Sie haben also alle Eigenschaften so, wie es in der DB stehen würde.
Diese Properties müssen nicht übereinstimmen. Und selbst wenn sie das tun: Mappe es um und trenne das. Du brauchst über versch. Schichten auch versch. Models. Eventuell willst du mal ein Event hinzufügen oder auf dem ViewModel an "Fullname" statt "Firstname" und "Lastname" binden.
Gruss
Coffeebean
Microsoft MVP // Me // Blog // GitHub // @Egghead // All my talks // Speakerdeck
Hi resper,
das ViewModel hat andere Aufgaben als das Model, siehe dazu auch den Abschnitt zum ViewModel:
Das ViewModel hat dabei folgende Aufgaben:
Alle erforderlichen Daten für die Anzeige bereitstellen: Das ViewModel muss der Benutzeroberfläche alle Daten zur Verfügung stellen, die sie zur Anzeige benötigt. Dazu gehören neben den Daten, die dem Benutzer angezeigt werden sollen, auch die Aktionen, die der Benutzer ausführen kann (siehe Abschnitt Commands), sowie zusätzliche Daten für Auswahllisten. Will man beispielsweise eine ComboBox für eine Länderauswahl darstellen, muss dafür über das ViewModel eine Liste mit allen auswählbaren Ländern bereitgestellt werden.
Die Benutzeroberfläche über geänderte Werte informieren: Das ViewModel löst die jeweiligen Events aus den INotifyPropertyChanged- und INotifyCollectionChanged-Schnittstellen aus, damit die Anzeige automatisch aktualisiert werden kann.
Kapselung der Logik für die Benutzeroberfläche: Das ViewModel enthält sämtlichen Code, der für die korrekte Darstellung der Benutzeroberfläche notwendig ist, beispielsweise die Aktivierung und Deaktivierung von Steuerelementen oder ganzen Bereichen der Oberfläche sowie Benutzeraktionen (Commands). Damit befindet sich die gesamte Logik der Anwendungsoberfläche im ViewModel, und kann dort unabhängig von anderen Teilen der Anwendung mit Hilfe von
> getestet werde.
Das Model ist dagegen für die Anwendungslogik (Berechnungen etc.) zuständig, siehe dazu auch [Artikel] Drei-Schichten-Architektur
Weeks of programming can save you hours of planning
Hi,
wollte nur einmal "DANKE" sagen.
Ich bin gerade wieder dabei eine kleine WPF App umzusetzen. Und dieser Thread hat mir enorm geholfen mich nochmal an alles zu erinnern.
Perfekt! 👍 👍 👍 👍 👍 👍
Wenn mans geschickt anstellt, kann man die meisten Bindings einfach im Xaml-Designer im PropertyGrid auswählen.
Und dabei sieht man auch eine Datenvorschau, und dann weiß man zu 90% bereits zur Designzeit, obs hinhaut oder nicht.
Hierzu habe ich inne App.Xaml eine MainViewModel
-Resource angelegt, an die dann (siehe Bildle unten) d:DataContext
binden kann:
<Application x:Class="MVVMTestProject.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
xmlns:vm="clr-namespace:MVVMTestProject.ViewModel"
>
<Application.Resources>
<vm:MainViewModel x:Key="MainViewModel"/>
</Application.Resources>
</Application>
Wie's ichs hier gemacht hab ist noch sehr unsauber, es gibt verschiedene Ansätze, es sauber hinzukriegen, dass1.das MainViewModel nur einmal instanziert wird
1.dass es zur Designzeit nur Mockdaten bereitstellt, während Laufzeit-Funktionalität abgeklemmt bleibt
(Das ist in diesem Fall sogar nicht zwingend, weil das Repository selbst derzeit ein Mock ist, schnell genug, um auch im Xaml-Designer ladbar zu sein.)
Ich hab auch ausführliche Artikel und sogar Video gebastelt zum Thema:
BindingPicking-Video
mein MVVM-Artikel
Mein Artikel wird wohl auf geteilte Meinungen treffen, weil ich "unvollständiges MVVM" empfehle, wenn dessen vollständige Ausbildung für ein kleines Problem nur ein grotesker Wasserkopf wäre.
Aber die dortigen Sample-Zips zeigen immerhin, wie man eine Anwendung aufbauen kann, ohne die BindingPicking-Funktionalität des Xaml-Editors zu torpedieren.
Der frühe Apfel fängt den Wurm.
Hallo Mr.Sparkle,
ich wollte nur mal Dankeschön sagen für diesen sehr gut geschriebenen Artikel. Daß ich ihn nicht beim ersten mal komplett verstanden habe, lag sicher nur an mir selber.
Und wie ich jetzt ein Binding invers abfragen und manche Controls nach einem Bool'schen Binding gar nicht anzeigen kann, danach mache ich mich jetzt auf die suche.
Oder ich muss den Artikel ein drittes mal lesen.
Und ich möchte anmerken, daß der Entschluß C# zu lernen die beste Entscheidung war, um nach meinem kleinen Schlaganfall mein altes Hirn wieder auf Geschwindigkeit zu trimmen. Danke für alles
2 stupid 4 chess? No way.
2 stupid 4 C#? It seems so X(