Blog ACENSI
MVVM : Model View Viewmodel

Introduction au MVVM

MVVM est un design pattern dont le but et de séparer distinctement les 3 couches d’une application (la donnée, l’intelligence et la présentation). Son nom signifie Model-View-ViewModel, en référence à ses 3 composants :

  • Le « Model» contient la donnée à manipuler
  • La « View» (ou Vue) est sa représentation à l’écran
  • Le « ViewModel» manipule la donnée et la prépare pour la View. La différence avec les autres patterns de ce style est que le ViewModel assure le relais des données avec du Data Binding

Le Data Binding
Le Data Binding, est la mise en place de liaisons entre des données du « ViewModel » et leur affichage dans la vue.
Voici un exemple rapide en C# de son implémentation et du résultat. Afin de garder cet exemple au plus simple, je vais volontairement faire un exemple sans Modèle dans un premier temps, avec juste une Vue et un « ViewModel ». Nous introduirons un Modèle simple dans un second temps.
Voici la structure du projet :
Dossier Solution 'MVVMApp'

App.xaml :

<Application x:Class="MVVMApp.App"
	xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	xmlns:local="clr-namespace:MVVMApp"
	StartupUri="View.xaml">
<Application.Resources>

View.xaml :

<Window x:Class="MVVMApp.View"
	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:MVVMApp"
	mc:Ignorable="d"
	Title="Titre"
	Height="350" Width="525">
	<Grid Name="SimpleGrid">
	<TextBox &nbsp;Text="{Binding Path=TextProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="100,100,100 ,100" />
	<TextBlock Text="{Binding Path=TextProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Height="100" Width="200" Margin="0,0,0,200" />
	</Grid>
</Window>

View.xaml.cs :

using System.Windows;
namespace MVVMApp
{
	/// <summary>
	/// Logique d'interaction pour MainWindow.xaml
	/// </summary>
	public partial class View : Window
	{
		public View()
		{
			InitializeComponent();
			SimpleGrid.DataContext = new MainViewModel();
		}
	}
}

MainViewModel.cs :

using System.ComponentModel;
namespace MVVMApp
{
	public class MainViewModel : INotifyPropertyChanged
	{
		public event PropertyChangedEventHandler PropertyChanged;
		private string _textProperty;
		public string TextProperty
		{
			get
			{
				return _textProperty;
			}
			set
			{
				_textProperty = value;
				NotifyPropertyChanged("TextProperty");
			}
		}
		private void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
		public MainViewModel()
		{
			TextProperty = "Exemple";
		}
	}
}

Commençons par regarder la Vue et son Code Behind et penchons-nous sur l’instruction suivante :

SimpleGrid.DataContext = new MainViewModel();

Elle signifie que le la Grid SimpleGrid utilisera les fonctionnalités du ViewModel MainViewModel. Il existe plusieurs façons de faire ce même résultat mais j’ai choisi celle-ci car elle me semblait la plus simple en terme de nouvelles notions.
Si vous reproduisez ce code, vous pourrez constater que le contenu du Textbox et du Textblock, la valeur affichée dans la Vue donc, est mappé sur la propriété du ViewModel, c’est-à-dire que les changements de la propriété dans le ViewModels vont être directement reportés dans la Vue et/ou inversement selon le sens du binding (il est ici configuré en TwoWay dans les deux cas mais nous reviendrons sur ce point un peu plus tard). Cependant, pour que ce comportement fonctionne, il est nécessaire que le ViewModel implémente l’interface INotifyPropertyChanged et lance un évènement PropertyChanged à chaque modification de sa valeur.
Ici, nous voyons 2 choses importantes dans la mise en place du pattern MVVM. D’une part, l’implémentation de l’event PropertyChanged et de la méthode l’appelant, NotifyPropertyChanged, et d’autre part que la propriété TextProperty appelle cette méthode lors d’un changement de valeur. De cette façon, le ViewModel notifie la Vue qu’une modification a eu lieu pour que celle-ci rafraichisse son affichage.
Revenons maintenant aux sens de bindings dont je parlais plus tôt. Il existe 4 types de modes pour que les changements de données soient pris en compte :

  • OneWay: Les modifications de la propriété mettront à jour la valeur dans Vue
  • OneWayToSource : Les modifications de la valeur dans la Vue mettront à jour la propriété
  • TwoWay: Une combinaison de OneWay et OneWayToSource. La valeur affichée dans la vue et la propriété se mettront à jour mutuellement en parallèle
  • OneTime: Comme OneWay mais la valeur de la Vue ne sera mise à jour qu’une seule fois

L’ajout d’un « Model »
Un Model, comme nous le disions en introduction, contient la donnée à manipuler. Il s’agit donc de classes simples uniquement destinées à stocker les données. Nous allons donc créer un Modèle de Personne comme suivant :

namespace MVVMApp
{
	public class Personne
	{
		public string Nom { get; set; }
		public string Prenom { get; set; }
		public Personne()
		{
			this.Nom = "Sans Nom";
			this.Prenom = "Sans Prenom";
		}
	}
}

Nous allons continuer en mettant une référence du Modèle dans le ViewModel comme suivant :

using System.ComponentModel;
namespace MVVMApp
{
	public class MainViewModel : INotifyPropertyChanged
	{
		public event PropertyChangedEventHandler PropertyChanged;
		private Personne _personne;
		public Personne Personne
		{
			get
			{
				return _personne;
			}
			set
			{
				_personne = value;
				NotifyPropertyChanged("Personne");
			}
		}
		private void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
		public MainViewModel()
		{
			Personne = new Personne();
		}
	}
}

Enfin, nous allons modifier la Vue pour prendre en compte ces modifications et refléter les données du Modèle.

<Window x:Class="MVVMApp.View"
	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:MVVMApp"
	mc:Ignorable="d"
	Title="Titre"
	Height="350" Width="525">
	<Grid Name="SimpleGrid">
		<TextBox  Text="{Binding Path=Personne.Prenom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,95,195,189" />
		<TextBox  Text="{Binding Path=Personne.Nom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,135,195,149" />
		<TextBlock Text="{Binding Path=Personne.Prenom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Width="200" Margin="10,10,307,265" />
		<TextBlock Text="{Binding Path=Personne.Nom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Width="200" Margin="307,10,10,265" />
	</Grid>
</Window>

Notons ici que le Code Behind de la vue ne subit aucune modification.

Le cas particulier des Observablecollection
Les listes sont un cas particulier puisqu’il ne suffit pas que celles-ci implémentent l’interface INotifyPropertyChanged pour refléter leurs modifications dans la Vue mais aussi INotifyCollectionChanged. Heureusement, il existe une classe de liste présentant déjà cette fonctionnalité, la classe ObservableCollection<T>.  Cette classe a en effet l‘avantage de lancer une notification lors de la modification, de l’ajout ou de la suppression de l’un de ses éléments afin d’assurer un bon fonctionnement avec le Design Pattern MVVM.

Les Commandes
Enfin, il existe une autre fonctionnalité qui permet au MVVM de centraliser l’intelligence dans le ViewModel, les Commandes. Elles permettent d’appeler des méthodes du ViewModels depuis la Vue et offrent aussi la possibilité de paramétrer une fonction disant si oui ou non cette méthode peut être appelée. Elles doivent implémenter l’interface ICommand qui a 3 méthodes :

  • CanExecuteChanged
  • CanExecute
  • Execute

Nous allons donc créer une classe implémentant cette interface et nous l’appellerons ici RelayCommand.
Voici un exemple montrant l’implémentation d’une commande permettant l’ajout et la suppression d’une Personne à la liste. Le Code Behind de la Vue ou le code du Modèle reste inchangé par rapport à l’exemple précédent.

Code de la classe RelayCommand :

using System;
using System.Windows.Input;
namespace MVVMApp
{
	public class RelayCommand : ICommand
	{
		public event EventHandler CanExecuteChanged
		{
			add { CommandManager.RequerySuggested += value; }
			remove { CommandManager.RequerySuggested -= value; }
		}
		private Action methodToExecute;
		private Func<bool> canExecuteEvaluator;
		public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
		{
			this.methodToExecute = methodToExecute;
			this.canExecuteEvaluator = canExecuteEvaluator;
		}
		public RelayCommand(Action methodToExecute)
		: this(methodToExecute, null)
		{
		}
		public bool CanExecute(object parameter)
		{
			if (this.canExecuteEvaluator == null)
			{
				return true;
			}
			else
			{
				bool result = this.canExecuteEvaluator.Invoke();
				return result;
			}
		}
		public void Execute(object parameter)
		{
		this.methodToExecute.Invoke();
		}
	}
}

Code de la Vue :

<Window x:Class="MVVMApp.View"
	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:MVVMApp"
	mc:Ignorable="d"
	Title="Titre"
	Height="350" Width="525">
	<Grid Name="SimpleGrid">
		<TextBox  Text="{Binding Path=Personne.Prenom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,95,195,189" />
		<TextBox  Text="{Binding Path=Personne.Nom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,135,195,149" />
		<ListView ItemsSource="{Binding ListPersonne}" Margin="349,35,10,0" SelectedItem="{Binding Personne}">
		<ListView.View>
		<GridView AllowsColumnReorder="true">
		<GridViewColumn DisplayMemberBinding="{Binding Path=Prenom}" Header="Prenom" Width="70"/>
		<GridViewColumn DisplayMemberBinding="{Binding Path=Nom}" Width="70" Header="Nom" />
		</GridView>
		</ListView.View>
		</ListView>
		<Button Command="{Binding AjoutCommand}" Margin="10,196,403,82" >Ajouter</Button>
		<Button Command="{Binding SupprimerCommand}" Margin="158,196,255,82" >Supprimer</Button>
	</Grid>
</Window>

Code du ViewModel :

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
namespace MVVMApp
{
	public class MainViewModel : INotifyPropertyChanged
	{
		public event PropertyChangedEventHandler PropertyChanged;
		private Personne _personne;
		public Personne Personne
		{
			get
			{
				return _personne;
			}
			set
			{
				_personne = value;
				NotifyPropertyChanged("Personne");
			}
		}
		private ObservableCollection<Personne> _listPersonne;
		public ObservableCollection<Personne> ListPersonne
		{
			get
			{
				return _listPersonne;
			}
			set
			{
				_listPersonne = value;
				NotifyPropertyChanged("ListPersonne");
			}
		}
		private void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
		public ICommand AjoutCommand
		{
			get;
			private set;
		}
		public ICommand SupprimerCommand
		{
			get;
			private set;
		}
		private void AjouterPersonne()
		{
			ListPersonne.Add(new Personne());
		}
		private void SupprimerPersonne()
		{
			ListPersonne.Remove(Personne);
			Personne = null;
		}
		private bool CanSupprimerPersonne()
		{
			return Personne != null;
		}
		public MainViewModel()
		{
			Personne = new Personne();
			ListPersonne = new ObservableCollection<Personne>();
			ListPersonne.Add(Personne);
			AjoutCommand = new RelayCommand(()=> AjouterPersonne());
			SupprimerCommand = new RelayCommand(() => SupprimerPersonne(),()=> CanSupprimerPersonne());
		}
	}
}

Dans cet exemple, nous pouvons voir que la Vue appelle directement les méthodes du ViewModel et que nous avons mis en place une commande d’ajout pouvant être appelée tout le temps et une méthode de suppression qui nécessite qu’un élément de la liste soit sélectionné. Si vous exécutez ce code, vous pourrez en effet voir que le bouton devient invalide dès que la liste est vide.

Plusieurs ViewModels dans une seule View
Comme nous l’avons vu, le pattern MVVM permet d’abstraire complètement l’affichage d’une donnée et sa manipulation. Il est donc envisageable, dans certains cas, de faire en sorte qu’une même vue autorise plusieurs types de manipulations différentes et appelle donc plusieurs ViewModels.
Nous allons donc modifier une dernière fois la Vue pour faire cohabiter le ViewModel actuel et le premier ViewModel ensemble.
Voici donc le code du nouveau ViewModel, reprenant le premier que nous avons créé :

using System.ComponentModel;
namespace MVVMApp
{
	public class SecondViewModel : INotifyPropertyChanged
	{
		public event PropertyChangedEventHandler PropertyChanged;
		private string _textProperty;
		public string TextProperty
		{
			get
			{
				return _textProperty;
			}
			set
			{
				_textProperty = value;
				NotifyPropertyChanged("TextProperty");
			}
		}
		private void NotifyPropertyChanged(string propertyName)
		{
			if (PropertyChanged != null)
			{
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}
		public SecondViewModel()
		{
			TextProperty = "Exemple";
		}
	}
}

Le code de la nouvelle vue. Nous pouvons remarquer que nous avons maintenant 3 grids, 1 pour chaque ViewModel et 1 principale pour les englober toutes les deux.

<Window x:Class="MVVMApp.View"
	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:MVVMApp"
	mc:Ignorable="d"
	Title="Titre"
	Height="700" Width="525">
	<Grid Name="GlobalGrid" >
		<Grid Name="SimpleGrid" Margin="0 0 0 350">
			<TextBox  Text="{Binding Path=Personne.Prenom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,95,195,189" />
			<TextBox  Text="{Binding Path=Personne.Nom, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="10,135,195,149" />
			<ListView ItemsSource="{Binding ListPersonne}" Margin="349,35,10,0" SelectedItem="{Binding Personne}">
			<ListView.View>
			<GridView AllowsColumnReorder="true">
			<GridViewColumn DisplayMemberBinding="{Binding Path=Prenom}" Header="Prenom" Width="70"/>
			<GridViewColumn DisplayMemberBinding="{Binding Path=Nom}" Width="70" Header="Nom" />
			</GridView>
			</ListView.View>
			</ListView>
			<Button Command="{Binding AjoutCommand}" Margin="10,196,403,82" >Ajouter</Button>
			<Button Command="{Binding SupprimerCommand}" Margin="158,196,255,82" >Supprimer</Button>
			</Grid>
			<Grid Name="SecondGrid" Margin="0 350 0 0">
			<TextBox  Text="{Binding Path=TextProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Margin="100,100,100 ,100" />
			<TextBlock Text="{Binding Path=TextProperty, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" Height="100" Width="200" Margin="0,0,0,200" />
		</Grid>
	</Grid>
</Window>

Et enfin son Code Behind dans lequel nous affectons le nouveau ViewModel à la SecondGrid :

using System.Windows;
namespace MVVMApp
{
	/// <summary>
	/// Logique d'interaction pour MainWindow.xaml
	/// </summary>
	public partial class View : Window
	{
		public View()
		{
			InitializeComponent();
			SimpleGrid.DataContext = new MainViewModel();
			SecondGrid.DataContext = new SecondViewModel();
		}
	}
}

Avec ces exemples, nous avons donc fait un tour général permettant d’avoir une première approche pratique du pattern MVVM et de ses principales fonctionnalités et contraintes. J’aimerai cependant finir en précisant que le but de cet article n’est donc pas d’être totalement exhaustif et qu’il existe plusieurs implémentations possibles pour obtenir un résultat équivalent. Enfin, ce pattern étant maintenant assez répandu, il existe aussi de nombreux frameworks spécialisés dans le MVVM.
Nous aurons peut-être l’occasion d’aller plus en profondeur sur ce sujet dans un prochain article !

Pourquoi ce blog ?

Pour permettre à nos consultants et experts techniques de partager leurs connaissances et retours d’expérience autour des sujets qui les passionnent. Ce blog, intégralement écrit par eux, a pour vocation d’être un véritable lieu d’échanges et d’apprentissage.

Alors n’hésitez pas à commenter nos articles pour rejoindre la conversation !

Une suggestion ?

Si vous avez des idées pour améliorer ce blog, nous sommes à l’écoute de vos remarques. Vous pouvez nous écrire via le formulaire de contact qui se trouve en bas de page.

Bonne visite !