Le Patron de Conception MVVM

Introduction

Si vous avez déjà apprécié le concept de découplage et de testabilité apporté par l’Injection de Dépendances (DI) dans votre code, vous devez absolument appliquer cette philosophie à la couche de présentation de votre application WPF. C’est là qu’intervient le patron Model-View-ViewModel (MVVM), une architecture spécialement conçue pour les frameworks basés sur le Data Binding (WPF, UWP, Xamarin, etc.).

MVVM est un patron de conception qui permet d’atteindre une Séparation des Préoccupations (SoC) quasi parfaite entre l’interface utilisateur (UI) et la logique métier.

Contrairement à MVC, qui sépare l’interface utilisateur de la logique métier en centralisant les actions dans le Controller mais ne découple pas la View du Controller, MVVM rend la View passive (dans le sens où elle ne contient aucune logique métier) et la synchronise automatiquement avec le ViewModel via le data-binding.

Le Problème Historique : le “Code-Behind”

Traditionnellement, les applications UI (comme celles basées sur Windows Forms) souffraient d’un couplage fort, où la logique applicative (gestion des données, validation, etc.) se retrouvait imbriquée dans les fichiers événementiels de l’interface (Button_Click, etc.). C’est le piège du Code-Behind, qui rend le code :

  1. Difficile à Tester : Tester la logique nécessite l’instanciation de l’UI, ce qui est lourd.
  2. Rigide : Une modification de l’UI (ex: changer un bouton en lien hypertexte) nécessite de modifier la logique.

MVVM résout ce problème en introduisant une couche intermédiaire totalement testable et indépendante de l’UI.

Les Trois Piliers du MVVM

MVVM définit trois composants distincts, chacun avec un rôle unique et clair. L’absence de dépendance directe entre la View et le ViewModel est la clé du découplage.

  1. Model (Modèle)

    Il représente la couche de données et la logique métier (Business Logic) de l’application.

    • Rôle : Fournir les données, exécuter les règles métier, interagir avec les bases de données ou les services web.
    • Dépendances : Aucune (il ne connaît ni la View ni le ViewModel).
  2. View (Vue)

    C’est l’interface utilisateur construite en XAML.

    • Rôle : Définir l’apparence, la structure et les événements d’interaction utilisateur.
    • Dépendances : Elle dépend du ViewModel via son DataContext. Elle est responsable de lier (binding) ses contrôles aux propriétés et commandes du ViewModel. Elle ne contient aucune logique métier.
  3. ViewModel (Modèle-Vue)

    C’est le médiateur et le cœur du pattern. Il est la source de données de la View et contient la logique de présentation.

    • Rôle : Exposer les données du Model dans un format adapté à la View et gérer les commandes (actions) de l’utilisateur.
    • Dépendances : Il dépend du Model (souvent via une interface injectée) mais est totalement indépendant de la View.
graph TD
    A[View] -.-> B(ViewModel)
    B --> C[Model]
    C --> B
    B -.-> A

    subgraph User Interface
        A
    end

    subgraph Business Logic / Data
        B
        C
    end


    D("View interagit avec ViewModel (via Commands/Events)")
    E("ViewModel expose des données et les au View (via Data Binding)")
    F("ViewModel accède et met à jour le Model")
    G("Model notifie ViewModel des changements (via Observers/Callbacks)")

    D --> A
    E --> B
    F --> B
    G --> C

La Dynamique du Découplage

Le secret de MVVM réside dans la manière dont la View et le ViewModel communiquent sans se connaître mutuellement, grâce à deux mécanismes centraux de WPF : le Data Binding et l’interface ICommand.

1. Le Data Binding (INotifyPropertyChanged)

La View lie ses propriétés aux propriétés du ViewModel. Pour que les changements du ViewModel se reflètent dans la View, le ViewModel doit implémenter l’interface INotifyPropertyChanged.

  • Lorsqu’une propriété du ViewModel est modifiée, elle lève l’événement PropertyChanged.
  • Le moteur de Data Binding de WPF capte cet événement et met automatiquement à jour l’élément lié dans la View.

2. Le Système de Commandes (ICommand)

La View utilise le pattern ICommand pour transmettre les actions (clic de bouton, etc.) au ViewModel, sans avoir à créer de gestionnaires d’événements dans le Code-Behind.

  • Un élément d’interface (ex: Button) est lié à une propriété de type ICommand du ViewModel.
  • Lorsque l’utilisateur interagit, la méthode Execute de la commande dans le ViewModel est appelée.
graph TD
    A[View]
    B(ViewModel)

    subgraph Communication Découplée WPF
        DB[Data Binding & INotifyPropertyChanged]
        IC[Système ICommand]
    end

    B -- Expose les données --> DB
    DB -- Met à jour --> A
    
    A -- Envoie l'action --> IC
    IC -- Exécute la logique --> B

Structure et Relations Clés

Voici comment les composants s’organisent en termes de structure de classes :

classDiagram
    direction RL
    class ICommand {
        <<interface>>
        +bool CanExecute(object)
        +void Execute(object)
    }

    class INotifyPropertyChanged {
        <<interface>>
        +event PropertyChangedEventHandler PropertyChanged
    }

    class Model {
        -List<Data> businessData
        +void GetData()
        +void SaveData()
    }

    class ViewModel {
        -Model _model
        +Property DataAffichable : Type
        +ICommand ActionCommand
        +ViewModel(Model model)
    }

    class View {
        <<WPF Window/UserControl>>
        ~DataContext : ViewModel
    }

    View --> ViewModel : Context de Donnees (DataContext)
    ViewModel --> Model : Dependance
    ViewModel ..> INotifyPropertyChanged : Implémente
    ViewModel ..> ICommand : Contient les Commandes

Exemple complet

namespace MVVMExample
{
    // Modèle
    public class Utilisateur
    {
        public string Nom { get; set; }
        public int Age { get; set; }
    }

    // ViewModel
    public class UtilisateurViewModel : INotifyPropertyChanged
    {
        private Utilisateur _utilisateur;
        public Utilisateur Utilisateur
        {
            get => _utilisateur;
            set { _utilisateur = value; OnPropertyChanged(nameof(Utilisateur)); }
        }

        public DelegateCommand RafraichirCommand { get; }

        public UtilisateurViewModel()
        {
            Utilisateur = new Utilisateur { Nom = "Alice", Age = 25 };
            RafraichirCommand = new DelegateCommand(Rafraichir);
        }

        private void Rafraichir()
        {
            Utilisateur = new Utilisateur { Nom = "Bob", Age = 30 };
        }

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyName) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    // Vue
    public partial class UtilisateurView : Window
    {
        public UtilisateurView()
        {
            InitializeComponent();
            DataContext = new UtilisateurViewModel();
        }
    }
}
<!-- XAML UtilisateurView.xaml -->
<Window x:Class="MVVMExample.UtilisateurView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Utilisateur" Height="250" Width="300">
    <StackPanel Margin="20">
        <TextBlock Text="Nom:"/>
        <TextBox Text="{Binding Utilisateur.Nom, UpdateSourceTrigger=PropertyChanged}"/>
        <TextBlock Text="Age:" Margin="0,10,0,0"/>
        <TextBox Text="{Binding Utilisateur.Age, UpdateSourceTrigger=PropertyChanged}"/>
        <Button Content="Rafraichir" Command="{Binding RafraichirCommand}" Margin="0,20,0,0"/>
    </StackPanel>
</Window>
classDiagram
    class Utilisateur {
        +string Nom
        +int Age
    }

    class UtilisateurViewModel {
        -Utilisateur _utilisateur
        +Utilisateur Utilisateur
        +DelegateCommand RafraichirCommand
        +UtilisateurViewModel()
        -void Rafraichir()
        +event PropertyChangedEventHandler PropertyChanged
        -void OnPropertyChanged(string propertyName)
    }

    class UtilisateurView {
        +UtilisateurView()
        -void InitializeComponent()
        -UtilisateurViewModel DataContext
    }

    UtilisateurViewModel --> Utilisateur
    UtilisateurView --> UtilisateurViewModel

Découplage de la View et de son View-ViewModel

Pour que le découplage théorique du MVVM fonctionne dans la pratique, il est essentiel d’utiliser un Conteneur d’Injection de Dépendances (DI). Pour découplé la View et le ViewModel.

Plutôt que la View n’instancie son ViewModel directement :

  • DataContext = new MonViewModel()

Ce qui créerait un couplage fort et empêcherait la View de recevoir un ViewModel mocké pour les tests. On utilise le Conteneur DI qui orchestre cette association.

Le frameworks Prism facilitent cela grâce au ViewModelLocator. Ce mécanisme utilise une convention de nommage.

Exemple

La ligne suivante dans UtilisateurView.xaml demande à Prism d’effectuer cette opération :

<Window ...
             xmlns:prism="http://prismlibrary.com/"
             prism:ViewModelLocator.AutoWireViewModel="true">
</Window>

Le ViewModelLocator demande alors au Conteneur DI d’instancier UtilisateurViewModel dans le DataContext de UtilisateurView. Le conteneur injecte les dépendances nécessaires (les Models et Services) dans le constructeur du ViewModel avant de l’assigner au DataContext de la View.

Conclusion

L’effort initial d’adoption de MVVM est rapidement amorti par les bénéfices structurels qu’il procure :

  • Testabilité : Le ViewModel étant une simple classe C# sans aucune référence à l’UI, il peut être testé unitairement facilement et rapidement.
  • Facilité de Maintenance : Les modifications de la logique n’affectent pas l’UI, et vice-versa. Cela réduit le risque de régression lors des mises à jour.
  • Collaboration Efficace : Les designers peuvent travailler sur le XAML (la View) et les développeurs sur la logique (le ViewModel) en parallèle, en utilisant des Design Data pour simuler les données du ViewModel.
  • Réutilisation : Un ViewModel bien conçu peut être réutilisé par plusieurs Views (ex: une View ListeUtilisateurs et une View CarteUtilisateurs peuvent partager une grande partie du même ViewModel).

Inconvénients à considérer :

  • MVVM peut devenir trop verbeux pour des interfaces simples, avec beaucoup de ViewModels et de bindings inutiles qui alourdissent le projet.
  • Les bindings complexes ou imbriqués peuvent générer des effets de bord difficiles à suivre et des problèmes de performance.
  • L’absence de conventions strictes dans de grands projets peut entraîner une duplication de code ou un couplage involontaire entre ViewModels.
  • Le pattern n’est pas forcément adapté aux applications très dynamiques ou minimalistes où l’overhead d’un ViewModel complet ne se justifie pas.

Ainsi, bien que puissant, MVVM nécessite un usage réfléchi et des pratiques disciplinées pour rester efficaces et maintenables, tout en évitant les pièges liés à la complexité excessive.