WPF MVVM : Les bases
WPF MVVM : Les bases
Richard Clark
(Vu 4048)
Français C# ASP .NET Confirmé Article

Série d'articles sur l'approche MVVM pour WPF/Silverlight

Il y a deux ans de cela, j'ai passé presque une année complète sur le développement d'une application WPF. L'année qui a suivi a elle été consacrée à une application Windows Forms "classique".

Cela n'a l'air de rien mais WPF : ce n'est pas comme le vélo! Et il m'a fallu un certain temps pour m'y remettre ;-) J'en ai donc profité pour regarder un petit peu ce qu'il se faisait deci-dela à propos de WPF.

L'un des grandes tendances il me semble est l'adoption petit à petit de l'approche MVVM (Model-View-ViewModel). Vous pouvez lire un excellent article de Josh Smith, MVP .NET, sur MSDN sur l'approche MVVM.

Microsoft, via Codeplex, propose plusieurs alternatives pour nous aider à développer "à la MVVM".

La première, la plus simple, voire simpliste, est le WPF Model-View-ViewModel Toolkit. Il comprend un template pour Visual Studio, un exemple complet d'application et une documentation sur ce qu'est l'approche MVVM. A l'heure ou j'écris ces lignes (28 Juillet 2009), ce toolkit est vraiment trop simpliste et son seul intérêt est la documentation livrée avec.

La seconde est la plus complexe : il s'agit de Prism ou "patterns & practices Composite Application Guidance for WPF and Silverlight site". Elle repose notamment sur les MS Applications Blocks. Autant le toolkit est simpliste, autant Prism peut se révéler très rapidement une usine à gaz si l'on n'a pas une approche très stricte. (a noter que la documentation de Prism est disponible sur MSDN).

Pour une vraie grosse application WPF (Silverlight), Prism est à mon avis une bonne solution, mais cela demande une rigueur de la part de l'équipe de dev très importante (il ne faut pas ques les différents architectes essayent d'imposer chacun leurs points de vue sans consultations préalables et surtout accord).

J'étais donc dans la situation ou pour le développement de ma nouvelle application, soit je me lançais corps et âme avec Prism (avec l'énorme avantage que j'étais seul ;-)), soit je me réinventais une version light de Prism car le toolkit est largement insuffisant.

Et là, mon sauveur est arrivé : j'ai nommé Laurent Kempé, co-auteur de http://www.techheadbrothers.com . Bon je rigole parce que c'est lui qui m'a contacté à la recherche désespérée des locations de vacances mais toujours est-il qu'on en est arrivé à parler de cette approche MVVM. Je recherchais plus particulièrement une implémentation de l'eventaggregator de Prism. Il m'indiqua alors un autre Laurent, Laurent Bugnion, suisse de son état (personne n'est parfait), MVP .NET (mais si il est parfait ;-)), et auteur d'un petit framework MVVM pour WPF et Silverlight light très intéressant.

MVVM Light Toolkit

Vous pouvez le télécharger ici (c'est une installation Click Once) et suivre les instructions d'installation ici ou télécharger les sources là. (NB : je vous conseille tout de même l'installation ClickOnce).

Contrairement aux frameworks de Microsoft, tout est fait pour faciliter l'utilisation de ce petit framework : des modèles sont proposés et intégrés dans Visual Studio, des Code Snippets sont ajoutés pour faciliter le code, etc. Bref, grâce à ce toolkit, vous pourrez entrer dans le monde MVVM avec aisance et naturel. (NB : juste un reproche, j'aime pas trop l'exemple d'application proposé qui ajoute du code dans la vue, mais nous verrons cela plus tard).

Que contient alors ce toolkit ?

Tout d'abord une dll (une version WPF et une version Silverlight 3 sont disponibles) nommée GalaSoft.MvvmLight.dll. Dans cette assembly, nous avons :

  • Une classe RelayCommand et son équivalent générique RelayCommand<T>,
  • Un event aggregator Messenger (singleton) qui enregistre les évènements et assure leurs diffusions (+ des classes décrivant les messages diffusés),
  • Une classe ViewModelBase pour nos ViewModel.

Dans une approche MVVM, nous avons une vue, View et un objet ViewModel associé à cette vue. En WPF, cela signifie que l'on a un UserControl (ou une Windows) qui a comme DataContext une instance d'un objet (son ViewModel). On va prototyper notre ViewModel pour que dans notre UserControl, on n'ai que du Binding simple à mettre en oeuvre : on a un texte a afficher ? On ajoute alors une propriété Text à notre classe ViewModel. On a une collection d'objet ? On ajoute une collection comme propriété à notre ViewModel.

Si vous avez déjà un framework qui expose vos objets (genre NHibernate, RIA, CodeFluent), pour chaque vue que vous souhaitez, vous allez créer un objet ViewModel qui encapsule les données de vos objets du framework. Nous verrons plus loin concrètement ce que cela signifie, mais il faut avant tout présenter quelques concepts présents et utilisés dans ce mini toolkit.

View et ViewModel

Prenons l'exemple d'une vue qui affiche les données d'un objet Personne. Notre objet Personne a les propriétés Nom et Prenom :

 public class Personne : INotifyPropertyChanged
{
    private string _Nom;
    private string _Prenom;

    public string Prenom
	{
		get { return _Prenom; }
		set
		{
			_Prenom = value;
			OnPropertyChanged("Prenom");
		}
	}

	public string Nom
	{    
		get { return _Nom; }
		set
		{
			_Nom = value;
			OnPropertyChanged("Nom");
		}
	}

	#region INotifyPropertyChanged Members
	public event PropertyChangedEventHandler PropertyChanged;
	protected void OnPropertyChanged(string propertyName)
	{
		if (PropertyChanged != null)
			PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
	}
	#endregion
}

Rien de bien particulier. Maintenant, dans notre vue, nous voulons afficher et éditer notre objet personne. Ajoutons un simple UserControl a notre projet avec le code suivant :

<UserControl x:Class="WpfApplication1.PersonneModule.PersonneView"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:vm="clr-namespace:WpfApplication1"
     >

     <Grid>
          <Grid.RowDefinitions>
               <RowDefinition Height="Auto" />
               <RowDefinition Height="*" />
          </Grid.RowDefinitions>
          <Grid.ColumnDefinitions>
               <ColumnDefinition Width="Auto" />
               <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>

          <TextBlock Text="Nom" Grid.Row="0" VerticalAlignment="Center"/>
          <TextBlock Text="Prénom" Grid.Row="1" VerticalAlignment="Center"/>

          <TextBox Text="{Binding Personne.Nom}" Grid.Row="0" Grid.Column="1" VerticalAlignment="Center"/>
          <TextBox Text="{Binding Personne.Prenom}" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center"/>
     </Grid>
</UserControl>

Remarquons qu'au niveau du binding des deux TextBox, on se binde aux propriétés d'une Personne. Il faut donc que le DataContext de notre UserControl possède une propriété Personne de type Personne. Nous allons donc créer un objet, ViewModel, possédant une propriété Personne. Avec le toolkit, nous allons créer une classe héritant de ViewModelBase. Cette classe implémente INotifyPropertyChanged et possède une propriété IsInDesignMode indiquant si l'on est en design ou en exécution :

 public class PersonneViewModel : ViewModelBase
{
     public PersonneViewModel()
     {
          if (IsInDesignMode)
          {
               this.Personne = new Personne() { Nom = "Clark", Prenom = "Richard" };
          }
     }

     #region Personne property
     public const string PersonnePropertyName = "Personne";

     private Personne _personne = null;

     public Personne Personne
     {
          get
          {
               return this._personne;
          }

          private set
          {
               if (this._personne == value)
               {
                    return;
               }

               this._personne = value;

               RaisePropertyChanged(PersonnePropertyName);
          }
     }
     #endregion
}	 

Plusieurs remarques :

  • RaisePropertyChanged déclenche l'évènement PropertyChanged (INotifyPropertyChanged) indiquant que la propriété Personne a changée.
  • Dans le constructeur, si on est en mode design, on affecte la propriété Personne. Ainsi, dans Visual Studio, dans Blend, on voit le résultat à l'écran sans avoir besoin de lancer l'application. C'est l'un des gros point fort de ce framework.

Vue en design

Maintenant, il ne nous reste plus qu'à faire le lien entre la vue, notre UserControl PersonneView, et notre ViewModel, la classe PersonneViewModel.

Il y a plusieurs solutions à cela. Laurent Bugnion utilise une classe qu'il nomme Locator qui indique à l'application ou sont les ViewModel. UNe instance de cette classe est initialisée au lancement de l'application en la plaçant dans les ressources globales, App.xaml :

<Application x:Class="WpfApplication1.App"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:vm="clr-namespace:WpfApplication1"
     StartupUri="Window1.xaml">
     
     <Application.Resources>
          <vm:Locator x:Key="Locator" />
     </Application.Resources>
</Application>

Et le code dans cette classe, pour chaque ViewModel, ressemble à cela :

 #region PersonneViewModel property
private static PersonneViewModel _personneViewModel;

public PersonneViewModel PersonneViewModel
{
     get{return PersonneViewModelStatic;}
}

public static PersonneViewModel PersonneViewModelStatic
{
     get
     {
          if (_personneViewModel == null)
          {
               CreatePersonneViewModel();
          }

          return _personneViewModel;
     }
}

public static void CreatePersonneViewModel()
{
     _personneViewModel = new PersonneViewModel();
}

public static void ClearPersonneViewModel()
{
     if (_personneViewModel != null)
     {
          _personneViewModel.Dispose();
          _personneViewModel = null;
     }
}
#endregion

Donc pour résumer :

  • Comme on a placer dans App.xaml une instance de notre classe Locator, on peut l'utiliser partout,
  • Cette classe Locator possède une propriété PersonneViewModel (d'instance et Static pour des problèmes de compatibilité WPF et Silverlight).
  • On peut donc dans notre View, affecter son DataContext à la propriété PersonneViewModel de notre Locator.

CQFD :

<UserControl x:Class="WpfApplication1.PersonneModule.PersonneView"
     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     xmlns:vm="clr-namespace:WpfApplication1"
     DataContext="{Binding Source={x:Static vm:Locator.PersonneViewModelStatic}}">

Conclusion

Nous avons réussi (quel exploit!) à créer notre première View associée à notre premier ViewModel. Nous verrons dans un prochain article l'utilisation des RelayCommand.

Vous pouvez télécharger l'exemple ici.

Richard Clark
Richard Clark

Vos commentaires
Richard Clark
Richard Clark a dit le 30/07/09 à 10:11
Je comprends ton point de vue (et je l'approuve en partie).
On peut faire des choses extrèmement difficiles en 3s et buter sur un problème simple pendant des heures. En bref, y'a un gros pb de design de WPF (merci qui ? merci Chris Anderson et Don Box!!!). A l'initiative de Scotte Guthrie des simplifications arrivent (Visual State Manager par exemple) mais le chemin est encore long pour arriver à la simplicité et flexibilité des Windows Forms.
DotNET74
DotNET74 a dit le 29/07/09 à 18:21
Hello,

Je suis fan de .NET et j'ai voulu passer sur WPF et là un gros choque !

Trop lourd, trop compliqué. On était arrivé à quelque chose de simple et rapide avec les Winforms:

- je claque un composant sur une winform.
- je le dipose et le dimensionne très simplement.
- j'ai accès à tout les évènnements simplement.

Et là arrive le WPF qui outre son aspect graphique (et encore) remet tout à la poubelle.

on place un composant et là on cherche comment le dimensionnement ou le positionnement fonctionne. on tire sur les poignées t le composants bouge dans tout les sens.

bref trop lourd à mettre en oeuvre !!

quand je vois l'exemple ci-dessus, je trouve cela bien compliqué pour changer de zones de texte !!!

Mais il doit y avoir quelque chose qui m'échappe :)

Vous devez être identifié pour pouvoir commenter l'article.

Identification - S'inscrire