Links
- Teil 2 zu INotifyPropertyChanged
- Teil 3 zu IDataErrorInfo
- Teil 4 zu Converter und Messenger
- Teil 5 zu Binding von Listen
Vorbemerkungen
Für diese Serie werde ich MvvmLight von Laurant Bugnion verwenden. Es gäbe z.B. mit Prism von Microsoft durchaus Alternativen, aber MvvmLight ist - wie der Name schon sagt - sehr schnell zu lernen und übersichtlich. Die Grundideen bleiben bei allen Frameworks die gleichen.
Ich erkläre hier MVVM als Pattern nicht, sondern gehe davon aus, dass der Leser bereits weiß, was es damit auf sich hat und warum es einsetzen sollte.
Ich lege in der gesamten Lösung sehr viel Wert auf saubere Implementierungen. Das wird den ein oder anderen vielleicht stören, hält mich aber - wie immer - nicht davon ab.
Dieser erste Teil richtet sich an absolute Beginner im Thema. Wir kommen über ein simples Daten-Binding noch nicht hinaus. Wer über den nächsten Teil mehr wissen will, kann ganz nach unten scrollen.
Der Quellcode kann über GitHub eingesehen werden.
Screencast
Solution aufbauen
Der erste Schritt ist wie immer der Aufbau einer Solution im Visual Studio. Wer sich im Detail für das Thema interessiert, sei auf meinen Beitrag ”Eine Solution bauen” verwiesen. Man sollte beachten, dass das NuGet-Package-Restore seit VS 2015 nicht mehr explizit eingeschaltet werden muss und soll.
Wir starten mit etwas, das im Visual Studio Solution Explorer so aussehen sollte:
Ich erstelle nun erstmal einen Solution-Ordner für das UI und packe dort hinein eine Standard-WPF-Application als neues Projekt. Ich benenne meine Projekte in den Solutions nach einem eigenen Schema, das immer zuerst den Ordner-Namen und dann einen Namen für das Projekt beinhaltet. In meinem Beispiel “Ui.Desktop”. Das mache ich, weil es durchaus vorkommen kann, dass ich später weitere UIs (Console, Web, etc.) erstellen möchte:
In der Abbildung kann man auch erkennen, dass ich ein wenig mehr unternommen habe, als nur ein Projekt anzulegen:
- Ich habe eine SharedAssemblyInfo.cs angelegt und diese als Verweis-Element in die Properties von Ui.Desktop eingebunden.
- Ich habe meinen Standard-Namespace auf “codingfreaks.blogsamples.MvvmSample.Dektop.Ui” geändert und entsprechend im Quellcode geändert.
Das alles sind Steps, die nicht notwendig sind, sondern zeigen sollen, wie man von Anfang an sauber arbeiten kann. Jeder sollte sich hier eine Konvention überlegen und diese einhalten.
MVVM Light einbinden
MVVM Light kann seit geraumer Zeit bequem per NuGet eingebunden werden. Es gibt 2 Pakete, die für uns von Interesse sind. Bei Eingabe von “mvvmlight” als Suchbegriff tauchen diese leider nicht beieinander auf, weshalb ich hier beide in einer kleinen Montage aufführe:
- MvvmLight ist das Komplett-Paket. Wir benötigen dieses Paket als erstes.
- MvvmLightLibs beinhaltet nur die Logik-Komponenten. Wir werden das im weiteren Verlauf des Artikels noch benötigen.
Fangen wir also mit dem Hinzufügen des Nuget-Paketes “MvvmLight” zu unserem Projekt “Ui.Desktop” an. Nachdem das NuGet-Setup durch ist, sollte unser Projekt ungefähr so aussehen:
Ich habe in gelb markiert, was neu ist bzw. geändert wurde. Fangen wir oben an:
- Es sind 3 Referenzen hinzugekommen.
- Ein neuer Ordner “ViewModel” enhält ein MainViewModel und einen ViewModelLocator.
- In der App.xaml sind Änderungen vorgenommen worden.
- Die packages.config wurde erstellt und MvvmLight dort eingetragen.
Die Automatik, die MvvmLight hier mitbringt ist Segen und Fluch zugleich. Zunächst zum Segen: Schert man sich eher nicht um saubere Implementierung, kann man mit dieser Vorlage schon loslegen und erzielt erste Ergebnisse.
Das Problem ist nun, dass die meisten Entwickler genau das tun und eigentlich die gesamte Idee von MVVM als Mittel zur Trennung von UI und Logik ad absurdum führen. Man muss auch sagen, dass die meisten Online-Quellen es nicht besser machen und daher ist den Entwicklern an sich schon fast kein Vorwurf zu machen. Egal: wir wollen es besser haben.
App.xaml und der ViewModel-Ordner
Befor wir uns in die Optimierung an sich stüzen, werfen wir einen Blick auf die App.xaml nachdem ich ein wenig Formatierungsarbeit geleistet habe (ein Enter bei jedem Attribut von Application und und ViewModelLocator):
<Application x:Class="codingfreaks.blogsamples.MvvmSample.Ui.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d1p1:Ignorable="d"
xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="Locator"
d:IsDataSource="True"
xmlns:vm="clr-namespace:codingfreaks.blogsamples.MvvmSample.Ui.Desktop.ViewModel" />
</ResourceDictionary>
</Application.Resources>
</Application>
In Listing 1 sind vor allem die Zeilen 5-7 und 10-12 von Bedeutung. Zunächst muss man verstehen, dass 2 XML-Namespaces deklariert werden (d
und d1p1
). d
bringt uns Zugriff auf das eigentlich für Expression Blend entwickelte Attribut IsDataSouirce
, das in Zeile 11 verwendet wird, um anzeigen, dass der ViewModelLocator
als Datenquelle interpretiert werden soll. Leider versteht nun Standard-XAML hier keinen Spaß und würde uns XAML-Fehler werfen, wenn wir nicht aus dem d1p1
-Namespace mit Ignorable
sicherstellten würden, dass keine Prüfung auf allen aus d
stammenden Elementen erfolgt.
Das alles ist sehr kompliziert und man muss es halt wissen und anwenden. Weniger schwierig ist, zu verstehen, was die Zeilen 10-12 eigentlich tun. Sie fügen dem ResourceDictionary der kompletten Applikation eine Ressource vom Typ ViewModelLocator
hinzu und nennen das Ding dann “Locator”. Damit WPF den Typ findet, bringt Zeile 12 den C#-Namespace in dem die entsprechende Klasse steht in die App.xaml ein.
Der letzte Punkt bringt uns nun zum neuen ViewModel-Ordner. Fangen wir mit der Datei MainViewModel an. Bereinigt um die durch MVVM zur Hilfestellung eingebrachten Kommentare bleibt folgendes darin übrig:
namespace codingfreaks.blogsamples.MvvmSample.Ui.Desktop.ViewModel
{
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
}
}
}
Unser erstes ViewModel ist also nichts weiter als ein Typ, der von ViewModelBase
erbt, einem Typ, der wiederum von MvvmLight zur Verfügung gestellt wird. Später kommen wir darauf zurück.
Die Datei ViewModelLocator ist nach der Bereinigung auch nicht viel komplizierter:
namespace codingfreaks.blogsamples.MvvmSample.Ui.Desktop.ViewModel
{
/// <summary>
/// This class contains static references to all the view models in the
/// application and provides an entry point for the bindings.
/// </summary>
public class ViewModelLocator
{
/// <summary>
/// Initializes a new instance of the ViewModelLocator class.
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
if (ViewModelBase.IsInDesignModeStatic)
{
// Create design time view services and models
}
else
{
// Create run time view services and models
}
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
public static void Cleanup()
{
}
}
}
Ich habe hier nicht alles aufgeräumt, das derzeit nicht benötigt wird. Hier handelt es sich um eine ganz normale C#-Klasse, die zunächst einmal eine Eigenschaft anbietet (Main
im Listing in der C#-6-Synthax als Expression-Body). Hierzu nutzt sie einen Typ ServiceLocator
auf den wir noch zu sprechen kommen werden. Die Initialiserung dieses ServiceLocators erfolgt im Constructor. Dieser definiert in seiner letzten Zeile noch, dass der SimpleIoC
-Typ Bekanntschaft mit unserem MainViewModel
machen soll (auch dazu später mehr). Die Cleanup
-Methode ist definiert, aber leer.
*** EDIT ***
Es gibt inzwischen einen Breaking Change bei dem NuGet-Paket für Microsoft.Practices.ServiceLocation
ist inzwischen in einer komplett neuen Version hinterlegt. Das ergibt Breaking Changes hinsichtlich der Samples!
Zusammenfassend könnte man sagen, dass diese beiden C#-Klassen nicht wirklich komplex sind, wenn auch für Anfänger einige Verständnisfragen offen bleiben. Wie gesagt, kommen wir dazu noch.
Zwischenstand
Wer mag, kann das Projekt ausführen und wird feststellen, dass noch nichts Spannendes zu sehen ist. Wer ein wenig mehr unter die Haube sehen möchte, kann gern im ViewModelLocator
einen Haltepunkt setzen und fasziniert feststellen, dass selbst hier kein Code ausgeführt wird, wenn man F5 drückt.
Mit anderen Worten: MVVM ist noch nicht eingebunden und läuft daher auch nicht an. Das ändern wir nun.
MainWindow mit MainViewModel verbinden
Der Sinn eines ViewModels ist es, die komplette Logik eines Views (in unserem Fall Windows) zu kapselb. Das ViewModel soll möglichst nichts vom View wissen müssen. Der View hingegen kennt sein ViewModel sehr genau. Mehr noch: Der View sorgt dafür, dass irgendwer (der ViewModelLocator) gefälligst ein ViewModel erstellt und ihm gibt, sobald er es braucht. Der View braucht in MVVM immer ein ViewModel, weil er ohne gar nicht exisitieren kann und daher mussten die Macher von WPF sich überlegen, wie sie so etwas umsetzen. Die Antwort kommt in Form des sog. Binding
daher.
Bindings basieren auf etwas, das WPF im Kern bereits mitbringt, nämlich DependencyProperty
. Man könnte sagen, eine DependencyProperty ist einfach eine Eigenschaft, die als berechneter Ausdruck zur Laufzeit ständig neu ausgewertet wird. Gibt man einer DependencyProperty einen variablen Wert, spricht man von einem Binding, weil der wirkliche Wert dieser Eigenschaft an etwas außerhalb des entsprechenden Objektes “gebunden” ist.
Wir haben nun bereits alle Zutaten durch MvvmLight erhalten, um ein Binding durchführen zu können. Es gibt bei Bindings immer 2 Wege: visuell und XAML. XAML heißt, man tippt das Binding einfach an die entsprechende Stelle. Visuall heißt, man benutzt Visual-Studio-Dialoge. Wir wollen erstmal letzteres versuchen, weil es hier einige Fallstricke zu beachten gibt und weil es nicht funktioniert, obwohl es das tun sollte. Wer diesen Schritt überspringen will, da er ja eh nicht funktionieren wird, der kann zum Abschnitt “Binding per XAML vornehmen” springen.
Bevor wir das Binding definieren, empfiehlt es sich, einen Build auf dem UI-Projekt oder ganzen Solution vorzunehmen. Danach öffnet man das MainWindow per Doppelklick und sorgt dafür, dass das “Properties”-Panel sichtbar ist (F4 drücken). Jetzt klickt man das MainWindow im Designer an (die Titel-Leiste) oder markiert das <Window
-Tag im XAML-Quellcode. Das Eigenschaften-Fenster muss nun ungefähr so aussehen:
Jedes Steuerelement in WPF hat einen sog. DataContext
. Dieser gibt für das Control und alle in ihm geschachtelten Controls an, woher es Daten bezieht.
*Anmerkung: Der DataContext sollte nicht mit der ItemSource
-Eigenschaften von Listen-Controls verwechselt werden!
Das ist genau das, was wir für die Verbindung von View und ViewModel brauchen. Wir suchen also die entsprechende Eigenschaft im Eigenschaften-Fenster. Hierzu kann man im Suchfeld direkt den Namen eingeben:
Ich habe in der Abbildung nach “data” gesucht und bekomme im Bereich “Common” den richtigen Treffer angezeigt. Jede DependencyProperty hat im Eigenschaften-Fenster rechts ein Kästchen, das ein Kontextmenu öffnet. Darin wähle ich “Create Data Binding…” aus. Es öffnet sich ein Dialog, der allerdings keinen Zugriff auf den ViewModelLocator hat.
Das ist ein Problem von Visual Studio. Es hat den ViewModelLocator nicht als gültige Datenquelle erkannt, obwohl er das technisch ist. Das ist eine der vielen Stolpersteine, die Anfänger nehmen müssen. Das Binding-Fenster kann nun geschlossen werden.
Binding per XAML vornehmen
Da der Designer uns nicht weiterhilft, können wir das Bindung nur noch direkt im XAML vornehmen. Das Öffnungs-Tag von Window
muss wie folgt ergänzt werden:
#!!{"brush":"xml","title":"Listing 4: MainWindow.xaml anpassen","highlight":"[8]"}
<Window x:Class="codingfreaks.blogsamples.MvvmSample.Ui.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding Main, Source={StaticResource Locator}}">
<Grid>
</Grid>
</Window>
Das neue Attribut DataContext
wird mit der Eigenschaft Main
aus der Quelle Locator
verbunden. Locator ist dabei der Key aus Listing 1 Zeile 10. Man muss also wissen, dass alle in App.xaml definierten Ressourcen der gesamten Anwendung zur Verfügung stehen, auch wenn das Binding-Fenster davon scheinbar keine Ahnung hat.
StaticResource
ist übrigens wiederum ein eigenes Binding innerhalb des DataContext-Bindings, das WPF anweist, als Quelle für die Ressource nicht irgendwas in sich selbst zu verwenden, sondern in den statisch verfügbaren Ressourcen der Anwendung nach einer mit dem Schlüssel “Locator” zu suchen.
Man kann nun testen, ob die Verbindung geklappt hat. Setzt man einen Haltepunkt in der ersten Zeile des Constructors von ViewModelLocator
, wird dieser nun tatsächlich erreicht:
Durch das Binding auf dem DataContext des MainWindow wird nun erstmals ein ViewModelLocator benötigt, was zu seiner Initialisierung führt. Wichtig ist, dass der ViewModelLocator technisch ein Singleton ist, also innerhalb der Applikation immer genau eine Instanz davon gehalten wird. Ursache dafür ist, dass die App.xaml eine - und zwar genau eine - entsprechende Ressource bereit hält.
ViewModel sinnvoll einsetzen
Um das Beispiel abzurunden, bietet es sich an, einmal eine sinnvolle Verbindung zwischen den beiden Elementen (View und ViewModel) herzustellen. Der erste Schritt hierfür ist, dem ViewModel eine Eigenschaft zu geben:
/// <summary>
/// Contains logic of the main window.
/// </summary>
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
if (IsInDesignMode)
{
WindowTitle = "MvvSample (Designmode)";
}
else
{
WindowTitle = "MvvSample";
}
}
public string WindowTitle { get; private set; }
}
In Listing 5 erzeuge ich zunächst eine AutoProperty vom Typ string und nenne sie “WindowTitle”. Diese initialisiere ich im Constructor in Abhängigkeit von der Tatsache, ob das Fenster gerade im Visual-Studio- oder Blend-Designer läuft oder wirklich ausgeführt wird. Die entsprechende IsInDesignMode
-Eigenschaft wird mir von dem MvvmLight-Typ ViewModelBase
vererbt.
Alles, was ich nun tun muss ist, diese Eigenschaft des ViewModels im View zu benutzen. Dazu benötige ich ein Bindung auf der Eigenschaft Title
. Jetzt können wir das Binding-Fenster nutzen:
Abb. 8 zeigt, dass ich zunächst nach der Eigenschaft “title” suche, dann auf das kleine Rechteck rechts neben der Eigenschaft klicke (und dort “Create Data Binding…” anklicke) und dann im entsprechenden Dialog endlich die Bindings angezeigt bekomme. Ich wähle “WindowTitle” aus (die Eigenschaft, die ich gerade im ViewModel angelegt habe) und klicke dann “OK”. Das Ergebnis im XAML ist folgendes:
<Window x:Class="codingfreaks.blogsamples.MvvmSample.Ui.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Height="350" Width="525"
Title="{Binding WindowTitle, Mode=OneWay}"
DataContext="{Binding Main, Source={StaticResource Locator}}">
Natürlich könnte man auch dieses Bindung per Hand schreiben. Man könnte dabei übrigens die Mode
-Angabe weglassen, sauberer ist es aber so, denn wir teilen WPF mit, dass eine Änderung der Eigenschaft im View nicht zurück zum ViewModel übertragen werden soll.
Testen des Bindings
Sieht man sich das Fenster im XAML-Designer an, erkennt man sofort, dass das Binding funktioniert:
Führt man das Programm jetzt aus, wird entsprechend den Einstellungen aus Listing 5 ein anderer Title angezeigt:
Da war doch noch was?
Jetzt, wo die Anwendung so gut funktioniert, könnte man sich entspannt zurück lehnen, was wie gesagt die meisten Menschen dann auch tun. Ich stelle mir jedoch die Frage, wie sinnvoll es wohl ist, dass die Logik im gleichen Projekt, wie das UI steckt? Richtig! Die Antwort ist, dass es gar nicht sinnvoll ist.
Es gibt mehrere gute Gründe für diese Einschätzung. Wir werden uns im Laufe der Artikelserie dem einen oder anderen davon noch nähern. Die einfachste Begründung aber lautet, dass es bisher immer eine sehr gute Idee war, getrennte Bereiche auch getrennt zu verwalten.
Ein erster Schritt ist nun, eine neue Klassenbibliothek innerhalb der Solution zu erstellen. Ich packe diese in einen Ordner Logic und nenne sie “Logic.Ui”. Nachdem ich die obligatorische “Class1.cs” darauf entfernt und meine obligatorischen Grund-Einstellungen vorgenommen habe, sieht mein Projekt ungefähr so aus:
Als nächstes bekommt mein Ui.Desktop-Projekt einen Verweis auf dieses Projekt. Nun füge ich dem Projekt “Logic.Ui” eine NuGet-Referenz auf MvvmLightLibs hinzu:
install-package MvvmLightLibs -ProjectName Logic.Ui
Ich habe hier mal das entsprechende NuGet-Kommando für die Eingabe in der Package Manager Console im Visual Studio eingebunden. Das geht schneller, als die visuelle Suche nach dem richtigen Paket und verhindert das versehentliche Einbinden falsche Pakete.
Das Projekt “Logic.Ui” hat nun außer den 3 Referenzen und der packages.config-Datei keine weiteren Änderungen erfahren.
Jetzt verschiebe ich die beiden Dateien aus dem ViewModel-Ordner des Projekts “Ui.Desktop” in das neue Projekt und lösche den ViewModel-Ordner in “Ui.Desktop”:
Ich passe nun meine Namespaces in MainViewModel.cs
und ViewModelLocator.cs
entsprechend an und versuche nur das Projekt “Logic.Ui” zu bauen (sollte funktionieren).
Zum Schluss muss ich noch Anpassungen an App.xaml
in “Ui.Desktop” vornehmen:
<Application x:Class="codingfreaks.blogsamples.MvvmSample.Ui.Desktop.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
d1p1:Ignorable="d"
xmlns:d1p1="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:UiLogic="clr-namespace:codingfreaks.blogsamples.MvvmSample.Logic.Ui;assembly=MvvmSample.Logic.Ui">
<Application.Resources>
<ResourceDictionary>
<UiLogic:ViewModelLocator x:Key="Locator"
d:IsDataSource="True" />
</ResourceDictionary>
</Application.Resources>
</Application>
Listing 8 zeigt, dass ich nun einen Namespace aus einer anderen Assembly importiere (Logic.Ui
) und dementsprechend auch einen neuen Alias dafür verwende. Das ist alles, was ich hier anpassen muss.
Startet man das Projekt nun, sollte alles wie vorher funktionieren.
Ausblick
Im nächsten Teil der Serie möchte ich mich mit Themen, wie dem Interface INotifyPropertyChanged
, dem RelayCommand
und ObservableCollection
befassen.