Le design pattern MVVM12 mars 2010

Ce design pattern est devenu à la mode avec l’arrivée du WPF. Il se décompose en trois parties intéragissant les une avec les autres. M-V-VM ou MVVM signifie Model-View-ViewModel.
Model représente la couche d’accès aux données. Il est possible qu’elle soit elle même segmentée en fonction de l’architecture de votre projet.
View représente l’interface graphique utilisateur et uniquement cela.
ViewModel fait le lien entre Model et View en définissant les propriétés de Binding pour View ainsi que ses commandes associées (clique sur un bouton, sélection d’un élément d’une liste, etc…).
Vous pouvez télécharger la solution ici : Solution MVVM 1.0 (192)
Vue d’ensemble
Dans le dossier Model, j’ai placé deux classes, Result et SearchManager. Result représente un objet simple représentant la donnée. SearchManager est le fournisseur de données qui propose une méthode Search permettant de récupérer une liste de Result.
GenericCommand est une implémentation générique de ICommand, que l’on va utiliser pour prendre en charge les commandes des différents contrôles.
MainWindow.xaml (ici il n’y a pas de code-behind MainWindow.xaml.cs) est l’interface graphique utilisateur. Mais il est possible de vouloir interférer avec des évènements particuliers sur certains contrôles, dans ce cas il faudra replacer un fichier MainWindow.xaml.cs . L’interface graphique peut être créée de toute pièce par un designer sans aucune connaissance des langages .NET avec le logiciel Blend par exemple.
MainWindowViewModel.cs contient les propriétés et commandes nécessaires à l’interface pour fonctionner. Ces dernières sont utilisées via le Binding.
App.xaml (ici il n’y a pas besoin de code-behind, il n’y a donc pas de App.xaml.cs) contient la liste des ViewModel dans ses ressources.
Fichier App.xaml:
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MVVM.ViewModels"
StartupUri="Views\MainWindow.xaml">
<Application.Resources>
<vm:MainWindowViewModel x:Key="MainWindowVM" />
</Application.Resources>
</Application>
View
Intéressons-nous à l’élément View. En voici un exemple avec le fichier MainWindow.xaml de ce projet de démonstration :
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" DataContext="{StaticResource MainWindowVM}">
<StackPanel>
<Label Target="{Binding ElementName=SearchTextBox}">Rechercher le texte :</Label>
<TextBox Name="SearchTextBox" Text="{Binding Path=SearchText, Mode=TwoWay}" />
<Button Command="{Binding SearchTextCommand}">Lancer la recherche</Button>
<ListBox ItemsSource="{Binding Path=Results}" Height="50"
SelectedValue="{Binding Path=SelectedResult, Mode=TwoWay}" />
<Label>Résultat sélectionné :</Label>
<TextBlock Name="SelectedResultLabel" Text="{Binding Path=SelectedResult}" />
</StackPanel>
</Window>
On remarque ici dans les premières lignes l’absence de l’attribut x:Class référant à la partie code-behind de la fenêtre. En effet, comme je l’ai dis plus haut, l’intégralité de l’interface utilisateur est définie ici, son esthétique comme son comportement.
Second point, le DataContext de la fenêtre est ici « Bindé » à une ressource définie dans App.xaml, cette ressource est le ViewModel de cette View.
Le comportement est géré par les commandes. Ici SearchTextCommand (propriété de MainWindowViewModel que nous verrons plus loin) prends en charge l’action du clique sur le bouton « Lancer la recherche« .
Model
Le modèle de données est une architecture à lui tout seul. Pour ce projet de démonstration, il y a peu de code et peu d’objets, mais vos projets peuvent être plus complexes et nécessiter d’externaliser cette couche dans un ou plusieurs autres projets.
Fichier Result.cs:
class Result
{
public string Name { get; set; }
public double Total { get; set; }
public override string ToString()
{
return string.Format("{0} => {1}", Name, Total);
}
}
Fichier SearchManager.cs:
class SearchManager
{
public static List<Result> Search(string searchText)
{
List<Result> results = new List<Result>();
results.Add(new Result { Name = "Greg", Total = 45.50 });
results.Add(new Result { Name = "Toto", Total = 79.90 });
results.Add(new Result { Name = "Neo", Total = 23.50 });
return results;
}
}
ViewModel
Le ViewModel est certainement la partie de ce modèle MVVM la plus intéressante. En effet, c’est ici que vous allez écrire le plus de code (DependencyProperty pour le binding, handlers d’event et accès au Model).
Fichier MainWindowViewModel.cs:
class MainWindowViewModel : DependencyObject
{
private GenericCommand searchTextCommand;
public ICommand SearchTextCommand
{
get { return searchTextCommand; }
}
#region SearchText
public string SearchText
{
get { return (string)GetValue(SearchTextProperty); }
set { SetValue(SearchTextProperty, value); }
}
public static readonly DependencyProperty SearchTextProperty =
DependencyProperty.Register("SearchText", typeof(string), typeof(MainWindowViewModel), new UIPropertyMetadata(null));
#endregion
...
public MainWindowViewModel()
{
searchTextCommand = new GenericCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => Search()
};
}
private void Search()
{
Results = SearchManager.Search(SearchText);
}
}
La propriété SearchText s’écrirait de cette manière si nous avions créer un ViewModel implémentant INotifyPropertyChanged:
// Evènement lié à l'interface INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private string _searchText;
public string SearchText
{
get { return _searchText; }
set
{
if (_searchText != value)
{
_searchText = value;
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("SearchText"));
}
}
}
Le ViewModel doit dériver de DependencyObject ou bien implémenter l’interface INotifyPropertyChanged afin de pouvoir être appelé comme je l’ai fais dans le App.xaml
Les membres de type DependencyProperty occupent la majorité de l’espace dans ce fichier, il convient alors de les regrouper dans des régions portant le nom de la propriété exposée afin de gagner en lisibilité. Une autre astuce est de séparé le ViewModel en deux fichiers (utilisation de partial class), un contenant les propriétés (données) et un autre pour les commandes.
La gestion des commandes peut sembler un peu complexe de prime abord, mais se révèle très intéressante lorsqu’il s’agit de gérer des comportements de contrôles en fonction de l’état courant de l’application. Par exemple, ici on pourrait très bien remplacer la ligne CanExecuteDelegate = x => true par un appel à une fonction renseignant sur la possibilité ou non de lancer la recherche, ceci afin d’éviter de lancer plusieurs recherches en même temps par exemple.
J’ai écrit ici une classe GenericCommand afin de vous permettre de réutiliser le code de cette commande générique dans le cas de commandes embarquant peu de code. Cela permet d’éviter d’avoir plein de classes qui se baladent avec quasiement la même chose dedans.
Avantages et inconvénients de ce design pattern
Dans le cas de petits projets (on va dire moins de 5 UI différentes), ce pattern c’est pas vraiment utile, le projet reste lisible (en principe).
Là ou le pattern MVVM va vraiment apporté une plus-value est dans des projets contenant de nombreuses interfaces utilisateur. Souvent, on voit une UI (Window, Page, UserControl) avec un présenteur de données et un fichier de code-behind pour les events. En appliquant MVVM sur un tel projet, non seulement vous réduisez le nombre de fichiers à charger par Visual Studio et le compilateur (il n’y a que très rarement besoin d’un fichier de code-behind), mais vous gagnez également en lisibilité dans l’explorateur de solution en n’étant plus obligé de switcher entre code-behind(events) et présenteurs(données).
L’inconvérient majeur du MVVM est dans sa mise en place. Il requiert que vous déclariez les ViewModel en tant que ressources (soit dans l’App.xaml ce que je recommande, soit dans les différentes UI propriété Resources). Ensuite, vous devez définir le DataContext de chaque UI pointant sur le ViewModel correspondant. Enfin, certaines commandes sont inexistantes dans certains contrôles, et vous devrez les créér vous-même (par exemple, pas de commande sur ListBox pour l’event SelectionChanged).
Pingback: Raccourcis clavier sur un Menu | DotnetNews.fr
Pingback: Tutoriel : Consommer le service de données OGDI avec Windows Phone 7/Mango – 1ère partie - OGDI (Open Government Data Initiative) France - Site Home - MSDN Blogs
Pingback: Implémentation d’une ICommand de façon générique | DotnetNews.fr