Leider gibt es keine großartige MVVM-Beispiel-App, die alles kann, und es gibt viele verschiedene Ansätze, um Dinge zu tun. Zunächst möchten Sie sich vielleicht mit einem der App-Frameworks vertraut machen (Prism ist eine gute Wahl), da es Ihnen praktische Tools wie Abhängigkeitsinjektion, Befehle, Ereignisaggregation usw. bietet, mit denen Sie auf einfache Weise verschiedene Muster ausprobieren können, die zu Ihnen passen .
Die Prismenfreigabe:
http://www.codeplex.com/CompositeWPF
Es enthält eine ziemlich anständige Beispiel-App (den Aktienhändler) sowie viele kleinere Beispiele und Anleitungen. Zumindest ist es eine gute Demonstration einiger gängiger Untermuster, mit denen MVVM tatsächlich funktioniert. Ich glaube, sie haben Beispiele für CRUD und Dialoge.
Prisma ist nicht unbedingt für jedes Projekt geeignet, aber es ist eine gute Sache, sich damit vertraut zu machen.
CRUD:
Dieser Teil ist ziemlich einfach. WPF-Zweiwege-Bindungen machen es wirklich einfach, die meisten Daten zu bearbeiten. Der eigentliche Trick besteht darin, ein Modell bereitzustellen, mit dem die Benutzeroberfläche einfach eingerichtet werden kann. Zumindest möchten Sie sicherstellen, dass Ihr ViewModel (oder Geschäftsobjekt) implementiert wird INotifyPropertyChanged
, um die Bindung zu unterstützen, und Sie können Eigenschaften direkt an UI-Steuerelemente binden, aber Sie möchten sie möglicherweise auch IDataErrorInfo
zur Validierung implementieren . Wenn Sie eine ORM-Lösung verwenden, ist das Einrichten von CRUD in der Regel ein Kinderspiel.
Dieser Artikel beschreibt einfache Rohoperationen:
http://dotnetslackers.com/articles/wpf/WPFDataBindingWithLINQ.aspx
Es basiert auf LinqToSql, aber das ist für das Beispiel irrelevant - alles, was wichtig ist, ist, dass Ihre Geschäftsobjekte implementiert werden INotifyPropertyChanged
(welche Klassen von LinqToSql generiert werden). MVVM ist nicht der Punkt dieses Beispiels, aber ich denke nicht, dass es in diesem Fall wichtig ist.
Dieser Artikel demonstriert die Datenvalidierung
http://blogs.msdn.com/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx
Wiederum generieren die meisten ORM-Lösungen Klassen, die bereits implementiert sind IDataErrorInfo
und normalerweise einen Mechanismus bieten, der das Hinzufügen benutzerdefinierter Validierungsregeln erleichtert.
Meistens können Sie ein von einem ORM erstelltes Objekt (Modell) in ein ViewModel einbinden, das es und Befehle zum Speichern / Löschen enthält - und Sie können die Benutzeroberfläche direkt an die Eigenschaften des Modells binden.
Die Ansicht würde ungefähr so aussehen (ViewModel hat eine Eigenschaft Item
, die das Modell enthält, wie eine im ORM erstellte Klasse):
<StackPanel>
<StackPanel DataContext=Item>
<TextBox Text="{Binding FirstName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
<TextBox Text="{Binding LastName, Mode=TwoWay, ValidatesOnDataErrors=True}" />
</StackPanel>
<Button Command="{Binding SaveCommand}" />
<Button Command="{Binding CancelCommand}" />
</StackPanel>
Dialoge:
Dialoge und MVVM sind etwas knifflig. Ich bevorzuge es, eine Variante des Mediator-Ansatzes mit Dialogen zu verwenden. Weitere Informationen hierzu finden Sie in dieser StackOverflow-Frage:
Beispiel für einen WPF MVVM-Dialog
Mein üblicher Ansatz, der nicht ganz klassisch MVVM ist, kann wie folgt zusammengefasst werden:
Eine Basisklasse für ein Dialogfeld ViewModel, das Befehle zum Festschreiben und Abbrechen von Aktionen bereitstellt, ein Ereignis, mit dem die Ansicht darüber informiert wird, dass ein Dialogfeld zum Schließen bereit ist, und alles, was Sie sonst noch in all Ihren Dialogfeldern benötigen.
Eine allgemeine Ansicht für Ihren Dialog - Dies kann ein Fenster oder ein benutzerdefiniertes "modales" Overlay-Typ-Steuerelement sein. Im Kern handelt es sich um einen Content Presenter, in den wir das Ansichtsmodell kopieren, und der die Verkabelung zum Schließen des Fensters übernimmt. Beispielsweise können Sie bei Änderungen des Datenkontexts überprüfen, ob das neue ViewModel von Ihrer Basisklasse geerbt wurde und ob dies der Fall ist. Abonnieren Sie das entsprechende Abschlussereignis (der Handler weist das Dialogergebnis zu). Wenn Sie eine alternative universelle Schließfunktion bereitstellen (z. B. die Schaltfläche X), sollten Sie sicherstellen, dass Sie den entsprechenden Befehl zum Schließen auch im ViewModel ausführen.
Wenn Sie Datenvorlagen für Ihre ViewModels bereitstellen müssen, können diese sehr einfach sein, insbesondere da Sie wahrscheinlich eine Ansicht für jeden Dialog haben, der in einem separaten Steuerelement gekapselt ist. Die Standarddatenvorlage für ein ViewModel würde dann ungefähr so aussehen:
<DataTemplate DataType="{x:Type vmodels:AddressEditViewModel}">
<views:AddressEditView DataContext="{Binding}" />
</DataTemplate>
Die Dialogansicht muss Zugriff auf diese haben, da sie sonst nicht weiß, wie das ViewModel angezeigt werden soll. Abgesehen von der Benutzeroberfläche des freigegebenen Dialogfelds sind die Inhalte im Wesentlichen folgende:
<ContentControl Content="{Binding}" />
Die implizite Datenvorlage ordnet die Ansicht dem Modell zu, aber wer startet sie?
Dies ist der nicht so mvvm Teil. Eine Möglichkeit besteht darin, ein globales Ereignis zu verwenden. Ich denke, es ist besser, ein Ereignisaggregatortyp-Setup zu verwenden, das durch Abhängigkeitsinjektion bereitgestellt wird. Auf diese Weise ist das Ereignis für einen Container global und nicht für die gesamte App. Prism verwendet das Unity-Framework für die Containersemantik und die Abhängigkeitsinjektion, und insgesamt gefällt mir Unity ziemlich gut.
Normalerweise ist es sinnvoll, dass das Stammfenster dieses Ereignis abonniert. Es kann den Dialog öffnen und seinen Datenkontext auf das ViewModel festlegen, das mit einem ausgelösten Ereignis übergeben wird.
Wenn Sie dies auf diese Weise einrichten, können ViewModels die Anwendung auffordern, ein Dialogfeld zu öffnen und dort auf Benutzeraktionen zu reagieren, ohne etwas über die Benutzeroberfläche zu wissen, sodass die MVVM-Funktion größtenteils vollständig bleibt.
Es gibt jedoch Situationen, in denen die Benutzeroberfläche die Dialoge öffnen muss, was die Dinge etwas schwieriger machen kann. Überlegen Sie beispielsweise, ob die Dialogposition von der Position der Schaltfläche abhängt, mit der sie geöffnet wird. In diesem Fall benötigen Sie einige UI-spezifische Informationen, wenn Sie ein geöffnetes Dialogfeld anfordern möchten. Im Allgemeinen erstelle ich eine separate Klasse, die ein ViewModel und einige relevante UI-Informationen enthält. Leider scheint dort eine gewisse Kopplung unvermeidlich.
Pseudocode eines Schaltflächenhandlers, der einen Dialog auslöst, der Elementpositionsdaten benötigt:
ButtonClickHandler(sender, args){
var vm = DataContext as ISomeDialogProvider; // check for null
var ui_vm = new ViewModelContainer();
// assign margin, width, or anything else that your custom dialog might require
...
ui_vm.ViewModel = vm.SomeDialogViewModel; // or .GetSomeDialogViewModel()
// raise the dialog show event
}
Die Dialogansicht wird an Positionsdaten gebunden und das enthaltene ViewModel an das Innere übergeben ContentControl
. Das ViewModel selbst weiß immer noch nichts über die Benutzeroberfläche.
Im Allgemeinen verwende ich die DialogResult
return-Eigenschaft der ShowDialog()
Methode nicht und erwarte nicht, dass der Thread blockiert, bis der Dialog geschlossen wird. Ein nicht standardmäßiger modaler Dialog funktioniert nicht immer so, und in einer zusammengesetzten Umgebung möchten Sie oft nicht, dass ein Ereignishandler sowieso so blockiert. Ich ziehe es vor, die ViewModels damit befassen zu lassen - der Ersteller eines ViewModels kann seine relevanten Ereignisse abonnieren, Commit / Cancel-Methoden festlegen usw., sodass Sie sich nicht auf diesen UI-Mechanismus verlassen müssen.
Also anstelle dieses Flusses:
// in code behind
var result = somedialog.ShowDialog();
if (result == ...
Ich benutze:
// in view model
var vm = new SomeDialogViewModel(); // child view model
vm.CommitAction = delegate { this.DoSomething(vm); } // what happens on commit
vm.CancelAction = delegate { this.DoNothing(vm); } // what happens on cancel/close (optional)
// raise dialog request event on the container
Ich bevorzuge es auf diese Weise, da die meisten meiner Dialoge nicht blockierende pseudomodale Steuerelemente sind und es einfacher erscheint, dies zu tun, als es zu umgehen. Einfach zu testen.