MVVM - Guide rapide
La manière la plus ordonnée et peut-être la plus réutilisable d'organiser votre code est d'utiliser le modèle «MVVM». leModel, View, ViewModel (MVVM pattern) Il s'agit de vous guider dans la manière d'organiser et de structurer votre code pour écrire des applications maintenables, testables et extensibles.
Model - Il contient simplement les données et n'a rien à voir avec la logique métier.
ViewModel - Il agit comme le lien / la connexion entre le modèle et la vue et rend les choses jolies.
View - Il contient simplement les données formatées et délègue essentiellement tout au modèle.
Présentation séparée
Pour éviter les problèmes causés par la mise en place de la logique d'application dans code-behind ou XAML, il est préférable d'utiliser une technique appelée présentation séparée. Nous essayons d'éviter cela, où nous aurons XAML et code-behind avec le minimum requis pour travailler directement avec des objets d'interface utilisateur. Les classes d'interface utilisateur contiennent également du code pour les comportements d'interaction complexes, la logique d'application et tout le reste, comme illustré dans la figure suivante sur le côté gauche.
Avec une présentation séparée, la classe d'interface utilisateur est beaucoup plus simple. Il a le XAML bien sûr, mais le code derrière fait aussi peu que pratique.
La logique d'application appartient à une classe distincte, souvent appelée modèle.
Cependant, ce n'est pas toute l'histoire. Si vous vous arrêtez ici, vous risquez de répéter une erreur très courante qui vous mènera sur la voie de la folie de liaison de données.
De nombreux développeurs tentent d'utiliser la liaison de données pour connecter des éléments du XAML directement aux propriétés du modèle.
Parfois, cela peut être correct, mais souvent ce n'est pas le cas. Le problème est que le modèle est entièrement concerné par ce que fait l'application, et non par la façon dont l'utilisateur interagit avec l'application.
La façon dont vous présentez les données est souvent quelque peu différente de la façon dont elles sont structurées en interne.
De plus, la plupart des interfaces utilisateur ont un état qui n'appartient pas au modèle d'application.
Par exemple, si votre interface utilisateur utilise un glisser-déposer, quelque chose doit garder une trace de choses comme l'endroit où l'élément à glisser est maintenant, comment son apparence devrait changer lorsqu'il se déplace sur les cibles de dépôt possibles, et comment ces cibles de dépôt pourraient également changer au fur et à mesure que l'élément est glissé dessus.
Ce type d'état peut devenir étonnamment complexe et doit être minutieusement testé.
En pratique, vous voulez normalement une autre classe assise entre l'interface utilisateur et le modèle. Cela a deux rôles importants.
Tout d'abord, il adapte votre modèle d'application à une vue d'interface utilisateur particulière.
Deuxièmement, c'est là que réside toute logique d'interaction non triviale, et par là, je veux dire le code requis pour que votre interface utilisateur se comporte comme vous le souhaitez.
Le modèle MVVM est finalement la structure moderne du modèle MVC, donc l'objectif principal est toujours le même pour fournir une séparation claire entre la logique de domaine et la couche de présentation. Voici quelques-uns des avantages et des inconvénients du modèle MVVM.
Le principal avantage est de permettre une véritable séparation entre la vue et le modèle au-delà de la séparation et de l'efficacité que vous en tirez. Ce que cela signifie en termes réels, c'est que lorsque votre modèle doit changer, il peut être changé facilement sans que la vue ait besoin de le faire et vice-versa.
Il y a trois éléments clés importants qui découlent de l'application de MVVM qui sont les suivants.
Maintenabilité
Une séparation nette des différents types de code devrait faciliter l'accès à une ou plusieurs de ces parties plus granulaires et ciblées et apporter des modifications sans souci.
Cela signifie que vous pouvez rester agile et passer rapidement à de nouvelles versions.
Testabilité
Avec MVVM, chaque morceau de code est plus granulaire et s'il est implémenté correctement, vos dépendances externes et internes sont dans des morceaux de code séparés des parties avec la logique de base que vous souhaitez tester.
Cela rend beaucoup plus facile d'écrire des tests unitaires par rapport à une logique de base.
Assurez-vous qu'il fonctionne correctement lorsqu'il est écrit et qu'il continue de fonctionner même lorsque les choses changent lors de la maintenance.
Extensibilité
Il chevauche parfois la maintenabilité, en raison des limites de séparation propres et des morceaux de code plus granulaires.
Vous avez de meilleures chances de rendre l'une de ces pièces plus réutilisable.
Il a également la capacité de remplacer ou d'ajouter de nouveaux morceaux de code qui font des choses similaires aux bons endroits de l'architecture.
L'objectif évident du modèle MVVM est l'abstraction de la vue, ce qui réduit la quantité de logique métier dans le code-behind. Cependant, voici quelques autres avantages solides -
- Le ViewModel est plus facile à tester unitaire que le code derrière ou le code événementiel.
- Vous pouvez le tester sans automatisation ni interaction maladroites de l'interface utilisateur.
- La couche de présentation et la logique sont faiblement couplées.
Désavantages
- Certaines personnes pensent que pour les interfaces utilisateur simples, MVVM peut être excessif.
- De même, dans les cas plus importants, il peut être difficile de concevoir le ViewModel.
- Le débogage serait un peu difficile lorsque nous avons des liaisons de données complexes.
Le modèle MVVM se compose de trois parties: Model, View et ViewModel. La plupart des développeurs au début sont peu confus quant à ce qu'un modèle, une vue et un ViewModel devraient ou ne devraient pas contenir et quelles sont les responsabilités de chaque partie.
Dans ce chapitre, nous allons apprendre les responsabilités de chaque partie du modèle MVVM afin que vous puissiez clairement comprendre quel type de code va où. MVVM est en fait une architecture en couches pour le côté client, comme illustré dans la figure suivante.
La couche de présentation est composée des vues.
La couche logique sont les modèles de vue.
La couche de présentation est la combinaison des objets du modèle.
Les services client qui les produisent et les conservent soit ont dirigé l'accès dans une application à deux niveaux, soit via des appels de service dans puis vers votre application.
Les services client ne font pas officiellement partie du modèle MVVM, mais ils sont souvent utilisés avec MVVM pour obtenir des séparations supplémentaires et éviter le code en double.
Responsabilités du modèle
En général, le modèle est le plus simple à comprendre. C'est le modèle de données côté client qui prend en charge les vues dans l'application.
Il est composé d'objets avec des propriétés et quelques variables pour contenir des données en mémoire.
Certaines de ces propriétés peuvent faire référence à d'autres objets de modèle et créer le graphe d'objets qui, dans son ensemble, sont les objets de modèle.
Les objets de modèle doivent déclencher des notifications de modification de propriété, ce qui signifie dans WPF la liaison de données.
La dernière responsabilité est la validation qui est facultative, mais vous pouvez intégrer les informations de validation sur les objets de modèle à l'aide des fonctionnalités de validation de liaison de données WPF via des interfaces telles que INotifyDataErrorInfo / IDataErrorInfo
Voir les responsabilités
L'objectif principal et les responsabilités des vues sont de définir la structure de ce que l'utilisateur voit à l'écran. La structure peut contenir des parties statiques et dynamiques.
Les parties statiques sont la hiérarchie XAML qui définit les contrôles et la disposition des contrôles dont une vue est composée.
La partie dynamique est comme des animations ou des changements d'état qui sont définis dans le cadre de la vue.
L'objectif principal de MVVM est qu'il ne doit pas y avoir de code derrière dans la vue.
Il est impossible qu'il n'y ait pas de code derrière en vue. En vue, vous avez au moins besoin du constructeur et d'un appel pour initialiser le composant.
L'idée est que le code logique de gestion des événements, d'action et de manipulation des données ne doit pas être dans le code derrière View.
Il existe également d'autres types de code qui doivent entrer dans le code derrière tout code qui doit avoir une référence à l'élément d'interface utilisateur est intrinsèquement le code d'affichage.
Responsabilités de ViewModel
ViewModel est le point principal de l'application MVVM. La responsabilité principale du ViewModel est de fournir des données à la vue, afin que la vue puisse mettre ces données à l'écran.
Il permet également à l'utilisateur d'interagir avec les données et de modifier les données.
L'autre responsabilité clé d'un ViewModel est d'encapsuler la logique d'interaction pour une vue, mais cela ne signifie pas que toute la logique de l'application doit aller dans ViewModel.
Il doit être capable de gérer le séquencement approprié des appels pour que la bonne chose se produise en fonction de l'utilisateur ou des modifications apportées à la vue.
ViewModel doit également gérer toute logique de navigation, comme décider du moment où il est temps de naviguer vers une vue différente.
Dans ce chapitre, nous allons apprendre à utiliser les modèles MVVM pour un écran de saisie simple et l'application WPF à laquelle vous êtes peut-être déjà habitué.
Jetons un coup d'œil à un exemple simple dans lequel nous utiliserons l'approche MVVM.
Step 1 - Créez un nouveau projet d'application WPF MVVMDemo.
Step 2 - Ajoutez les trois dossiers (Model, ViewModel et Views) dans votre projet.
Step 3 - Ajoutez une classe StudentModel dans le dossier Model et collez le code ci-dessous dans cette classe
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get {
return firstName;
}
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get {return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Step 4 - Ajoutez une autre classe StudentViewModel dans le dossier ViewModel et collez le code suivant.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Step 5 - Ajoutez un nouveau contrôle utilisateur (WPF) en cliquant avec le bouton droit sur le dossier Vues et sélectionnez Ajouter> Nouvel élément…
Step 6- Cliquez sur le bouton Ajouter. Vous verrez maintenant le fichier XAML. Ajoutez le code suivant dans le fichier StudentView.xaml qui contient différents éléments d'interface utilisateur.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Step 7 - Ajoutez maintenant StudentView dans votre fichier MainPage.xaml en utilisant le code suivant.
<Window x:Class = "MVVMDemo.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"
xmlns:local = "clr-namespace:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl" Loaded = "StudentViewControl_Loaded"/>
</Grid>
</Window>
Step 8 - Voici l'implémentation de l'événement Loaded dans le fichier MainPage.xaml.cs, qui mettra à jour la vue à partir du ViewModel.
using System.Windows;
namespace MVVMDemo {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
private void StudentViewControl_Loaded(object sender, RoutedEventArgs e) {
MVVMDemo.ViewModel.StudentViewModel studentViewModelObject =
new MVVMDemo.ViewModel.StudentViewModel();
studentViewModelObject.LoadStudents();
StudentViewControl.DataContext = studentViewModelObject;
}
}
}
Step 9 - Lorsque le code ci-dessus est compilé et exécuté, vous recevrez la sortie suivante sur votre fenêtre principale.
Nous vous recommandons d'exécuter l'exemple ci-dessus étape par étape pour une meilleure compréhension.
Dans ce chapitre, nous aborderons différentes manières dont vous pouvez connecter vos vues à ViewModel. Tout d'abord, examinons la première construction de View où nous pouvons la déclarer en XAML. Comme nous l'avons vu l'exemple dans le dernier chapitre où nous avons accroché une vue de la fenêtre principale. Nous allons maintenant voir d'autres façons de connecter des vues.
Nous utiliserons également le même exemple dans ce chapitre. Voici la même implémentation de classe Model.
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Voici l'implémentation de la classe ViewModel. Cette fois, la méthode LoadStudents est appelée dans le constructeur par défaut.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel{
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Que la vue soit une fenêtre, un contrôle utilisateur ou une page, l'analyseur fonctionne généralement de haut en bas et de gauche à droite. Il appelle le constructeur par défaut pour chaque élément lorsqu'il le rencontre. Il existe deux manières de construire une vue. Vous pouvez en utiliser n'importe lequel.
- Afficher la première construction en XAML
- Voir la première construction dans Code-behind
Afficher la première construction en XAML
Une façon consiste simplement à ajouter votre ViewModel en tant qu'élément imbriqué dans le setter pour la propriété DataContext, comme indiqué dans le code suivant.
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
Voici le fichier View XAML complet.
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Voir la première construction dans Code-behind
Une autre façon est que vous pouvez obtenir la première construction de View en construisant simplement le modèle de vue vous-même dans le code derrière votre View en définissant la propriété DataContext avec l'instance.
En règle générale, la propriété DataContext est définie dans la méthode de vue du constructeur, mais vous pouvez également différer la construction jusqu'à ce que l'événement Load de la vue se déclenche.
using System.Windows.Controls;
namespace MVVMDemo.Views {
/// <summary>
/// Interaction logic for StudentView.xaml
/// </summary>
public partial class StudentView : UserControl {
public StudentView() {
InitializeComponent();
this.DataContext = new MVVMDemo.ViewModel.StudentViewModel();
}
}
}
L'une des raisons de la construction du modèle de vue dans Code-behind au lieu de XAML est que le constructeur de modèle View prend des paramètres, mais l'analyse XAML ne peut construire des éléments que s'ils sont définis dans le constructeur par défaut.
Maintenant, dans ce cas, le fichier XAML de View ressemblera à celui indiqué dans le code suivant.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300"
d:DesignWidth = "300">
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal"<
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Vous pouvez déclarer cette vue dans MainWindow comme indiqué dans le fichier MainWindow.XAML.
<Window x:Class = "MVVMDemo.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"
xmlns:local = "clr-namespace:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la sortie suivante sur votre fenêtre principale.
Nous vous recommandons d'exécuter l'exemple ci-dessus étape par étape pour une meilleure compréhension.
Dans ce chapitre, nous verrons comment connecter ViewModel. C'est une continuation du dernier chapitre dans lequel nous avons discuté de la première construction de View. Maintenant, la forme suivante de la première construction est unmeta-pattern qui est connu comme ViewModelLocator. Il s'agit d'un pseudo motif et se superpose au motif MVVM.
Dans MVVM, chaque vue doit être connectée à son ViewModel.
Le ViewModelLocator est une approche simple pour centraliser le code et découpler davantage la vue.
Cela signifie qu'il n'a pas à connaître explicitement le type ViewModel et comment le construire.
Il existe un certain nombre d'approches différentes pour utiliser ViewModelLocator, mais ici nous utilisons la plus similaire à celle qui fait partie du cadre PRISM.
ViewModelLocator fournit un moyen standard, cohérent, déclaratif et faiblement couplé de faire la première construction de la vue, ce qui automatise le processus de connexion de ViewModel à la vue. La figure suivante représente le processus de haut niveau de ViewModelLocator.
Step 1 - Déterminez quel type de vue est en cours de construction.
Step 2 - Identifiez le ViewModel pour ce type de vue particulier.
Step 3 - Construisez ce ViewModel.
Step 4 - Définissez Views DataContext sur ViewModel.
Pour comprendre le concept de base, jetons un œil à l'exemple simple de ViewModelLocator en continuant le même exemple du dernier chapitre. Si vous regardez le fichier StudentView.xaml, vous verrez que nous avons connecté le ViewModel de manière statique.
Maintenant, comme indiqué dans le programme suivant, commentez ce code XAML supprimez également le code de Code-behind.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Créons maintenant un nouveau dossier VML et ajoutons une nouvelle classe publique ViewModelLocator qui contiendra une seule propriété attachée (propriété de dépendance) AutoHookedUpViewModel comme indiqué dans le code suivant.
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new PropertyMetadata(false,
AutoHookedUpViewModelChanged));
Et maintenant, vous pouvez voir une définition de propriété d'attachement de base. Pour ajouter un comportement à la propriété, nous devons ajouter un gestionnaire d'événements modifié pour cette propriété qui contient le processus automatique de raccordement du ViewModel pour View. Le code pour ce faire est le suivant -
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
Voici l'implémentation complète de la classe ViewModelLocator.
using System;
using System.ComponentModel;
using System.Windows;
namespace MVVMDemo.VML {
public static class ViewModelLocator {
public static bool GetAutoHookedUpViewModel(DependencyObject obj) {
return (bool)obj.GetValue(AutoHookedUpViewModelProperty);
}
public static void SetAutoHookedUpViewModel(DependencyObject obj, bool value) {
obj.SetValue(AutoHookedUpViewModelProperty, value);
}
// Using a DependencyProperty as the backing store for AutoHookedUpViewModel.
//This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoHookedUpViewModelProperty =
DependencyProperty.RegisterAttached("AutoHookedUpViewModel",
typeof(bool), typeof(ViewModelLocator), new
PropertyMetadata(false, AutoHookedUpViewModelChanged));
private static void AutoHookedUpViewModelChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e) {
if (DesignerProperties.GetIsInDesignMode(d)) return;
var viewType = d.GetType();
string str = viewType.FullName;
str = str.Replace(".Views.", ".ViewModel.");
var viewTypeName = str;
var viewModelTypeName = viewTypeName + "Model";
var viewModelType = Type.GetType(viewModelTypeName);
var viewModel = Activator.CreateInstance(viewModelType);
((FrameworkElement)d).DataContext = viewModel;
}
}
}
La première chose à faire est d'ajouter un espace de noms afin que nous puissions accéder à ce type ViewModelLocator à la racine de notre projet. Ensuite, sur l'élément de route qui est un type de vue, ajoutez la propriété AutoHookedUpViewModel et définissez-la sur true.
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
Voici l'implémentation complète du fichier StudentView.xaml.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Lorsque le code ci-dessus est compilé et exécuté, vous verrez que ViewModelLocator connecte le ViewModel pour cette vue particulière.
Une chose clé à noter à ce sujet est que la vue n'est plus couplée en quelque sorte au type de son ViewModel ou à la façon dont il est construit. Tout cela a été déplacé vers l'emplacement central à l'intérieur du ViewModelLocator.
Dans ce chapitre, nous allons apprendre comment la liaison de données prend en charge le modèle MVVM. La liaison de données est la fonctionnalité clé qui différencie MVVM des autres modèles de séparation d'interface utilisateur tels que MVC et MVP.
Pour la liaison de données, vous devez avoir une vue ou un ensemble d'éléments d'interface utilisateur construits, puis vous avez besoin d'un autre objet vers lequel les liaisons vont pointer.
Les éléments d'interface utilisateur dans une vue sont liés aux propriétés qui sont exposées par le ViewModel.
L'ordre dans lequel la vue et le modèle de vue sont construits dépend de la situation, car nous avons d'abord couvert la vue.
Une vue et un ViewModel sont construits et le DataContext de la vue est défini sur le ViewModel.
Les liaisons peuvent être des liaisons de données OneWay ou TwoWay pour faire circuler les données dans les deux sens entre View et ViewModel.
Jetons un coup d'œil aux liaisons de données dans le même exemple. Vous trouverez ci-dessous le code XAML de StudentView.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<!--<UserControl.DataContext>
<viewModel:StudentViewModel/>
</UserControl.DataContext>-->
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Si vous regardez le code XAML ci-dessus, vous verrez que ItemsControl est lié à la collection Students exposée par ViewModel.
Vous pouvez également voir que la propriété du modèle Student a également ses propres liaisons individuelles, et celles-ci sont liées aux Textboxes et TextBlock.
Le ItemSource de ItemsControl est capable de se lier à la propriété Students, car le DataContext global pour la vue est défini sur ViewModel.
Les liaisons individuelles des propriétés ici sont également des liaisons DataContext, mais elles ne sont pas liées au ViewModel lui-même, en raison du fonctionnement d'un ItemSource.
Lorsqu'une source d'élément se lie à sa collection, elle restitue un conteneur pour chaque élément lors du rendu et définit le DataContext de ce conteneur sur l'élément. Ainsi, le DataContext global pour chaque zone de texte et bloc de texte dans une ligne sera un étudiant individuel de la collection. Et vous pouvez également voir que ces liaisons pour TextBoxes sont des liaisons de données TwoWay et pour TextBlock, c'est une liaison de données OneWay car vous ne pouvez pas modifier TextBlock.
Lorsque vous exécutez à nouveau cette application, vous verrez la sortie suivante.
Modifions maintenant le texte dans la deuxième zone de texte de la première ligne d'Allain à Upston et appuyez sur tab pour perdre le focus. Vous verrez que le texte TextBlock est également mis à jour.
Cela est dû au fait que les liaisons des TextBoxes sont définies sur TwoWay et qu'il met également à jour le modèle, et à partir du modèle à nouveau, le TextBlock est mis à jour.
Un modèle décrit l'aspect général et l'aspect visuel du contrôle. Pour chaque contrôle, un modèle par défaut lui est associé qui donne l'apparence à ce contrôle. Dans l'application WPF, vous pouvez facilement créer vos propres modèles lorsque vous souhaitez personnaliser le comportement visuel et l'apparence visuelle d'un contrôle. La connectivité entre la logique et le modèle peut être obtenue par liaison de données.
Dans MVVM, il existe une autre forme principale connue sous le nom de première construction ViewModel.
La première approche de construction de ViewModel exploite les capacités des modèles de données implicites dans WPF.
Les modèles de données implicites peuvent sélectionner automatiquement un modèle approprié dans le dictionnaire de ressources actuel pour un élément qui utilise la liaison de données. Ils le font en fonction du type d'objet de données rendu par la liaison de données. Tout d'abord, vous devez avoir un élément qui se lie à un objet de données.
Jetons à nouveau un coup d'œil à notre exemple simple dans lequel vous comprendrez comment vous pouvez d'abord afficher le modèle en exploitant les modèles de données, en particulier les modèles de données implicites. Voici l'implémentation de notre classe StudentViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public StudentViewModel() {
LoadStudents();
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
}
}
Vous pouvez voir que le ViewModel ci-dessus est inchangé. Nous continuerons avec le même exemple du chapitre précédent. Cette classe ViewModel expose simplement la propriété de collection Students et la remplit lors de la construction. Allons au fichier StudentView.xaml, supprimons l'implémentation existante et définissons un modèle de données dans la section Ressources.
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
Ajoutez maintenant une zone de liste et les données lient cette zone de liste à la propriété des étudiants, comme indiqué dans le code suivant.
<ListBox ItemsSource = "{Binding Students}" ItemTemplate = "{StaticResource studentsTemplate}"/>
Dans la section Ressource, le DataTemplate a une clé de StudentsTemplate, puis pour utiliser réellement ce modèle, nous devons utiliser la propriété ItemTemplate d'un ListBox. Vous pouvez maintenant voir que nous demandons à la zone de liste d'utiliser ce modèle spécifique pour rendre ces étudiants. Voici l'implémentation complète du fichier StudentView.xaml.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate x:Key = "studentsTemplate">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox
ItemsSource = "{Binding Students}"
ItemTemplate = "{StaticResource studentsTemplate}"/>
</Grid>
</UserControl>
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la fenêtre suivante, qui contient une ListBox. Chaque ListBoxItem contient les données d'objet de classe Student qui sont affichées dans les zones TextBlock et Text.
Pour en faire un modèle implicite, nous devons supprimer la propriété ItemTemplate d'une zone de liste et ajouter une propriété DataType dans notre définition de modèle, comme indiqué dans le code suivant.
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
Dans DataTemplate, l'extension de balisage x: Type est très importante et ressemble à un type d'opérateur en XAML. Donc, fondamentalement, nous devons pointer vers le type de données Student qui se trouve dans l'espace de noms MVVMDemo.Model. Voici le fichier XAML complet mis à jour.
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d" d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ListBox ItemsSource = "{Binding Students}"/>
</Grid>
</UserControl>
Lorsque vous exécutez à nouveau cette application, vous obtiendrez toujours le même rendu du modèle Etudiants avec données, car il mappe automatiquement le type de l'objet rendu en localisant le DataTemplate approprié.
Nous vous recommandons d'exécuter l'exemple ci-dessus dans une méthode étape par étape pour une meilleure compréhension.
Dans ce chapitre, nous allons apprendre à ajouter de l'interactivité aux applications MVVM et à appeler proprement la logique. Vous verrez également que tout cela se fait en conservant le couplage lâche et une bonne structuration qui est au cœur du pattern MVVM. Pour comprendre tout cela, apprenons d'abord les commandes.
Affichage / Affichage de la communication du modèle via des commandes
Le modèle de commande a été bien documenté et utilise fréquemment le modèle de conception depuis quelques décennies. Dans ce modèle, il y a deux acteurs principaux, l'invocateur et le receveur.
Invocateur
L'invocateur est un morceau de code qui peut exécuter une logique impérative.
En règle générale, il s'agit d'un élément d'interface utilisateur avec lequel l'utilisateur interagit, dans le contexte d'une infrastructure d'interface utilisateur.
Il peut s'agir simplement d'un autre morceau de code logique ailleurs dans l'application.
Destinataire
Le récepteur est la logique destinée à être exécutée lorsque l'invocateur se déclenche.
Dans le contexte de MVVM, le récepteur est généralement une méthode de votre ViewModel qui doit être appelée.
Entre ces deux, vous avez une couche d'obstruction, ce qui implique que l'invocateur et le receveur n'ont pas à se connaître explicitement. Ceci est généralement représenté comme une abstraction d'interface exposée à l'invocateur et une implémentation concrète de cette interface est capable d'appeler le récepteur.
Jetons un coup d'œil à un exemple simple dans lequel vous apprendrez les commandes et comment les utiliser pour communiquer entre View et ViewModel. Dans ce chapitre, nous continuerons avec le même exemple du dernier chapitre.
Dans le fichier StudentView.xaml, nous avons un ListBox qui relie les données des étudiants à partir d'un ViewModel. Ajoutons maintenant un bouton pour supprimer un étudiant de la ListBox.
La chose importante est que travailler avec des commandes sur un bouton est très facile car elles ont une propriété de commande à connecter à une ICommand.
Ainsi, nous pouvons exposer une propriété sur notre ViewModel qui a un ICommand et s'y lie à partir de la propriété de commande du bouton, comme indiqué dans le code suivant.
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
Ajoutons une nouvelle classe dans votre projet, qui implémentera l'interface ICommand. Voici l'implémentation de l'interface ICommand.
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod){
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime
is longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod();
}
}
}
}
Comme vous pouvez le voir, il s'agit d'une implémentation de délégation simple d'ICommand où nous avons deux délégués, un pour executeMethod et un pour canExecuteMethod qui peut être transmis lors de la construction.
Dans l'implémentation ci-dessus, il y a deux constructeurs surchargés, un pour seulement executeMethod et un pour les deux executeMethod et je peux canExecuteMethod.
Ajoutons une propriété de type MyICommand dans la classe StudentView Model. Nous devons maintenant construire une instance dans StudentViewModel. Nous utiliserons le constructeur surchargé de MyICommand qui prend deux paramètres.
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
Ajoutez maintenant l'implémentation des méthodes OnDelete et CanDelete.
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
Nous devons également ajouter un nouveau SelectedStudent afin que l'utilisateur puisse supprimer l'élément sélectionné de ListBox.
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
Voici l'implémentation complète de la classe ViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
}
}
Dans StudentView.xaml, nous devons ajouter la propriété SelectedItem dans un ListBox qui se liera à la propriété SelectStudent.
<ListBox ItemsSource = "{Binding Students}" SelectedItem = "{Binding SelectedStudent}"/>
Voici le fichier xaml complet.
<UserControl x:Class = "MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la fenêtre suivante.
Vous pouvez voir que le bouton de suppression est désactivé. Il sera activé lorsque vous sélectionnez un élément.
Lorsque vous sélectionnez un élément et appuyez sur Supprimer. Vous verrez que la liste des éléments sélectionnés est supprimée et le bouton de suppression est à nouveau désactivé.
Nous vous recommandons d'exécuter l'exemple ci-dessus étape par étape pour une meilleure compréhension.
Lors de la création d'applications MVVM, vous décomposez généralement des écrans d'informations complexes en un ensemble de vues parent et enfant, où les vues enfants sont contenues dans les vues parent dans des panneaux ou des contrôles de conteneur et forment elles-mêmes une hiérarchie d'utilisation.
Après avoir décomposé les vues complexes, cela ne signifie pas que chaque élément de contenu enfant que vous séparez dans son propre fichier XAML doit nécessairement être une vue MVVM.
Le morceau de contenu fournit simplement la structure pour rendre quelque chose à l'écran et ne prend en charge aucune entrée ou manipulation par l'utilisateur pour ce contenu.
Il peut ne pas avoir besoin d'un ViewModel distinct, mais il peut simplement s'agir d'un bloc XAML qui effectue le rendu en fonction des propriétés exposées par le ViewModel parent.
Enfin, si vous avez une hiérarchie de vues et de ViewModels, le ViewModel parent peut devenir un hub pour les communications afin que chaque ViewModel enfant puisse rester découplé des autres ViewModels enfants et de leur parent autant que possible.
Jetons un œil à un exemple dans lequel nous définirons une hiérarchie simple entre différentes vues. Créer un nouveau projet d'application WPFMVVMHierarchiesDemo
Step 1 - Ajoutez les trois dossiers (Model, ViewModel et Views) dans votre projet.
Step 2 - Ajoutez des classes Customer et Order dans le dossier Model, CustomerListView et OrderView dans le dossier Views, et CustomerListViewModel et OrderViewModel dans le dossier ViewModel, comme illustré dans l'image suivante.
Step 3- Ajoutez des blocs de texte à la fois dans CustomerListView et OrderView. Voici le fichier CustomerListView.xaml.
<UserControl x:Class="MVVMHierarchiesDemo.Views.CustomerListView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Customer List View"/>
</Grid>
</UserControl>
Voici le fichier OrderView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.OrderView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d ="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views" mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<TextBlock Text = "Order View"/>
</Grid>
</UserControl>
Maintenant, nous avons besoin de quelque chose pour héberger ces vues, et un bon endroit pour cela dans notre MainWindow car c'est une application simple. Nous avons besoin d'un contrôle de conteneur que nous pouvons placer nos vues et les changer en mode navigation. Pour cela, nous devons ajouter ContentControl dans notre fichier MainWindow.xaml et nous utiliserons sa propriété content et la lierons à une référence ViewModel.
Définissez maintenant les modèles de données pour chaque vue dans un dictionnaire de ressources. Voici le fichier MainWindow.xaml. Notez comment chaque modèle de données mappe un type de données (le type ViewModel) à une vue correspondante.
<Window x:Class = "MVVMHierarchiesDemo.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"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<ContentControl Content = "{Binding CurrentView}"/>
</Grid>
</Window>
Chaque fois que le modèle de vue actuel est défini sur une instance d'un CustomerListViewModel, il restituera un CustomerListView avec le ViewModel connecté. C'est un ViewModel de commande, il rendra OrderView et ainsi de suite.
Nous avons maintenant besoin d'un ViewModel qui a une propriété CurrentViewModel et une logique et une commande pour pouvoir changer la référence actuelle de ViewModel à l'intérieur de la propriété.
Créons un ViewModel pour cette MainWindow appelé MainWindowViewModel. Nous pouvons simplement créer une instance de notre ViewModel à partir de XAML et l'utiliser pour définir la propriété DataContext de la fenêtre. Pour cela, nous devons créer une classe de base pour encapsuler l'implémentation de INotifyPropertyChanged pour nos ViewModels.
L'idée principale derrière cette classe est d'encapsuler l'implémentation INotifyPropertyChanged et de fournir des méthodes d'assistance à la classe dérivée afin qu'elle puisse facilement déclencher les notifications appropriées. Voici l'implémentation de la classe BindableBase.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class BindableBase : INotifyPropertyChanged {
protected virtual void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Il est maintenant temps de commencer à changer de vue à l'aide de notre propriété CurrentViewModel. Nous avons juste besoin d'un moyen de piloter le réglage de cette propriété. Et nous allons faire en sorte que l'utilisateur final puisse commander d'accéder à la liste des clients ou à la vue des commandes. Ajoutez d'abord une nouvelle classe dans votre projet qui implémentera l'interface ICommand. Voici l'implémentation de l'interface ICommand.
using System;
using System.Windows.Input;
namespace MVVMHierarchiesDemo {
public class MyICommand<T> : ICommand {
Action<T> _TargetExecuteMethod;
Func<T, bool> _TargetCanExecuteMethod;
public MyICommand(Action<T> executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action<T> executeMethod, Func<T, bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
#region ICommand Members
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
T tparm = (T)parameter;
return _TargetCanExecuteMethod(tparm);
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime is
longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod((T)parameter);
}
}
#endregion
}
}
Nous devons maintenant configurer une navigation de niveau supérieur vers ceux-ci vers ViewModels et la logique pour cette commutation doit appartenir à MainWindowViewModel. Pour cela, nous allons utiliser une méthode appelée sur la navigation qui prend une chaîne de destination et renvoie la propriété CurrentViewModel.
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
Pour la navigation de ces différentes vues, nous devons ajouter deux boutons dans notre fichier MainWindow.xaml. Voici l'implémentation complète du fichier XAML.
<Window x:Class = "MVVMHierarchiesDemo.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"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
Voici l'implémentation complète de MainWindowViewModel.
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get {return _CurrentViewModel;}
set {SetProperty(ref _CurrentViewModel, value);}
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
Dérivez tous vos ViewModels de la classe BindableBase. Lorsque le code ci-dessus est compilé et exécuté, vous verrez la sortie suivante.
Comme vous pouvez le voir, nous n'avons ajouté que deux boutons et un CurrentViewModel sur notre MainWindow. Si vous cliquez sur n'importe quel bouton, il accédera à cette vue particulière. Cliquons sur le bouton Clients et vous verrez que le CustomerListView est affiché.
Nous vous recommandons d'exécuter l'exemple ci-dessus étape par étape pour une meilleure compréhension.
Dans ce chapitre, nous découvrirons les validations. Nous examinerons également un moyen propre de faire la validation avec ce que les liaisons WPF prennent déjà en charge, mais en le liant aux composants MVVM.
Validation dans MVVM
Lorsque votre application commence à accepter l'entrée de données des utilisateurs finaux, vous devez envisager de valider cette entrée.
Assurez-vous qu'il est conforme à vos exigences générales.
WPF a d'excellentes versions et fonctionnalités dans le système de liaison pour valider l'entrée et vous pouvez toujours tirer parti de toutes ces fonctionnalités lorsque vous utilisez MVVM.
Gardez à l'esprit que la logique qui prend en charge votre validation et définit les règles existantes pour les propriétés qui doivent faire partie du modèle ou du ViewModel, et non de la vue elle-même.
Vous pouvez toujours utiliser tous les moyens d'exprimer la validation qui sont pris en charge par la liaison de données WPF, y compris -
- Lancer des exceptions sur une propriété est défini.
- Implémentation de l'interface IDataErrorInfo.
- Implémentation d'INotifyDataErrorInfo.
- Utilisez les règles de validation WPF.
En général, INotifyDataErrorInfo est recommandé et a été introduit dans WPF .net 4.5 et il prend en charge l'interrogation de l'objet pour les erreurs associées aux propriétés et corrige également quelques lacunes avec toutes les autres options. Plus précisément, il permet une validation asynchrone. Il permet aux propriétés d'avoir plus d'une erreur qui leur est associée.
Ajout de la validation
Jetons un coup d'œil à un exemple dans lequel nous ajouterons la prise en charge de la validation à notre vue d'entrée, et dans une grande application, vous en aurez probablement besoin à plusieurs endroits dans votre application. Parfois sur les vues, parfois sur les ViewModels et parfois sur ces objets d'assistance, il existe des wrappers autour des objets de modèle.
C'est une bonne pratique pour placer la prise en charge de la validation dans une classe de base commune que vous pouvez ensuite hériter de différents scénarios.
La classe de base prendra en charge INotifyDataErrorInfo afin que cette validation soit déclenchée lorsque les propriétés changent.
Créez, ajoutez une nouvelle classe appelée ValidatableBindableBase. Puisque nous avons déjà une classe de base pour la gestion des modifications de propriété, dérivons-en la classe de base et implémentons également l'interface INotifyDataErrorInfo.
Voici l'implémentation de la classe ValidatableBindableBase.
using System;
using System.Collections.Generic;
using System.ComponentModel;
//using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace MVVMHierarchiesDemo {
public class ValidatableBindableBase : BindableBase, INotifyDataErrorInfo {
private Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
public event EventHandler<DataErrorsChangedEventArgs>
ErrorsChanged = delegate { };
public System.Collections.IEnumerable GetErrors(string propertyName) {
if (_errors.ContainsKey(propertyName))
return _errors[propertyName];
else
return null;
}
public bool HasErrors {
get { return _errors.Count > 0; }
}
protected override void SetProperty<T>(ref T member, T val,
[CallerMemberName] string propertyName = null) {
base.SetProperty<T>(ref member, val, propertyName);
ValidateProperty(propertyName, val);
}
private void ValidateProperty<T>(string propertyName, T value) {
var results = new List<ValidationResult>();
//ValidationContext context = new ValidationContext(this);
//context.MemberName = propertyName;
//Validator.TryValidateProperty(value, context, results);
if (results.Any()) {
//_errors[propertyName] = results.Select(c => c.ErrorMessage).ToList();
} else {
_errors.Remove(propertyName);
}
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
}
}
Ajoutez maintenant AddEditCustomerView et AddEditCustomerViewModel dans les dossiers respectifs. Voici le code d'AddEditCustomerView.xaml.
<UserControl x:Class = "MVVMHierarchiesDemo.Views.AddEditCustomerView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo.Views"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Grid x:Name = "grid1"
HorizontalAlignment = "Left"
DataContext = "{Binding Customer}"
Margin = "10,10,0,0"
VerticalAlignment = "Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "Auto" />
<ColumnDefinition Width = "Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
<RowDefinition Height = "Auto" />
</Grid.RowDefinitions>
<Label Content = "First Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "0"
VerticalAlignment = "Center" />
<TextBox x:Name = "firstNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "0"
Text = "{Binding FirstName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Last Name:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "1"
VerticalAlignment = "Center" />
<TextBox x:Name = "lastNameTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "1"
Text = "{Binding LastName, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Email:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "2"
VerticalAlignment = "Center" />
<TextBox x:Name = "emailTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "2"
Text = "{Binding Email, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
<Label Content = "Phone:"
Grid.Column = "0"
HorizontalAlignment = "Left"
Margin = "3"
Grid.Row = "3"
VerticalAlignment = "Center" />
<TextBox x:Name = "phoneTextBox"
Grid.Column = "1"
HorizontalAlignment = "Left"
Height = "23"
Margin = "3"
Grid.Row = "3"
Text = "{Binding Phone, ValidatesOnNotifyDataErrors = True}"
VerticalAlignment = "Center"
Width = "120" />
</Grid>
<Grid Grid.Row = "1">
<Button Content = "Save"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Add"
Command = "{Binding SaveCommand}"
HorizontalAlignment = "Left"
Margin = "25,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
<Button Content = "Cancel"
Command = "{Binding CancelCommand}"
HorizontalAlignment = "Left"
Margin = "150,5,0,0"
VerticalAlignment = "Top"
Width = "75" />
</Grid>
</Grid>
</UserControl>
Voici l'implémentation d'AddEditCustomerViewModel.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
public AddEditCustomerViewModel() {
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value);}
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value);}
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
Done();
}
private bool CanSave() {
return !Customer.HasErrors;
}
}
}
Voici l'implémentation de la classe SimpleEditableCustomer.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Model {
public class SimpleEditableCustomer : ValidatableBindableBase {
private Guid _id;
public Guid Id {
get { return _id; }
set { SetProperty(ref _id, value); }
}
private string _firstName;
[Required]
public string FirstName {
get { return _firstName; }
set { SetProperty(ref _firstName, value); }
}
private string _lastName;
[Required]
public string LastName {
get { return _lastName; }
set { SetProperty(ref _lastName, value); }
}
private string _email;
[EmailAddress]
public string Email {
get { return _email; }
set { SetProperty(ref _email, value); }
}
private string _phone;
[Phone]
public string Phone {
get { return _phone; }
set { SetProperty(ref _phone, value); }
}
}
}
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la fenêtre suivante.
Lorsque vous appuyez sur le bouton Ajouter un client, la vue suivante s'affiche. Lorsque l'utilisateur laisse un champ vide, il sera mis en surbrillance et le bouton Enregistrer sera désactivé.
Dans ce chapitre, nous discuterons brièvement de l'injection de dépendances. Nous avons déjà couvert la liaison de données qui découpe les vues et les ViewModels les uns des autres, ce qui leur permet de communiquer sans savoir explicitement ce qui se passe à l'autre extrémité de la communication.
Nous avons maintenant besoin de quelque chose de similaire pour découpler notre ViewModel des services client.
Au début de la programmation orientée objet, les développeurs ont été confrontés au problème de la création et de la récupération d'instances de classes dans les applications. Différentes solutions ont été proposées à ce problème.
Au cours des dernières années, l'injection de dépendances et l'inversion de contrôle (IoC) ont gagné en popularité parmi les développeurs et ont pris le pas sur certaines solutions plus anciennes telles que le modèle Singleton.
Injection de dépendances / conteneurs IoC
L'IoC et l'injection de dépendances sont deux modèles de conception étroitement liés et le conteneur est essentiellement un morceau de code d'infrastructure qui exécute ces deux modèles pour vous.
Le modèle IoC consiste à déléguer la responsabilité de la construction et le modèle d'injection de dépendances consiste à fournir des dépendances à un objet qui a déjà été construit.
Ils peuvent tous deux être traités comme une approche de construction en deux phases. Lorsque vous utilisez un conteneur, le conteneur assume plusieurs responsabilités qui sont les suivantes -
- Il construit un objet lorsqu'il est demandé.
- Le conteneur déterminera de quoi dépend cet objet.
- Construire ces dépendances.
- Les injecter dans l'objet en cours de construction.
- Processus récursif.
Voyons comment nous pouvons utiliser l'injection de dépendances pour rompre le découplage entre ViewModels et les services client. Nous allons câbler le formulaire AddEditCustomerViewModel de gestion de sauvegarde en utilisant l'injection de dépendance liée à cela.
Nous devons d'abord créer une nouvelle interface dans notre projet dans le dossier Services. Si vous n'avez pas de dossier de services dans votre projet, créez-le d'abord et ajoutez l'interface suivante dans le dossier Services.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public interface ICustomersRepository {
Task<List<Customer>> GetCustomersAsync();
Task<Customer> GetCustomerAsync(Guid id);
Task<Customer> AddCustomerAsync(Customer customer);
Task<Customer> UpdateCustomerAsync(Customer customer);
Task DeleteCustomerAsync(Guid customerId);
}
}
Voici l'implémentation de ICustomersRepository.
using MVVMHierarchiesDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq; using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.Services {
public class CustomersRepository : ICustomersRepository {
ZzaDbContext _context = new ZzaDbContext();
public Task<List<Customer>> GetCustomersAsync() {
return _context.Customers.ToListAsync();
}
public Task<Customer> GetCustomerAsync(Guid id) {
return _context.Customers.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<Customer> AddCustomerAsync(Customer customer){
_context.Customers.Add(customer);
await _context.SaveChangesAsync();
return customer;
}
public async Task<Customer> UpdateCustomerAsync(Customer customer) {
if (!_context.Customers.Local.Any(c => c.Id == customer.Id)) {
_context.Customers.Attach(customer);
}
_context.Entry(customer).State = EntityState.Modified;
await _context.SaveChangesAsync();
return customer;
}
public async Task DeleteCustomerAsync(Guid customerId) {
var customer = _context.Customers.FirstOrDefault(c => c.Id == customerId);
if (customer != null) {
_context.Customers.Remove(customer);
}
await _context.SaveChangesAsync();
}
}
}
Le moyen simple de gérer la sauvegarde consiste à ajouter une nouvelle instance de ICustomersRepository dans AddEditCustomerViewModel et à surcharger le constructeur AddEditCustomerViewModel et CustomerListViewModel.
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
Mettez à jour la méthode OnSave comme indiqué dans le code suivant.
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
Voici le AddEditCustomerViewModel complet.
using MVVMHierarchiesDemo.Model;
using MVVMHierarchiesDemo.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo.ViewModel {
class AddEditCustomerViewModel : BindableBase {
private ICustomersRepository _repo;
public AddEditCustomerViewModel(ICustomersRepository repo) {
_repo = repo;
CancelCommand = new MyIcommand(OnCancel);
SaveCommand = new MyIcommand(OnSave, CanSave);
}
private bool _EditMode;
public bool EditMode {
get { return _EditMode; }
set { SetProperty(ref _EditMode, value); }
}
private SimpleEditableCustomer _Customer;
public SimpleEditableCustomer Customer {
get { return _Customer; }
set { SetProperty(ref _Customer, value); }
}
private Customer _editingCustomer = null;
public void SetCustomer(Customer cust) {
_editingCustomer = cust;
if (Customer != null) Customer.ErrorsChanged -= RaiseCanExecuteChanged;
Customer = new SimpleEditableCustomer();
Customer.ErrorsChanged += RaiseCanExecuteChanged;
CopyCustomer(cust, Customer);
}
private void RaiseCanExecuteChanged(object sender, EventArgs e) {
SaveCommand.RaiseCanExecuteChanged();
}
public MyIcommand CancelCommand { get; private set; }
public MyIcommand SaveCommand { get; private set; }
public event Action Done = delegate { };
private void OnCancel() {
Done();
}
private async void OnSave() {
UpdateCustomer(Customer, _editingCustomer);
if (EditMode)
await _repo.UpdateCustomerAsync(_editingCustomer);
else
await _repo.AddCustomerAsync(_editingCustomer);
Done();
}
private void UpdateCustomer(SimpleEditableCustomer source, Customer target) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
private bool CanSave() {
return !Customer.HasErrors;
}
private void CopyCustomer(Customer source, SimpleEditableCustomer target) {
target.Id = source.Id;
if (EditMode) {
target.FirstName = source.FirstName;
target.LastName = source.LastName;
target.Phone = source.Phone;
target.Email = source.Email;
}
}
}
}
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la même sortie, mais les ViewModels sont maintenant plus lâchement découplés.
Lorsque vous appuyez sur le bouton Ajouter un client, vous verrez la vue suivante. Lorsque l'utilisateur laisse un champ vide, il sera mis en surbrillance et le bouton Enregistrer sera désactivé.
Un événement est une construction de programmation qui réagit à un changement d'état, notifiant tous les points de terminaison qui se sont inscrits pour la notification. Principalement, les événements sont utilisés pour informer une entrée utilisateur via la souris et le clavier, mais leur utilité ne se limite pas à cela. Chaque fois qu'un changement d'état est détecté, peut-être lorsqu'un objet a été chargé ou initialisé, un événement peut être déclenché pour alerter les tiers intéressés.
Dans une application WPF qui utilise le modèle de conception MVVM (Model-View-ViewModel), le modèle de vue est le composant responsable de la gestion de la logique et de l'état de présentation de l'application.
Le fichier code-behind de la vue ne doit contenir aucun code pour gérer les événements qui sont déclenchés à partir de tout élément de l'interface utilisateur (UI) tel qu'un bouton ou une zone de liste déroulante et ne doit pas contenir de logique spécifique au domaine.
Idéalement, le code-behind d'une vue contient uniquement un constructeur qui appelle la méthode InitializeComponent et peut-être du code supplémentaire pour contrôler ou interagir avec la couche de vue qui est difficile ou inefficace à exprimer en XAML, par exemple des animations complexes.
Jetons un coup d'œil à un exemple simple d'événements de clic de bouton dans notre application. Voici le code XAML du fichier MainWindow.xaml dans lequel vous verrez deux boutons.
<Window x:Class = "MVVMHierarchiesDemo.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"
xmlns:local = "clr-namespace:MVVMHierarchiesDemo"
xmlns:views = "clr-namespace:MVVMHierarchiesDemo.Views"
xmlns:viewModels = "clr-namespace:MVVMHierarchiesDemo.ViewModel"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType = "{x:Type viewModels:CustomerListViewModel}">
<views:CustomerListView/>
</DataTemplate>
<DataTemplate DataType = "{x:Type viewModels:OrderViewModel}">
<views:OrderView/>
</DataTemplate>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height = "Auto" />
<RowDefinition Height = "*" />
</Grid.RowDefinitions>
<Grid x:Name = "NavBar">
<Grid.ColumnDefinitions>
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
<ColumnDefinition Width = "*" />
</Grid.ColumnDefinitions>
<Button Content = "Customers"
Command = "{Binding NavCommand}"
CommandParameter = "customers"
Grid.Column = "0" />
<Button Content = "Order"
Command = "{Binding NavCommand}"
CommandParameter = "orders"
Grid.Column = "2" />
</Grid>
<Grid x:Name = "MainContent" Grid.Row = "1">
<ContentControl Content = "{Binding CurrentViewModel}" />
</Grid>
</Grid>
</Window>
Vous pouvez voir que la propriété Click sur le bouton n'est pas utilisée dans le fichier XAML ci-dessus, mais les propriétés Command et CommandParameter sont utilisées pour charger différentes vues lorsque le bouton est enfoncé. Vous devez maintenant définir l'implémentation des commandes dans le fichier MainWindowViewModel.cs mais pas dans le fichier View. Voici l'implémentation complète de MainWindowViewModel.
using MVVMHierarchiesDemo.ViewModel;
using MVVMHierarchiesDemo.Views;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMHierarchiesDemo {
class MainWindowViewModel : BindableBase {
public MainWindowViewModel() {
NavCommand = new MyICommand<string>(OnNav);
}
private CustomerListViewModel custListViewModel = new CustomerListViewModel();
private OrderViewModel orderViewModelModel = new OrderViewModel();
private BindableBase _CurrentViewModel;
public BindableBase CurrentViewModel {
get { return _CurrentViewModel; }
set { SetProperty(ref _CurrentViewModel, value); }
}
public MyICommand<string> NavCommand { get; private set; }
private void OnNav(string destination) {
switch (destination) {
case "orders":
CurrentViewModel = orderViewModelModel;
break;
case "customers":
default:
CurrentViewModel = custListViewModel;
break;
}
}
}
}
Dérivez tous vos ViewModels de la classe BindableBase. Lorsque le code ci-dessus est compilé et exécuté, vous verrez la sortie suivante.
Comme vous pouvez le voir, nous n'avons ajouté que deux boutons et un CurrentViewModel sur notre MainWindow. Maintenant, si vous cliquez sur n'importe quel bouton, il accédera à cette vue particulière. Cliquons sur le bouton Clients et vous verrez que le CustomerListView est affiché.
Nous vous recommandons d'exécuter l'exemple ci-dessus dans une méthode étape par étape pour une meilleure compréhension.
L'idée derrière les tests unitaires est de prendre des morceaux discrets de code (unités) et d'écrire des méthodes de test qui utilisent le code d'une manière attendue, puis de tester pour voir si elles obtiennent les résultats attendus.
Étant eux-mêmes du code, les tests unitaires sont compilés comme le reste du projet.
Ils sont également exécutés par le logiciel de test, qui peut accélérer chaque test, donnant effectivement le pouce vers le haut ou le pouce vers le bas pour indiquer si le test a réussi ou échoué, respectivement.
Jetons un coup d'œil à un exemple créé précédemment. Voici la mise en œuvre du modèle étudiant.
using System.ComponentModel;
namespace MVVMDemo.Model {
public class StudentModel {}
public class Student : INotifyPropertyChanged {
private string firstName;
private string lastName;
public string FirstName {
get { return firstName; }
set {
if (firstName != value) {
firstName = value;
RaisePropertyChanged("FirstName");
RaisePropertyChanged("FullName");
}
}
}
public string LastName {
get { return lastName; }
set {
if (lastName != value) {
lastName = value;
RaisePropertyChanged("LastName");
RaisePropertyChanged("FullName");
}
}
}
public string FullName {
get {
return firstName + " " + lastName;
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string property) {
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
}
Voici l'implémentation de StudentView.
<UserControl x:Class="MVVMDemo.Views.StudentView"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
xmlns:local = "clr-namespace:MVVMDemo.Views"
xmlns:viewModel = "clr-namespace:MVVMDemo.ViewModel"
xmlns:data = "clr-namespace:MVVMDemo.Model"
xmlns:vml = "clr-namespace:MVVMDemo.VML"
vml:ViewModelLocator.AutoHookedUpViewModel = "True"
mc:Ignorable = "d"
d:DesignHeight = "300" d:DesignWidth = "300">
<UserControl.Resources>
<DataTemplate DataType = "{x:Type data:Student}">
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}"
Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}"
Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}"
Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid>
<StackPanel Orientation = "Horizontal">
<ListBox ItemsSource = "{Binding Students}"
SelectedItem = "{Binding SelectedStudent}"/>
<Button Content = "Delete"
Command = "{Binding DeleteCommand}"
HorizontalAlignment = "Left"
VerticalAlignment = "Top"
Width = "75" />
</StackPanel>
</Grid>
</UserControl>
Voici l'implémentation de StudentViewModel.
using MVVMDemo.Model;
using System.Collections.ObjectModel;
using System.Windows.Input;
using System;
namespace MVVMDemo.ViewModel {
public class StudentViewModel {
public MyICommand DeleteCommand { get; set;}
public StudentViewModel() {
LoadStudents();
DeleteCommand = new MyICommand(OnDelete, CanDelete);
}
public ObservableCollection<Student> Students {
get;
set;
}
public void LoadStudents() {
ObservableCollection<Student> students = new ObservableCollection<Student>();
students.Add(new Student { FirstName = "Mark", LastName = "Allain" });
students.Add(new Student { FirstName = "Allen", LastName = "Brown" });
students.Add(new Student { FirstName = "Linda", LastName = "Hamerski" });
Students = students;
}
private Student _selectedStudent;
public Student SelectedStudent {
get {
return _selectedStudent;
}
set {
_selectedStudent = value;
DeleteCommand.RaiseCanExecuteChanged();
}
}
private void OnDelete() {
Students.Remove(SelectedStudent);
}
private bool CanDelete() {
return SelectedStudent != null;
}
public int GetStudentCount() {
return Students.Count;
}
}
}
Voici le fichier MainWindow.xaml.
<Window x:Class = "MVVMDemo.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"
xmlns:local = "clr-namespace:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable = "d"
Title = "MainWindow" Height = "350" Width = "525">
<Grid>
<views:StudentView x:Name = "StudentViewControl"/>
</Grid>
</Window>
Voici l'implémentation MyICommand, qui implémente l'interface ICommand.
using System;
using System.Windows.Input;
namespace MVVMDemo {
public class MyICommand : ICommand {
Action _TargetExecuteMethod;
Func<bool> _TargetCanExecuteMethod;
public MyICommand(Action executeMethod) {
_TargetExecuteMethod = executeMethod;
}
public MyICommand(Action executeMethod, Func<bool> canExecuteMethod) {
_TargetExecuteMethod = executeMethod;
_TargetCanExecuteMethod = canExecuteMethod;
}
public void RaiseCanExecuteChanged() {
CanExecuteChanged(this, EventArgs.Empty);
}
bool ICommand.CanExecute(object parameter) {
if (_TargetCanExecuteMethod != null) {
return _TargetCanExecuteMethod();
}
if (_TargetExecuteMethod != null) {
return true;
}
return false;
}
// Beware - should use weak references if command instance lifetime
is longer than lifetime of UI objects that get hooked up to command
// Prism commands solve this in their implementation
public event EventHandler CanExecuteChanged = delegate { };
void ICommand.Execute(object parameter) {
if (_TargetExecuteMethod != null) {
_TargetExecuteMethod();
}
}
}
}
Lorsque le code ci-dessus est compilé et exécuté, vous verrez la sortie suivante sur votre fenêtre principale.
Pour écrire un test unitaire pour l'exemple ci-dessus, ajoutons un nouveau projet de test à la solution.
Ajoutez une référence au projet par un clic droit sur Références.
Sélectionnez le projet existant et cliquez sur OK.
Ajoutons maintenant un test simple qui vérifiera le nombre d'étudiants comme indiqué dans le code suivant.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod]
public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 3);
}
}
}
Pour exécuter ce test, sélectionnez l'option de menu Test → Exécuter → Tous les tests.
Vous pouvez voir dans l'Explorateur de tests que le test est réussi, car dans le StudentViewModel, trois étudiants sont ajoutés. Modifiez la condition de comptage de 3 à 4 comme indiqué dans le code suivant.
using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MVVMDemo.ViewModel;
namespace MVVMTest {
[TestClass]
public class UnitTest1 {
[TestMethod] public void TestMethod1() {
StudentViewModel sViewModel = new StudentViewModel();
int count = sViewModel.GetStudentCount();
Assert.IsTrue(count == 4);
}
}
}
Lorsque le plan de test est à nouveau exécuté, vous verrez que le test a échoué car le nombre d'élèves n'est pas égal à 4.
Nous vous recommandons d'exécuter l'exemple ci-dessus dans une méthode étape par étape pour une meilleure compréhension.
Dans ce chapitre, nous aborderons les toolkits ou frameworks MVVM disponibles. Vous pouvez également utiliser ces frameworks afin de ne pas avoir à écrire un tas de code répétitif pour implémenter vous-même le modèle MVVM. Voici quelques-uns des frameworks les plus populaires -
- Prism
- MVVM Lumière
- Caliburn Micro
Prisme
Prism fournit des conseils sous forme d'exemples et de documentation qui vous aident à concevoir et à créer facilement des applications de bureau Windows Presentation Foundation (WPF) riches, flexibles et faciles à entretenir. Applications Internet riches (RIA) construites avec le plug-in de navigateur Microsoft Silverlight et les applications Windows.
Prism utilise des modèles de conception qui incarnent d'importants principes de conception architecturale, tels que la séparation des préoccupations et le couplage lâche.
Prism vous aide à concevoir et à créer des applications en utilisant des composants faiblement couplés qui peuvent évoluer indépendamment mais qui peuvent être facilement et de manière transparente intégrés dans l'application globale.
Ces types d'applications sont appelés applications composites.
Prism a un certain nombre de fonctionnalités prêtes à l'emploi. Voici quelques-unes des caractéristiques importantes de Prism.
Modèle MVVM
Prism prend en charge le modèle MVVM. Il a une classe Bindablebase similaire à celle qui est implémentée dans les chapitres précédents.
Il a un ViewModelLocator flexible qui a des conventions mais vous permet de remplacer ces conventions et de connecter de manière déclarative vos Views et ViewModels d'une manière faiblement couplée.
Modularité
C'est la possibilité de diviser votre code en bibliothèques de classes totalement couplées en parties et de les rassembler au moment de l'exécution en un tout cohérent pour l'utilisateur final, tandis que le code reste entièrement découplé.
Composition / régions de l'interface utilisateur
C'est la possibilité de brancher des vues dans des conteneurs sans la vue qui effectue le branchement, nécessitant une référence explicite au conteneur d'interface utilisateur lui-même.
La navigation
Prism possède des fonctionnalités de navigation qui se superposent aux régions, comme la navigation vers l'avant et vers l'arrière et la pile de navigation qui permet à vos modèles de vue de participer directement au processus de navigation.
Commandes
Prism a des commandes, donc ils ont une commande déléguée qui est très similaire à MyICommand que nous avons utilisée dans les chapitres précédents, sauf qu'elle a une robustesse supplémentaire pour vous protéger des fuites de mémoire.
Événements Pub / Sub
Prism prend également en charge les événements Pub / Sub. Ce sont des événements faiblement couplés où l'éditeur et l'abonné peuvent avoir des durées de vie différentes et n'ont pas besoin d'avoir des références explicites l'un à l'autre pour communiquer via des événements.
MVVM Lumière
MVVM Light est produit par Laurent Bugnion et vous aide à séparer votre vue de votre modèle, ce qui crée des applications plus propres et plus faciles à entretenir et à étendre.
Il crée également des applications testables et vous permet d'avoir une couche d'interface utilisateur beaucoup plus fine (qui est plus difficile à tester automatiquement).
Cette boîte à outils met un accent particulier sur l'ouverture et la modification de l'interface utilisateur dans Blend, y compris la création de données au moment du design pour permettre aux utilisateurs de Blend de «voir quelque chose» lorsqu'ils travaillent avec des contrôles de données.
Caliburn Micro
Il s'agit d'un autre petit framework open-source qui vous aide à implémenter le modèle MVVM et prend également en charge un certain nombre de choses prêtes à l'emploi.
Caliburn Micro est un framework petit mais puissant, conçu pour créer des applications sur toutes les plates-formes XAML.
Grâce à une prise en charge solide de MVVM et d'autres modèles d'interface utilisateur éprouvés, Caliburn Micro vous permettra de créer rapidement votre solution, sans avoir à sacrifier la qualité du code ou la testabilité.