Malheureusement, il n'y a pas un seul excellent exemple d'application MVVM qui fasse tout, et il existe de nombreuses approches différentes pour faire les choses. Tout d'abord, vous voudrez peut-être vous familiariser avec l'un des frameworks d'application (Prism est un choix décent), car ils vous fournissent des outils pratiques tels que l'injection de dépendances, la commande, l'agrégation d'événements, etc. pour essayer facilement différents modèles qui vous conviennent. .
La version prisme:
http://www.codeplex.com/CompositeWPF
Il comprend un exemple d'application assez décent (le négociant en actions) ainsi que de nombreux exemples plus petits et comment faire. À tout le moins, c'est une bonne démonstration de plusieurs sous-modèles courants que les gens utilisent pour faire fonctionner MVVM. Ils ont des exemples de CRUD et de dialogues, je crois.
Prism n'est pas nécessairement pour tous les projets, mais c'est une bonne chose à connaître.
CRUD:
Cette partie est assez simple, les liaisons bidirectionnelles WPF facilitent la modification de la plupart des données. Le vrai truc est de fournir un modèle qui facilite la configuration de l'interface utilisateur. À tout le moins, vous voulez vous assurer que votre ViewModel (ou objet métier) est implémenté INotifyPropertyChanged
pour prendre en charge la liaison et que vous pouvez lier les propriétés directement aux contrôles de l'interface utilisateur, mais vous pouvez également implémenter IDataErrorInfo
pour la validation. En règle générale, si vous utilisez une sorte de solution ORM, la configuration de CRUD est un jeu d'enfant.
Cet article présente des opérations simples de crud:
http://dotnetslackers.com/articles/wpf/WPFDataBindingWithLINQ.aspx
Il est construit sur LinqToSql, mais cela n'est pas pertinent pour l'exemple - tout ce qui est important est que vos objets métier implémentent INotifyPropertyChanged
(ce que font les classes générées par LinqToSql). MVVM n'est pas le point de cet exemple, mais je ne pense pas que cela compte dans ce cas.
Cet article illustre la validation des données
http://blogs.msdn.com/wpfsdk/archive/2007/10/02/data-validation-in-3-5.aspx
Encore une fois, la plupart des solutions ORM génèrent des classes qui implémentent déjà IDataErrorInfo
et fournissent généralement un mécanisme pour faciliter l'ajout de règles de validation personnalisées.
La plupart du temps, vous pouvez prendre un objet (modèle) créé par un ORM et l'envelopper dans un ViewModel qui le contient et des commandes pour enregistrer / supprimer - et vous êtes prêt à lier l'interface utilisateur directement aux propriétés du modèle.
La vue ressemblerait à quelque chose comme ceci (ViewModel a une propriété Item
qui contient le modèle, comme une classe créée dans l'ORM):
<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>
Dialogues: les
dialogues et MVVM sont un peu compliqués. Je préfère utiliser une saveur de l'approche Mediator avec des dialogues, vous pouvez en savoir un peu plus à ce sujet dans cette question StackOverflow:
Exemple de dialogue WPF MVVM
Mon approche habituelle, qui n'est pas tout à fait classique MVVM, peut se résumer comme suit:
Une classe de base pour un ViewModel de dialogue qui expose des commandes pour les actions de validation et d'annulation, un événement pour informer la vue qu'une boîte de dialogue est prête à être fermée, et tout ce dont vous aurez besoin dans toutes vos boîtes de dialogue.
Une vue générique pour votre boîte de dialogue - cela peut être une fenêtre ou un contrôle de type de superposition "modal" personnalisé. En son cœur, c'est un présentateur de contenu dans lequel nous vidons le viewmodel, et il gère le câblage pour fermer la fenêtre - par exemple, lors du changement de contexte de données, vous pouvez vérifier si le nouveau ViewModel est hérité de votre classe de base, et si c'est le cas, abonnez-vous à l'événement de fermeture pertinent (le gestionnaire attribuera le résultat de la boîte de dialogue). Si vous fournissez une autre fonctionnalité de fermeture universelle (le bouton X, par exemple), vous devez également vous assurer d'exécuter la commande de fermeture appropriée sur le ViewModel.
Quelque part, vous devez fournir des modèles de données pour vos ViewModels, ils peuvent être très simples, d'autant plus que vous avez probablement une vue pour chaque boîte de dialogue encapsulée dans un contrôle séparé. Le modèle de données par défaut pour un ViewModel ressemblerait alors à ceci:
<DataTemplate DataType="{x:Type vmodels:AddressEditViewModel}">
<views:AddressEditView DataContext="{Binding}" />
</DataTemplate>
La vue de la boîte de dialogue doit y avoir accès, car sinon elle ne saura pas comment afficher le ViewModel, à part l'interface utilisateur de la boîte de dialogue partagée, son contenu est essentiellement le suivant:
<ContentControl Content="{Binding}" />
Le modèle de données implicite mappera la vue sur le modèle, mais qui le lance?
C'est la partie pas si mvvm. Une façon de le faire est d'utiliser un événement global. Ce que je pense qu'il vaut mieux faire est d'utiliser une configuration de type d'agrégateur d'événements, fournie par injection de dépendances - de cette façon, l'événement est global à un conteneur, pas à l'ensemble de l'application. Prism utilise le cadre d'unité pour la sémantique des conteneurs et l'injection de dépendances, et dans l'ensemble, j'aime beaucoup Unity.
Habituellement, il est logique que la fenêtre racine s'abonne à cet événement - elle peut ouvrir la boîte de dialogue et définir son contexte de données sur le ViewModel qui est transmis avec un événement déclenché.
Configurer cela de cette manière permet à ViewModels de demander à l'application d'ouvrir une boîte de dialogue et de répondre aux actions de l'utilisateur sans rien savoir sur l'interface utilisateur, de sorte que la MVVM-ness reste pour la plupart complète.
Il y a des moments, cependant, où l'interface utilisateur doit augmenter les dialogues, ce qui peut rendre les choses un peu plus délicates. Considérez par exemple si la position de la boîte de dialogue dépend de l'emplacement du bouton qui l'ouvre. Dans ce cas, vous devez disposer d'informations spécifiques à l'interface utilisateur lorsque vous demandez l'ouverture d'une boîte de dialogue. Je crée généralement une classe distincte qui contient un ViewModel et des informations d'interface utilisateur pertinentes. Malheureusement, un certain couplage y paraît inévitable.
Pseudo code d'un gestionnaire de bouton qui déclenche une boîte de dialogue nécessitant des données de position d'élément:
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
}
La vue de boîte de dialogue se liera aux données de position et transmettra le ViewModel contenu à l'intérieur ContentControl
. Le ViewModel lui-même ne sait toujours rien de l'interface utilisateur.
En général, je n'utilise pas la DialogResult
propriété return de la ShowDialog()
méthode ou je m'attends à ce que le thread se bloque jusqu'à ce que la boîte de dialogue soit fermée. Une boîte de dialogue modale non standard ne fonctionne pas toujours comme ça, et dans un environnement composite, vous ne voulez souvent pas vraiment qu'un gestionnaire d'événements se bloque comme ça de toute façon. Je préfère laisser les ViewModels s'occuper de cela - le créateur d'un ViewModel peut s'abonner à ses événements pertinents, définir des méthodes de validation / d'annulation, etc., il n'est donc pas nécessaire de s'appuyer sur ce mécanisme d'interface utilisateur.
Donc, au lieu de ce flux:
// in code behind
var result = somedialog.ShowDialog();
if (result == ...
J'utilise:
// 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
Je le préfère ainsi car la plupart de mes dialogues sont des contrôles pseudo-modaux non bloquants et le faire de cette façon semble plus simple que de contourner cela. Test unitaire facile également.