Implémenter le design pattern MVVM en C#


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.

Préambule

Pour rappel, le design pattern MVVM est composé de trois éléments d’architecture logicielle :

  • Model, représentant les données de la logique métier (issues de bases de données par exemple).
  • View, qui est la partie visible pour l’utilisateur (affichage de ces données, interactions avec l’utilisateur).
  • ViewModel, permettant de faire le lien entre les Views et Models (chargement des données du modèle dans la view, modification des données du modèle suite à une action dans la view, etc).

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.

Création du projet

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.

Model

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

    }

}

ViewModel

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.

  • DataBinding
    Le principe du DataBinding est de créer un lien des données entre la vue et le modèle de vue. De cette façon, les données modifiées exposées dans le modèle de vue seront mises à jour sur la vue et inversement. Pour notifier qu’une valeur d’une propriété a été changée et par la suite mettre à jour la vue par exemple, nous allons implémenter l’interface INotifyPropertyChanged.
    Une première bonne pratique est de créer une classe BaseViewModel, qui implémentera l’interface et qui pourra être hérité par nos autres ViewModels dans l’optique d’évolutions de l’application. Cela nous évitera ainsi de devoir implémenter à nouveau INotifyPropertyChanged pour chaque modèle de vue et de plus, d’alléger leur contenu. Nous pouvons placer cette classe dans notre dossier ViewModels, qui contiendra également le reste des potentiels autres modèles de vue.


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.

  • RelayCommand
    Puisque le design pattern MVVM sépare le contrôle des données de la vue, nous ne devons pas utiliser de « code behind » dans la view, par exemple pour gérer les événements de clics. A la place, nous pouvons utiliser la propriété Command de l’élément Button et nous servir d’un relai de la commande, créé en implémentant l’interface ICommand. J’ai décidé de créer un dossier Utilities et d’y placer notre RelayCommand.


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 :

  • La liste actuelle des utilisateurs.
  • L’utilisateur sélectionné.
  • Gérer le comportement au clic du bouton.

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

    }

}

View

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>

Test de l’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.


Conclusion

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