Recherche…
Remarques
Modèles et modèles de vue
La définition d'un modèle est souvent controversée et la ligne entre un modèle et un modèle de vue peut être floue. Certains préfèrent ne pas "polluer" leurs modèles avec l'interface INotifyPropertyChanged
et dupliquer les propriétés du modèle dans le modèle de vue, ce qui implémente cette interface. Comme beaucoup de choses dans le développement de logiciels, il n'y a pas de bonne ou de mauvaise réponse. Soyez pragmatique et faites tout ce qui vous convient.
Voir la séparation
L'intention de MVVM est de séparer ces trois zones distinctes: modèle, modèle de vue et vue. Bien qu'il soit acceptable que la vue accède au modèle de vue (VM) et (indirectement) au modèle, la règle la plus importante avec MVVM est que la VM ne doit pas avoir accès à la vue ou à ses contrôles. La VM doit exposer tout ce dont la vue a besoin, via des propriétés publiques. La VM ne doit pas exposer ou manipuler directement les contrôles de l'interface utilisateur tels que TextBox
, Button
, etc.
Dans certains cas, cette séparation stricte peut être difficile à utiliser, en particulier si vous avez besoin de fonctionnalités d'interface utilisateur complexes. Ici, il est parfaitement acceptable d'utiliser des événements et des gestionnaires d'événements dans le fichier "code-behind" de la vue. Si c'est uniquement une fonctionnalité d'interface utilisateur, utilisez tous les événements de la vue. Il est également acceptable que ces gestionnaires d'événements appellent des méthodes publiques sur l'instance de VM - ne passez pas simplement les références aux contrôles de l'interface utilisateur ou quelque chose du genre.
RelayCommand
Malheureusement, la classe RelayCommand
utilisée dans cet exemple ne fait pas partie du framework WPF (cela aurait dû être le cas!), Mais vous la trouverez dans presque toutes les boîtes à outils du développeur WPF. Une recherche rapide en ligne révélera de nombreux extraits de code que vous pouvez soulever pour créer les vôtres.
ActionCommand
est une alternative utile à RelayCommand
est fournie avec Microsoft.Expression.Interactivity.Core
et fournit des fonctionnalités comparables.
Exemple MVVM de base utilisant WPF et C #
Ceci est un exemple de base pour l'utilisation du modèle MVVM dans une application de bureau Windows, en utilisant WPF et C #. L'exemple de code implémente une simple boîte de dialogue "info utilisateur".
La vue
Le XAML
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="2" Margin="4" Text="{Binding FullName}" HorizontalAlignment="Center" FontWeight="Bold"/>
<Label Grid.Column="0" Grid.Row="1" Margin="4" Content="First Name:" HorizontalAlignment="Right"/>
<!-- UpdateSourceTrigger=PropertyChanged makes sure that changes in the TextBoxes are immediately applied to the model. -->
<TextBox Grid.Column="1" Grid.Row="1" Margin="4" Text="{Binding FirstName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="2" Margin="4" Content="Last Name:" HorizontalAlignment="Right"/>
<TextBox Grid.Column="1" Grid.Row="2" Margin="4" Text="{Binding LastName, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Width="200"/>
<Label Grid.Column="0" Grid.Row="3" Margin="4" Content="Age:" HorizontalAlignment="Right"/>
<TextBlock Grid.Column="1" Grid.Row="3" Margin="4" Text="{Binding Age}" HorizontalAlignment="Left"/>
</Grid>
et le code derrière
public partial class MainWindow : Window
{
private readonly MyViewModel _viewModel;
public MainWindow() {
InitializeComponent();
_viewModel = new MyViewModel();
// The DataContext serves as the starting point of Binding Paths
DataContext = _viewModel;
}
}
Le modèle de vue
// INotifyPropertyChanged notifies the View of property changes, so that Bindings are updated.
sealed class MyViewModel : INotifyPropertyChanged
{
private User user;
public string FirstName {
get {return user.FirstName;}
set {
if(user.FirstName != value) {
user.FirstName = value;
OnPropertyChange("FirstName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
public string LastName {
get { return user.LastName; }
set {
if (user.LastName != value) {
user.LastName = value;
OnPropertyChange("LastName");
// If the first name has changed, the FullName property needs to be udpated as well.
OnPropertyChange("FullName");
}
}
}
// This property is an example of how model properties can be presented differently to the View.
// In this case, we transform the birth date to the user's age, which is read only.
public int Age {
get {
DateTime today = DateTime.Today;
int age = today.Year - user.BirthDate.Year;
if (user.BirthDate > today.AddYears(-age)) age--;
return age;
}
}
// This property is just for display purposes and is a composition of existing data.
public string FullName {
get { return FirstName + " " + LastName; }
}
public MyViewModel() {
user = new User {
FirstName = "John",
LastName = "Doe",
BirthDate = DateTime.Now.AddYears(-30)
};
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChange(string propertyName) {
if(PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Le modèle
sealed class User
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
Le modèle de vue
Le modèle de vue est la "VM" dans MV VM . Cette classe sert d'intermédiaire, expose le (s) modèle (s) à l'interface utilisateur (vue) et gère les requêtes de la vue, telles que les commandes déclenchées par des clics de bouton. Voici un modèle de vue de base:
public class CustomerEditViewModel
{
/// <summary>
/// The customer to edit.
/// </summary>
public Customer CustomerToEdit { get; set; }
/// <summary>
/// The "apply changes" command
/// </summary>
public ICommand ApplyChangesCommand { get; private set; }
/// <summary>
/// Constructor
/// </summary>
public CustomerEditViewModel()
{
CustomerToEdit = new Customer
{
Forename = "John",
Surname = "Smith"
};
ApplyChangesCommand = new RelayCommand(
o => ExecuteApplyChangesCommand(),
o => CustomerToEdit.IsValid);
}
/// <summary>
/// Executes the "apply changes" command.
/// </summary>
private void ExecuteApplyChangesCommand()
{
// E.g. save your customer to database
}
}
Le constructeur crée un objet de modèle Customer
et l'affecte à la propriété CustomerToEdit
, de sorte qu'il soit visible pour la vue.
Le constructeur crée également un objet RelayCommand
et l'affecte à la propriété ApplyChangesCommand
, la rendant à nouveau visible à la vue. Les commandes WPF sont utilisées pour gérer les requêtes de la vue, telles que les clics sur les boutons ou les éléments de menu.
RelayCommand
prend deux paramètres - le premier est le délégué qui est appelé lorsque la commande est exécutée (par exemple en réponse à un clic sur un bouton). Le second paramètre est un délégué qui renvoie une valeur booléenne indiquant si la commande peut être exécutée; Dans cet exemple, il est connecté à la propriété IsValid
l'objet client. Lorsque ceci renvoie false, le bouton ou l'élément de menu lié à cette commande est désactivé (les autres contrôles peuvent se comporter différemment). Ceci est une fonctionnalité simple mais efficace, évitant d'avoir à écrire du code pour activer ou désactiver les contrôles en fonction de différentes conditions.
Si vous obtenez cet exemple opérationnel, essayez de vider un des TextBox
es (pour placer le modèle du Customer
dans un état non valide). Lorsque vous vous éloignez de la zone de TextBox
vous devriez constater que le bouton "Appliquer" est désactivé.
Remarque sur la création de client
Le modèle de vue INotifyPropertyChanged
pas INotifyPropertyChanged
(INPC). Cela signifie que si un autre objet Customer
devait être affecté à la propriété CustomerToEdit
, les contrôles de la vue ne changeraient pas pour refléter le nouvel objet - les TextBox
es contiendraient toujours le prénom et le nom de famille du client précédent.
L'exemple de code fonctionne car le Customer
est créé dans le constructeur du modèle de vue, avant d'être affecté au DataContext
la vue (à quel point les liaisons sont câblées). Dans une application du monde réel, vous pouvez récupérer des clients à partir d'une base de données dans des méthodes autres que le constructeur. Pour prendre cela en charge, la machine virtuelle doit implémenter INPC et la propriété CustomerToEdit
doit être modifiée pour utiliser le modèle getter et setter "étendu" que vous voyez dans l'exemple de code Model, en levant l'événement PropertyChanged
dans le setter.
La ApplyChangesCommand
du modèle de ApplyChangesCommand
n'a pas besoin d'implémenter INPC car il est très peu probable que la commande change. Vous auriez besoin de mettre en œuvre ce modèle si vous créez la commande quelque part autre que le constructeur, par exemple une sorte de Initialize()
méthode.
La règle générale est la suivante: implémenter INPC si la propriété est liée à des contrôles de vue et que la valeur de la propriété peut changer ailleurs que dans le constructeur. Vous n'avez pas besoin d'implémenter INPC si la valeur de la propriété n'est affectée que dans le constructeur (et vous vous épargnerez de la saisie dans le processus).
Le modèle
Le modèle est le premier "M" de M VVM. Le modèle est généralement une classe contenant les données que vous souhaitez exposer via une interface utilisateur.
Voici une classe de modèle très simple exposant quelques propriétés: -
public class Customer : INotifyPropertyChanged
{
private string _forename;
private string _surname;
private bool _isValid;
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Customer forename.
/// </summary>
public string Forename
{
get
{
return _forename;
}
set
{
if (_forename != value)
{
_forename = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Customer surname.
/// </summary>
public string Surname
{
get
{
return _surname;
}
set
{
if (_surname != value)
{
_surname = value;
OnPropertyChanged();
SetIsValid();
}
}
}
/// <summary>
/// Indicates whether the model is in a valid state or not.
/// </summary>
public bool IsValid
{
get
{
return _isValid;
}
set
{
if (_isValid != value)
{
_isValid = value;
OnPropertyChanged();
}
}
}
/// <summary>
/// Sets the value of the IsValid property.
/// </summary>
private void SetIsValid()
{
IsValid = !string.IsNullOrEmpty(Forename) && !string.IsNullOrEmpty(Surname);
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="propertyName">Name of the property.</param>
private void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Cette classe implémente l'interface INotifyPropertyChanged
qui expose un événement PropertyChanged
. Cet événement doit être déclenché chaque fois qu'une des valeurs de propriété change - vous pouvez le voir dans le code ci-dessus. L'événement PropertyChanged
est un élément clé des mécanismes de liaison de données WPF, sans lequel l'interface utilisateur ne serait pas en mesure de refléter les modifications apportées à la valeur d'une propriété.
Le modèle contient également une routine de validation très simple qui est appelée par les configurateurs de propriétés. Il définit une propriété publique indiquant si le modèle est ou non dans un état valide. J'ai inclus cette fonctionnalité pour démontrer une fonctionnalité "spéciale" des commandes WPF, que vous verrez bientôt. L'infrastructure WPF fournit un certain nombre d'approches plus sophistiquées à la validation, mais celles-ci ne relèvent pas de cet article .
La vue
La vue est le "V" dans M V VM. Ceci est votre interface utilisateur. Vous pouvez utiliser le concepteur de glisser-déposer de Visual Studio, mais la plupart des développeurs finissent par coder le fichier XAML brut - une expérience similaire à celle du langage HTML.
Voici le XAML d'une vue simple pour permettre l'édition d'un modèle de Customer
. Plutôt que de créer une nouvelle vue, vous pouvez simplement la coller dans le fichier MainWindow.xaml
un projet WPF, entre les balises <Window ...>
et </Window>
: -
<StackPanel Orientation="Vertical"
VerticalAlignment="Top"
Margin="20">
<Label Content="Forename"/>
<TextBox Text="{Binding CustomerToEdit.Forename}"/>
<Label Content="Surname"/>
<TextBox Text="{Binding CustomerToEdit.Surname}"/>
<Button Content="Apply Changes"
Command="{Binding ApplyChangesCommand}" />
</StackPanel>
Ce code crée un formulaire de saisie de données simple composé de deux TextBox
: une pour le nom du client et une pour le nom de famille. Il y a une Label
au-dessus de chaque zone de TextBox
et un Button
"Appliquer" au bas du formulaire.
Localisez le premier TextBox
et regardez sa propriété Text
:
Text="{Binding CustomerToEdit.Forename}"
Plutôt que de définir le texte du TextBox
sur une valeur fixe, cette syntaxe d'accolade spéciale lie à la place le texte au "chemin" CustomerToEdit.Forename
. Quel est ce chemin par rapport? C'est le "contexte de données" de la vue - dans ce cas, notre modèle de vue. Le chemin de liaison, comme vous pourrez peut-être le comprendre, est la propriété CustomerToEdit
du modèle de vue, de type Customer
qui à son tour expose une propriété appelée Forename
- d'où la notation de chemin "en pointillés".
De même, si vous examinez le XAML du Button
, il comporte une Command
liée à la propriété ApplyChangesCommand
du ApplyChangesCommand
de vue. C'est tout ce qui est nécessaire pour connecter un bouton à la commande de la VM.
Le DataContext
Alors, comment définir le modèle de vue comme étant le contexte de données de la vue? Une façon consiste à le définir dans le code-behind de la vue. Appuyez sur F7 pour afficher ce fichier de code et ajoutez une ligne au constructeur existant pour créer une instance du modèle de vue et l'attribuer à la propriété DataContext
la fenêtre. Il devrait finir par ressembler à ceci:
public MainWindow()
{
InitializeComponent();
// Our new line:-
DataContext = new CustomerEditViewModel();
}
Dans les systèmes du monde réel, d'autres approches sont souvent utilisées pour créer le modèle de vue, tel que l'injection de dépendances ou les frameworks MVVM.
Commandes dans MVVM
Les commandes sont utilisées pour gérer les Events
dans WPF tout en respectant le modèle MVVM.
Un EventHandler
normal ressemblerait à ceci (situé dans Code-Behind
):
public MainWindow()
{
_dataGrid.CollectionChanged += DataGrid_CollectionChanged;
}
private void DataGrid_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do what ever
}
Non, pour faire la même chose dans MVVM, nous utilisons des Commands
:
<Button Command="{Binding Path=CmdStartExecution}" Content="Start" />
Je recommande d'utiliser une sorte de préfixe (
Cmd
) pour vos propriétés de commande, car vous en aurez principalement besoin dans xaml - de cette façon, elles seront plus faciles à reconnaître.
Comme il s'agit de MVVM, vous voulez gérer cette commande (pour le Button
"eq" Button_Click
) dans votre ViewModel
.
Pour cela, nous avons essentiellement besoin de deux choses:
- System.Windows.Input.ICommand
- RelayCommand (par exemple pris ici .
Un exemple simple pourrait ressembler à ceci:
private RelayCommand _commandStart;
public ICommand CmdStartExecution
{
get
{
if(_commandStart == null)
{
_commandStart = new RelayCommand(param => Start(), param => CanStart());
}
return _commandStart;
}
}
public void Start()
{
//Do what ever
}
public bool CanStart()
{
return (DateTime.Now.DayOfWeek == DayOfWeek.Monday); //Can only click that button on mondays.
}
Alors qu'est-ce que cela fait en détail:
ICommand
est ce à quoi le Control
dans xaml est lié. RelayCommand
achemine votre commande vers une Action
(c'est-à-dire, appelle une Method
). Le Null-Check garantit simplement que chaque Command
ne sera initialisée qu'une fois (en raison de problèmes de performance). Si vous avez lu le lien ci-dessus pour RelayCommand
vous avez peut-être remarqué que RelayCommand
a deux surcharges pour son constructeur. (Action<object> execute)
et (Action<object> execute, Predicate<object> canExecute)
.
Cela signifie que vous pouvez (en plus) ajouter une seconde Method
renvoyant un bool
pour indiquer que le Control
peut être déclenché ou non.
Une bonne chose à cela est que Button
s par exemple sera Enabled="false"
si la Method
retournera false
Paramètres de commande
<DataGrid x:Name="TicketsDataGrid">
<DataGrid.InputBindings>
<MouseBinding Gesture="LeftDoubleClick"
Command="{Binding CmdTicketClick}"
CommandParameter="{Binding ElementName=TicketsDataGrid,
Path=SelectedItem}" />
</DataGrid.InputBindings>
<DataGrid />
Dans cet exemple, je souhaite transmettre DataGrid.SelectedItem
à Click_Command dans mon ViewModel.
Votre méthode devrait ressembler à ceci alors que l'implémentation ICommand reste elle-même comme ci-dessus.
private RelayCommand _commandTicketClick;
public ICommand CmdTicketClick
{
get
{
if(_commandTicketClick == null)
{
_commandTicketClick = new RelayCommand(param => HandleUserClick(param));
}
return _commandTicketClick;
}
}
private void HandleUserClick(object item)
{
MyModelClass selectedItem = item as MyModelClass;
if (selectedItem != null)
{
//Do sth. with that item
}
}