Dans un précédent article, nous vous présentions le patron de conception MVVM : https://www.arkance-systems.fr/pattern-mvvm/
Nous allons aujourd’hui nous pencher sur son implémentation au sein d’un projet d’exemple en WPF sous .NET Core 3.1.
Pour rappel, le design pattern MVVM est composé de trois éléments d’architecture logicielle :
La force de ce patron de conception réside dans sa forte maintenabilité, liée principalement au fait que ni la vue, ni le “model”, n’a connaissance de l’autre : il y a donc une séparation entre la logique et l’affichage.
La première étape consiste bien évidemment à créer un nouveau projet d’Application WPF sous .NET Core 3.1 :
Une fois le projet créé, nous allons le structurer en ajoutant les dossiers Models, Views et ViewModels.
Nous allons tout d’abord commencer par créer un modèle dans le dossier correspondant, qui représentera par exemple un utilisateur avec son nom, prénom et adresse de courriel.
namespace WpfMVVMApp.Models
{
public class User
{
#region Properties
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
#endregion Properties
#region Constructors
public User()
{
}
public User(string firstName, string lastName, string email)
{
FirstName = firstName;
LastName = lastName;
Email = email;
}
#endregion Constructors
}
}
Une fois le modèle créé, nous allons pouvoir passer à l’implémentation de l’un des éléments principaux du design pattern, le modèle de vue. Il existe plusieurs façons plus ou moins complexes d’implémenter ce patron de conception MVVM, mais quasiment toutes, reposent sur l’utilisation d’au moins deux mécanismes permettant de gérer les données et l’interaction entre le view et le viewModel : le DataBinding et le RelayCommand.
using System.ComponentModel;
namespace WpfMVVMApp.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Vous pourrez voir dans d’autres exemples ou frameworks d’autres noms tels que OnPropertyChanged ou bien encore RaisePropertyChanged, mais le comportement reste le même et a pour but d’informer que la valeur de la propriété a été modifiée.
using System;
using System.Windows.Input;
namespace WpfMVVMApp.Utilities
{
public class RelayCommand : ICommand
{
#region Fields
private readonly Action<object> _execute;
private readonly Predicate<object> _canExecute;
#endregion Fields
#region Constructor
public RelayCommand(Action<object> execute, Predicate<object> canExecute = null)
{
if (execute == null) throw new ArgumentNullException(“execute”);
_execute = execute;
_canExecute = canExecute;
}
#endregion Constructor
#region Public methods
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public void Execute(object parameter)
{
_execute(parameter);
}
#endregion Public methods
}
}
Passons maintenant à la création de notre viewModel qui sera associée à notre vue principale. Mettons que nous souhaitions créer une vue simple, qui se composera de la liste des d’utilisateurs enregistrés et d’un bouton permettant d’afficher une boite de dialogue avec les informations de l’utilisateur sélectionné.
Pour cela, il nous faudra donc dans notre viewModel :
Ainsi, nous allons tout d’abord créer une nouvelle classe intitulée MainViewModel qui implémentera notre BaseViewModel vu précédemment (et donc par la même occasion INotifyPropertyChanged).
namespace WpfMVVMApp.ViewModels
{
public class MainViewModel : BaseViewModel
{
}
}
Puis, nous allons créer deux champs privés et autant de propriétés pour garder en mémoire et manipuler la liste des utilisateurs ainsi que celui sélectionné. Notez l’utilisation de la méthode NotifyPropertyChanged lorsque l’une des propriétés est valorisée, qui nous permettra d’actualiser automatiquement la vue dans le cas de la liste des utilisateurs et inversement pour l’utilisateur sélectionné, au travers du DataBinding spécifié plus loin dans la création de la vue. Nous pouvons également ajouter la ICommand qui sera associée au clic du bouton.
#region Fields
private User _selectedUser;
private ObservableCollection<User> _users;
#endregion Fields
#region Properties
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
NotifyPropertyChanged(“SelectedUser”);
}
}
public ObservableCollection<User> Users
{
get { return _users; }
set
{
_users = value;
NotifyPropertyChanged(“Users”);
}
}
public ICommand DisplayUserCommand { get; }
#endregion Properties
Ensuite nous allons créer le constructeur, qui nous servira à instancier notre liste d’utilisateurs et d’associer notre commande à une méthode. Pour ce projet d’exemple, j’ai volontairement décidé de ne pas surcharger les explications avec la gestion d’une base de données ou l’appel d’une API tierce. Ainsi, afin d’avoir quelques exemples de données à manipuler, j’ai décidé d’initialiser la collection d’utilisateurs directement dans une méthode InitializeUsers().
#region Constructor
public MainViewModel()
{
_users = new ObservableCollection<User>();
InitializeUsers();
DisplayUserCommand = new RelayCommand(o => DisplayUser());
}
#endregion Constructor
#region Private methods
private void InitializeUsers()
{
Users.Add(new User(“John”, “Doe”, “john.doe@company.com”));
Users.Add(new User(“Alicia”, “Davis”, “alicia.davis@company.com”));
Users.Add(new User(“Mike”, “Jones”, “mike.jones@company.com”));
Users.Add(new User(“Justine”, “Anderson”, “justine.anderson@company.com”));
}
private void DisplayUser()
{
if (SelectedUser is null)
MessageBox.Show(“Please select a user before.”);
else
MessageBox.Show($”The selected user is {SelectedUser.FirstName} {SelectedUser.LastName}.”);
}
#endregion Private methods
Ici la méthode DisplayUser() associée à la commande à exécuter suite au clic du bouton consiste simplement à vérifier s’il y a bien un utilisateur sélectionné et dans ce cas, d’afficher une boîte de dialogue avec le nom et prénom de l’utilisateur sélectionné.
En assemblant le tout, cela nous donne l’ensemble du code ci-dessous :
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Input;
using WpfMVVMApp.Models;
using WpfMVVMApp.Utilities;
namespace WpfMVVMApp.ViewModels
{
public class MainViewModel : BaseViewModel
{
#region Fields
private User _selectedUser;
private ObservableCollection<User> _users;
#endregion Fields
#region Properties
public User SelectedUser
{
get { return _selectedUser; }
set
{
_selectedUser = value;
NotifyPropertyChanged(“SelectedUser”);
}
}
public ObservableCollection<User> Users
{
get { return _users; }
set
{
_users = value;
NotifyPropertyChanged(“Users”);
}
}
public ICommand DisplayUserCommand { get; }
#endregion Properties
#region Constructor
public MainViewModel()
{
_users = new ObservableCollection<User>();
InitializeUsers();
DisplayUserCommand = new RelayCommand(o => DisplayUser());
}
#endregion Constructor
#region Private methods
private void InitializeUsers()
{
Users.Add(new User(“John”, “Doe”, “john.doe@company.com”));
Users.Add(new User(“Alicia”, “Davis”, “alicia.davis@company.com”));
Users.Add(new User(“Mike”, “Jones”, “mike.jones@company.com”));
Users.Add(new User(“Justine”, “Anderson”, “justine.anderson@company.com”));
}
private void DisplayUser()
{
if (SelectedUser is null)
MessageBox.Show(“Please select a user before.”);
else
MessageBox.Show($”The selected user is {SelectedUser.FirstName} {SelectedUser.LastName}.”);
}
#endregion Private methods
}
}
Passons maintenant à l’aspect visuel et interactif de notre application. Lors de la création du projet, une vue de base a été générée, nous pouvons la supprimer puis recréer une nouvelle fenêtre intitulée MainView, localisée dans le dossier Views bien évidemment.
Dans cette vue, nous devons spécifier un certain nombre de points afin qu’elle puisse interagir avec notre MainViewModel.
Tout d’abord nous allons rajouter un DataContext pointant vers notre ViewModel.
< Window x:Class=”WpfMVVMApp.Views.MainView”
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:WpfMVVMApp.Views”
xmlns:viewmodel=”clr-namespace:WpfMVVMApp.ViewModels”
mc:Ignorable=“d”
Title=”MainView” Height=”450″ Width=”800″>
<Window.DataContext>
<viewmodel:MainViewModel x:Name=“MainViewModel” />
</Window.DataContext>
</Window>
Ensuite nous allons simplement ajouter à notre fenêtre un composant Grid composé de deux lignes, qui contiendront respectivement une ListView pour l’affichage/ sélection de nos utilisateurs et notre bouton permettant d’afficher l’utilisateur sélectionné.
Pour spécifier qu’il existe une liaison de données entre notre liste d’utilisateurs, leurs propriétés, l’élément de la liste sélectionné et notre commande, nous utilisons le mot clé Binding, suivi du nom de la propriété donnée dans notre MainViewModel.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=“*” />
<RowDefinition Height=”*” />
</Grid.RowDefinitions>
<ListView Grid.Row=”0″ Margin=”5″ ItemsSource=”{Binding Users}” SelectedItem=”{Binding SelectedUser}”>
<ListView.View>
<GridView>
<GridViewColumn Header=”First name” DisplayMemberBinding=”{Binding FirstName}” />
<GridViewColumn Header=”Last name” DisplayMemberBinding=”{Binding LastName}” />
<GridViewColumn Header=”Email” DisplayMemberBinding=”{Binding Email}” />
</GridView>
</ListView.View>
</ListView>
<Button Grid.Row=”1″ Width=”150″ VerticalAlignment=”Center” Content=”Display selected user” Command=”{Binding DisplayUserCommand}” />
</Grid>
Une fois notre view implémentée, attention à ne pas oublier de remplacer la valeur de la propriété StartupUri du fichier App.xaml par le nom de la nouvelle vue, sinon l’application ne pourra pas trouver la vue par défaut à charger au lancement.
<Application x:Class=”WpfMVVMApp.App”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:local=”clr-namespace:WpfMVVMApp”
StartupUri=”Views/MainView.xaml”>
<Application.Resources>
</Application.Resources>
</Application>
Si tout est correctement implémenté, au lancement de l’application, vous devriez voir apparaître vos données.
Après sélection d’un utilisateur dans la liste, si vous cliquez sur le bouton, un message contenant les informations de l’utilisateur s’affiche.
Nous avons vu l’une des nombreuses façons d’implémenter le design pattern MVVM dans une application WPF sous .NET Core 3.1, il s’agit là d’une des plus minimaliste et simple à aborder.
De plus, nous pourrions enrichir cet exemple en proposant à l’utilisateur de saisir les informations d’un nouvel utilisateur et l’ajouter à la liste, ou bien de supprimer celui sélectionné au travers d’un autre bouton. Enfin, de nombreux frameworks existent, tels que MVVM Toolkit, MVVM Foundation, Light MVVM ou encore Prism, pour ne citer que quelques-uns des plus connus. Ceux-ci sont parfois bien plus complexes et proposent des fonctionnalités étendues