• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 29/04/2014

MVVM

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Passons maintenant à MVVM…
Si vous êtes débutants en XAML, je ne vous cache pas que ce chapitre risque d'être difficile à appréhender. Il s'agit de concepts avancés qu'il n'est pas nécessaire de maitriser immédiatement. Au contraire, d'une manière générale, il faut déjà pas mal de pratique avant de pouvoir utiliser les concepts présentés dans ce chapitre.
Mais n'hésitez pas à le lire quand même et à y revenir plus tard, cela vous sera toujours utile.

Très à la mode, MVVM est un patron de conception (design pattern en anglais) qui s’est construit au fur et à mesure que les développeurs créaient des applications utilisant le XAML.
MVVM signifie Model-View-ViewModel, nous allons détailler son principe et son fonctionnement dans ce chapitre.

Principe du patron de conception

La première chose à savoir est qu’est-ce qu’un patron de conception ? Très connu sous son appellation anglaise, « design pattern », un patron de conception constitue une solution éprouvée et reconnue comme une bonne pratique à un problème récurrent dans la conception d’applications informatiques. En général, il décrit une modélisation de classes utilisées pour résoudre un problème précis. Il existe beaucoup de patrons de conceptions, comme le populaire MVC (Modèle-Vue-Contrôleur), très utilisé dans la réalisation de site webs.

Ici, le patron de conception MVVM est particulièrement adapté à la réalisation d’applications utilisant le XAML, comme les applications pour Windows Phone, mais également les applications Silverlight, Windows 8 ou WPF. Il permet d’architecturer efficacement une application afin d’en faciliter la maintenabilité et la testabilité.
Certains auront tendance à considérer MVVM comme un ensemble de bonnes pratiques plutôt qu’un vrai patron de conception. Ce qui est important, c’est qu’en le comprenant vous allez améliorer votre productivité dans la réalisation d’applications conséquentes. Voyons à présent de quoi il s’agit.

MVVM signifie Model-View-ViewModel.

  • Model, en français « le modèle », correspond aux données. Il s’agit en général de plusieurs classes qui permettent d’accéder aux données, comme une classe Client, une classe Commande, etc. Peu importe la façon dont on remplit ces données (base de données, service web,…), c’est ce modèle qui est manipulé pour accéder aux données.

  • View, en français « la vue », correspond à tout ce qui sera affiché, comme la page, les boutons, etc. En pratique, il s’agit du fichier .xaml.

  • View Model, que l’on peut traduire en « modèle de vue », c’est la colle entre le modèle et la vue. Il s’agit d’une classe qui fournit une abstraction de la vue. Ce modèle de vue, que j’appellerai désormais view-model, s’appuie sur la puissance du binding pour mettre à disposition de la vue les données du modèle. Il s’occupe également de gérer les commandes que nous verrons un peu plus loin.

Voici à la figure suivante un schéma représentant ce patron de conception.

Le patron de conception MVVM
Le patron de conception MVVM

Le but de MVVM est de faire en sorte que la vue n’effectue aucun traitement, elle ne doit faire qu’afficher les données présentées par le view-model. C’est le view-model qui a en charge de faire les traitements et d’accéder au modèle.

Et si on se tentait une petite métaphore pour essayer de comprendre un peu mieux ?
Essayons de représenter MVVM à travers un jeu, disons une machine à sous d'un casino.

  • Mon modèle correspondra aux différentes valeurs internes des images de la machine à sous, dont le fameux 7 qui fait gagner le gros lot.

  • Ma vue correspondra à la carcasse de la boite à sous et surtout aux images qui s’affichent. Il s’agira de tout ce que l’on voit.

  • Mon view-model correspondra aux engrenages qui relient les images à la machine à sous et qui transforment une valeur interne en image affichable sur la machine à sous.

Sans ces engrenages, mes images ne peuvent pas s'accrocher au cadre de la machine à sous et tout se casse la figure, on ne voit rien sur la vue.
Lorsque ces engrenages sont présents, on peut voir les données liées à la vue (grâce au binding). Et je peux agir sur mon modèle par l'intermédiaire de commandes, en l'occurrence le levier de la machine à sous.
Je tire sur le levier, une commande du view-model est activée, les images tournent, le modèle se met à jour (les valeurs internes ont changées) et la vue est mise à jour automatiquement. Je peux voir que les trois sept sont alignés. JACKPOT.

Plus compréhensible ? N’hésitez pas à me proposer d’autres métaphores en commentaires pour expliquer MVVM.

Première mise en place de MVVM

Nous avons commencé à pratiquer un peu MVVM au chapitre précédent, en fournissant un contexte dans une classe séparée. Ce contexte est le view-model, il prépare les données afin qu’elles soient affichables par la vue. Si on veut être un peu plus précis et utiliser un langage plus proche des patrons de conception, on pourrait dire que le view-model « adapte » le modèle pour la vue.
Commençons par créer un nouveau projet pour mettre en place une version simplifiée de MVVM. Comme l’idée est de séparer les responsabilités, nous allons en profiter pour créer des répertoires pour notre modèle, nos vues et nos view-models.
Créons donc les répertoires et les fichiers suivants :

  • Model

    • Client.cs

    • ServiceClient.cs

  • View

    • VoirClientView.xaml

  • ViewModel

    • VoirClientViewModel.cs

Et profitons-en pour supprimer le fichier MainPage.xaml, de manière à avoir la même architecture que sur la figure suivante.

Architecture de la solution MVVM
Architecture de la solution MVVM

Par convention, vous aurez compris que le modèle se place dans le répertoire Model, que les vues se placent dans le répertoire View et que les view-models se placent dans le répertoire ViewModel. De même, on suffixera les vues par « View » et les view-models par « ViewModel ».
En l’état, notre application ne pourra pas démarrer ainsi, car notre application va essayer de démarrer en naviguant sur le fichier MainPage.xaml, que nous avons supprimé. Nous devons donc lui indiquer un nouveau point d’entrée. Cela se fait depuis le fichier WMAppManifest.xml qui est sous le répertoire Properties. Ouvrez-le et modifiez la Page de navigation, comme nous l’avons déjà fait, pour y mettre : View/VoirClientView.xaml.

Cela nous permet de dire que l’application doit démarrer en affichant la page VoirClientView.xaml.
Commençons par créer un modèle ultra simple, qui consiste en une classe client qui contient un prénom, un âge et un booléen indiquant s’il est un bon client :

public class Client
{
    public string Prenom { get; set; }
    public int Age { get; set; }
    public bool EstBonClient { get; set; }
}

Ainsi qu’un service qui va nous simuler le chargement d’un client :

public class ServiceClient
{
    public Client Charger()
    {
        return new Client { Prenom = "Nico", Age = 30, EstBonClient = true };
    }
}

Maintenant, réalisons la vue. Nous allons simplement afficher le prénom et l’âge du client. Ceux-ci seront sur un fond vert si le client est un bon client et en rouge si c’est un mauvais client. Quelque chose comme ça :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="PageTitle" Text="Fiche client" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Background="{Binding BonClient}">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock Text="Prénom : " />
        <TextBlock Grid.Column="1" Text="{Binding Prenom}" />
        <TextBlock Grid.Row="1" Text="Age : " />
        <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Age}" />
    </Grid>
</Grid>

On utilise les expressions de balisage pour indiquer les valeurs grâce au binding. Sauf qu’il est difficile de se rendre compte ainsi si la vue est bien construite, car il nous manque les valeurs de design. Qu’à cela ne tienne, nous savons désormais comment créer un contexte de design. Créons un nouveau répertoire sous le répertoire ViewModel que nous appelons Design et une nouvelle classe s’appelant DesignVoirClientViewModel.cs qui contiendra les valeurs de design suivantes :

public class DesignVoirClientViewModel
{
    public string Prenom
    {
        get { return "Nico"; }
    }

    public int Age
    {
        get { return 30; }
    }

    public SolidColorBrush BonClient
    {
        get { return new SolidColorBrush(Color.FromArgb(100, 0, 255, 0)); }
    }
}

Pour rappel, SolidColorBrush se trouve dans l’espace de nom System.Windows.Media.

Il faut ensuite lier le contexte de design au view-model de design :

<d:DesignProperties.DataContext>
    <design:DesignVoirClientViewModel />
</d:DesignProperties.DataContext>

sans oublier d’importer l’espace de nom correspondant :

xmlns:design="clr-namespace:DemoMvvm.ViewModel.Design"

Ainsi, nous pourrons avoir en mode design le résultat affiché à la figure suivante.

Affichage des données en mode design grâce à MVVM
Affichage des données en mode design grâce à MVVM

Ce qui est le résultat attendu. Chouette ! :)
Passons enfin au view-model. Nous avons vu qu’il devait implémenter l’interface INotifyPropertyChanged :

public class VoirClientViewModel : INotifyPropertyChanged
{
    private string prenom;
    public string Prenom
    {
        get { return prenom; }
        set { NotifyPropertyChanged(ref prenom, value); }
    }


    private int age;
    public int Age
    {
        get { return age; }
        set { NotifyPropertyChanged(ref age, value); }
    }

    private SolidColorBrush bonClient;
    public SolidColorBrush BonClient
    {
        get { return bonClient; }
        set { NotifyPropertyChanged(ref bonClient, value); }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }
}

Rien de sorcier, nous définissons également les propriétés Prenom, Age et BonClient. Reste à charger notre modèle depuis notre view-model et à affecter les propriétés du view-model à partir des valeurs du modèle :

public void ChargeClient()
{
    ServiceClient service = new ServiceClient();
    Client client = service.Charger();

    Prenom = client.Prenom;
    Age = client.Age;
    if (client.EstBonClient)
        BonClient = new SolidColorBrush(Color.FromArgb(100, 0, 255, 0));
    else
        BonClient = new SolidColorBrush(Color.FromArgb(100, 255, 0, 0));
}

Il nous manque une dernière chose, hautement indispensable, qui est de lier la vue au view-model. Pour l’instant, nous avons vu que nous pouvions le faire depuis le code-behind de la vue, avec :

public partial class VoirClientView : PhoneApplicationPage
{
    public VoirClientView()
    {
        InitializeComponent();
        DataContext = new VoirClientViewModel();
    }
}

Il y a une autre solution qui évite de passer par le code, en utilisant le XAML :

<phone:PhoneApplicationPage.DataContext>
    <viewmodel:VoirClientViewModel />
</phone:PhoneApplicationPage.DataContext>

Ce qui revient au même, vu qu’on positionne la propriété DataContext de la page à une instance du view-model. C’est cette solution que nous allons privilégier ici.
Vous n’aurez bien sûr pas oublié d’inclure l’espace de nom qui va bien :

xmlns:viewmodel="clr-namespace:DemoMvvm.ViewModel"

Revenons à présent un peu sur ce que nous avons fait. Nous avons créé une vue, la page XAML, liée à l’exécution au view-model VoirClientViewModel et liée en design au pseudo view-model DesignVoirClientViewModel. Le pseudo view-model de design expose des données en dur, pour nous permettre d’avoir des données dans le designer alors que le view-model utilise et transforme le model pour exposer les mêmes données.
Une bonne pratique ici serait de définir une interface avec les données à exposer et que nos deux view-models l’implémentent. Créons donc un répertoire Interface dans le répertoire ViewModel et créons l’interface IVoirClientViewModel :

public interface IVoirClientViewModel
{
    string Prenom { get; set; }
    int Age { get; set; }
    SolidColorBrush BonClient { get; set; }
}

Nos deux view-models doivent implémenter cette interface :

public class VoirClientViewModel : INotifyPropertyChanged, IVoirClientViewModel
{
    …
}

Et :

public class DesignVoirClientViewModel : IVoirClientViewModel
{
    public string Prenom
    {
        get { return "Nico"; }
        set { }
    }

    public int Age
    {
        get { return 30; }
        set { }
    }

    public SolidColorBrush BonClient
    {
        get { return new SolidColorBrush(Color.FromArgb(100, 0, 255, 0)); }
        set { }
    }
}

Vous aurez remarqué que nous avons rajouté le mutateur set dans le view-model de design, et qu’elle n’a besoin de rien faire.

Nous pouvons encore faire une petite amélioration. Ici, elle est mineure car nous n’avons qu’un seul view-model, mais elle sera intéressante dès que nous en aurons plusieurs. En effet, chaque view-model doit implémenter l’interface INotifyPropertyChanged et avoir le code suivant :

public event PropertyChangedEventHandler PropertyChanged;

public void NotifyPropertyChanged(string nomPropriete)
{
    if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
}

private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
{
    if (object.Equals(variable, valeur)) return false;

    variable = valeur;
    NotifyPropertyChanged(nomPropriete);
    return true;
}

Nous pouvons factoriser ce code dans une classe de base dont vont dériver tous les view-models. Créons pour cela un répertoire FrameworkMvvm et dedans, mettons-y la classe ViewModelBase :

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void NotifyPropertyChanged(string nomPropriete)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(nomPropriete));
    }

    public bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        NotifyPropertyChanged(nomPropriete);
        return true;
    }
}

N’oubliez pas de passer la méthode NotifyPropertyChanged en public.
Il ne reste plus qu’à supprimer ce code du view-model et de le faire hériter de cette classe de base :

public class VoirClientViewModel : ViewModelBase, IVoirClientViewModel
{
    …
}

Les commandes

Bon, c’est très bien tout ça, mais nous n’avons fait qu’une partie du chemin… MVVM ce n’est pas que du binding avec une séparation entre la vue et les données. Il faut être capable de faire des actions. Imaginons que nous souhaitions charger les données du client suite à un appui sur un bouton. Avant MVVM, nous aurions utilisé un événement sur le clic du bouton, puis nous aurions chargé les données dans le code-behind et nous les aurions affichées sur notre page.
Avec notre découpage, le view-model n’est pas au courant d’une action sur l’interface, car c’est un fichier à part. Il n’est donc pas directement possible de réaliser une action dans le view-model lors d’un clic sur le bouton.
On peut résoudre ce problème d’une première façon très simple mais pas tout à fait parfaite. Créons tout d’abord un bouton dans notre XAML :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Background="{Binding BonClient}">
    <Grid.RowDefinitions>
        <RowDefinition Height="auto" />
        <RowDefinition Height="auto" />
        <RowDefinition Height="auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <TextBlock Text="Prénom : " />
    <TextBlock Grid.Column="1" Text="{Binding Prenom}" />
    <TextBlock Grid.Row="1" Text="Age : " />
    <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Age}" />
    <Button Grid.Row="2" Grid.ColumnSpan="2" Content="Charger client" Tap="Button_Tap" />
</Grid>

Voyons à présent comment résoudre simplement ce problème. Il suffit d’utiliser l’événement Tap et de faire quelque chose comme ceci dans le code-behind de la page, dans la méthode associée à l’événement de clic :

public partial class VoirClientView : PhoneApplicationPage
{
    public VoirClientView()
    {
        InitializeComponent();
    }

    private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        ((IVoirClientViewModel)DataContext).ChargeClient();
    }
}

On utilise la propriété DataContext pour récupérer l’instance du view-model. En effet, n’oubliez pas que nous avons lié la vue au view-model via la propriété DataContext. Cela implique de rajouter une méthode ChargeClient() dans l’interface et de la définir dans notre view-model ainsi que dans celui de design :

public interface IVoirClientViewModel
{
    string Prenom { get; set; }
    int Age { get; set; }
    SolidColorBrush BonClient { get; set; }
    void ChargeClient();
}

public class DesignVoirClientViewModel : IVoirClientViewModel
{
    public string Prenom
    {
        get { return "Nico"; }
        set { }
    }

    public int Age
    {
        get { return 30; }
        set { }
    }

    public SolidColorBrush BonClient
    {
        get { return new SolidColorBrush(Color.FromArgb(100, 0, 255, 0)); }
        set { }
    }

    public void ChargeClient()
    {
    }
}

public class VoirClientViewModel : ViewModelBase, IVoirClientViewModel
{
    […code supprimé pour plus de clarté…]

    public VoirClientViewModel()
    {
    }

    public void ChargeClient()
    {
        ServiceClient service = new ServiceClient();
        Client client = service.Charger();

        Prenom = client.Prenom;
        Age = client.Age;
        if (client.EstBonClient)
            BonClient = new SolidColorBrush(Color.FromArgb(100, 0, 255, 0));
        else
            BonClient = new SolidColorBrush(Color.FromArgb(100, 255, 0, 0));
    }
}

Vous pouvez tester cette solution, cela fonctionne. Cependant, elle est imparfaite car cela crée un couplage entre la vue et le view-model, c’est-à-dire que la vue connait l’instance du view-model car c’est effectivement elle qui l’instancie avec la déclaration que nous avons vue dans le XAML. Mais bien qu’imparfaite, n’écartez pas non plus complètement cette solution de votre esprit, elle reste bien pratique et ne rajoute pas tant de code que ça dans la vue (de plus, ce code ne fait pas de traitement, il fonctionne juste comme un relai du traitement).

Pour résoudre plus proprement ce problème, il y a une autre solution : les commandes.
Les commandes correspondent à des actions faites sur la vue. Le XAML dispose d’un mécanisme léger de gestion de commandes via l’interface ICommand. Par exemple, le contrôle bouton possède (par héritage) une propriété Command du type ICommand permettant d’invoquer une commande lorsque le bouton est appuyé.
Ainsi, il sera possible de remplacer :

<Button Grid.Row="2" Grid.ColumnSpan="2" Content="Charger client" Tap="Button_Tap" />

par :

<Button Grid.Row="2" Grid.ColumnSpan="2" Content="Charger client" Command="{Binding ChargerClientCommand}" />

Ici, nous avons enlevé l’événement Tap et la méthode associée (vous pouvez donc supprimer cette méthode dans le code behind) pour la remplacer par un binding d’une commande. Cette commande devra être définie dans le view-model :

public ICommand ChargerClientCommand { get; private set; }

N’oubliez pas d’inclure :

using System.Windows.Input;

Elle sera du type ICommand et évidemment en lecture seule afin que seul le view-model puisse instancier la commande. Cette commande doit ensuite être reliée à une méthode et pour faire cela, nous allons utiliser un délégué et plus particulièrement un délégué de type Action. Créons donc une classe qui implémente ICommand et qui associe la commande à une action. Nous pouvons placer cette classe dans le répertoire FrameworkMvvm et la nommer RelayCommand (ce nom n’est pas choisi au hasard, vous verrez plus loin pourquoi). L’interface ICommand impose de définir la méthode CanExecute qui permet d’indiquer si la commande peut être exécutée ou non. Nous allons renvoyer vrai dans tous les cas pour cet exemple. Elle oblige également à définir un événement CanExecuteChanged que nous n’allons pas utiliser et une méthode Execute qui appellera la méthode associée à la commande.
La classe RelayCommand pourra donc être :

public class RelayCommand : ICommand
{
    private readonly Action actionAExecuter;

    public RelayCommand(Action action)
    {
        actionAExecuter = action;
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        actionAExecuter();
    }
}

Ce qui fait que nous allons pouvoir instancier un objet du type RelayCommand que nous stockerons dans la propriété ChargerClientCommand dans notre view-model, par exemple depuis le constructeur :

public VoirClientViewModel()
{
    ChargerClientCommand = new RelayCommand(ChargeClient);
}

Et voilà, la méthode ChargeClilent() reste la même que précédemment. Vous pouvez donc la supprimer de l’interface, du view-model de design et même si vous le souhaitez, vous pouvez la passer en privée dans le view-model.
En ce qui concerne la propriété ChargerClientCommand, vous pouvez la déclarer dans l’interface mais ce n’est pas forcément utile car le view-model de design n’a pas besoin de connaitre la commande et ne saura d’ailleurs pas quoi mettre comme méthode dedans.

Super ces commandes sauf que la propriété Command que nous avons vu est présente sur le contrôle ButtonBase, dont hérite le contrôle Button ainsi que le contrôle de case à cocher, CheckBox. Elle n’est pas présente par contre sur tous les autres contrôles. Nous allons être bloqués pour associer une commande à un autre événement, par exemple la sélection d’un élément d’une ListBox. Nous allons voir plus loin comment résoudre ce problème.

Les frameworks à la rescousse : MVVM-Light

Entre la classe ViewModelBase et la classe RelayCommand, nous sommes en train d’écrire un vrai mini framework pour nous aider à implémenter MVVM.
Alors, je vous le dis tout de suite, nous allons nous arrêter là dans l’implémentation de ce framework… car d’autres l’ont déjà fait ; et en mieux ! :) Il existe beaucoup de framework pour utiliser MVVM et certains sont utilisables avec Windows Phone, comme par exemple le populaire MVVM Light Toolkit, que vous pouvez trouver ici.

Malgré sa dénomination, il n’est en fait pas si light que ça car il est utilisable avec WPF, Silverlight, Windows Phone et Windows 8.
Je ne vais pas faire un cours entier sur ce toolkit, car cela demanderait beaucoup trop de pages. Nous allons cependant regarder quelques points intéressants qui pourraient vous servir dans une application Windows Phone. Après, à vous de voir si vous avez besoin d’embarquer la totalité du framework ou si vous pouvez simplement vous inspirer de quelques idées…

Toujours est-il qu’il y a beaucoup de bonnes choses dans ce toolkit. Vous vous rappelez de nos deux classes ViewModelBase et RelayCommand ? Et bien, MVVM Light possède également ces deux classes… et en plus fournies !
Par exemple la classe RelayCommand. Nous en avons écrit une version ultra simplifiée. Celle de ce toolkit est complète et permet même d’y associer un paramètre. Cela peut-être utile lorsque la même commande est associée à plusieurs boutons et que chaque bouton possède un paramètre différent.

De même, ce toolkit propose une solution au problème que j’évoquais plus haut, à savoir de pouvoir relier n’importe quel événement à une commande.

Il propose également une solution pour résoudre le problème de couplage entre la vue et le view-model que nous avons rencontré précédemment. Il utilise en effet un patron de conception prévu pour ce genre de cas, le service locator, qui est un patron de conception d’inversion de dépendance.
Je ne parlerai pas en détail de ce patron de conception ici, vous pouvez trouver plus d’informations en anglais sur ce site. Sachez simplement que le principe général est d’avoir un gros objet contenant une liste de services et qui sait en retrouver un à partir d’une clé. Cette clé peut être de plusieurs sortes, comme une chaine de caractère, un type (en général une interface), etc.
Pour éviter le couplage entre la vue et le view-model, ce patron de conception permettra de faire en sorte que la vue connaisse simplement une clé afin d’obtenir son view-model, sans forcément connaître l’instance et le type du view-model.

Remarquez que pour moi, ce couplage fort entre la vue et le view-model n’est pas un vrai problème, au risque de faire hurler les puristes. Le but premier théorique est de pouvoir éventuellement changer facilement de view-model pour une même vue, sans avoir à tout modifier. En pratique, il s’avère qu’il existe toujours un et un seul view-model associé à une vue, et que c’est toujours le même. C’est pourquoi je trouve que la liaison du view-model depuis le XAML proposée dans le chapitre précédent est bien souvent suffisante.

Installation du toolkit

Voyons à présent comment créer une application MVVM Light. Nous allons installer la dernière version stable du framework à l’heure où j’écris ces lignes, à savoir la version 4.1.25. Le plus simple pour télécharger les bibliothèques est de passer par NuGet, qui est un gestionnaire de package .NET open source permettant de télécharger des bibliothèques externes très facilement.
Pour cela, allez dans le menu outils, puis gestionnaire de package de bibliothèques, puis Gérer les packages NuGet pour la solution, comme indiqué à la figure suivante.

Démarrer NuGet
Démarrer NuGet

Vous arrivez dans NuGet où vous allez pouvoir cliquer sur En ligne à gauche et commencer à rechercher le MVVM Light Toolkit (voir la figure suivante).

Installation du Mvvm Light Toolkit via NuGet
Installation du Mvvm Light Toolkit via NuGet

Vous pouvez choisir d’installer la version complète ou uniquement la bibliothèque. La version complète contient les binaires du toolkit, les templates et les snippets.

Les binaires sont indispensables pour utiliser le toolkit. Il s’agit des assemblys contenant le code du toolkit.
Les templates sont des modèles de projet permettant de créer facilement une application utilisant MVVM Light. Le projet ainsi créé contient déjà les bonnes références, un premier exemple de vue, de locator, etc.
Les snippets sont des extraits de code qui sont utilisés pour pouvoir créer plus rapidement des applications MVVM.

Comme je ne souhaite pas vous présenter tout MVVM Light et que je veux me concentrer sur la compréhension des éléments du patron de conception, je vais simplement installer les binaires en choisissant MVVM Light Libraries only. Cliquez sur Installer.
Après téléchargement (rapide) il me propose de l’installer dans ma solution en cours, comme vous pouvez le voir sur la figure suivante.

Installation de Mvvm Light Toolkit
Installation de Mvvm Light Toolkit

Faites Ok et acceptez également la licence. Et voilà, c’est installé !

Vous pouvez voir que dans votre projet, les assemblys suivantes ont automatiquement été référencées :

  • GalaSoft.MvvmLight.Extras.WP8.dll

  • GalaSoft.MvvmLight.WP8.dll

  • Microsoft.Practices.ServiceLocation

  • System.Windows.Interactivity.dll

sachant que la dernière est une assembly de Blend et que l’avant dernière permet d’utiliser le service locator.

Recréez alors un répertoire View pour y mettre votre page VoirClientView.xaml, un répertoire Model avec la classe Client et un répertoire ViewModel avec une classe VoirClientViewModel. La classe Client est la même que précédemment, le service aussi. Nous allons simplement lui rajouter une interface en plus :

public class Client
{
    public string Prenom { get; set; }
    public int Age { get; set; }
    public bool EstBonClient { get; set; }
}

public interface IServiceClient
{
    Client Charger();
}

public class ServiceClient : IServiceClient
{
    public Client Charger()
    {
        return new Client { Prenom = "Nico", Age = 30, EstBonClient = true };
    }
}

Puis nous allons rajouter une classe de design pour notre service. Au contraire de ce que nous avons fait dans le chapitre précédent, ce n’est pas ici le view-model qui sera en mode design, mais le model :

public class DesignServiceClient : IServiceClient
{
    public Client Charger()
    {
        return new Client { Prenom = "Nico", Age = 30, EstBonClient = true };
    }
}

Puis notre view-model va hériter de ViewModelBase qui se situe dans l’espace de nom GalaSoft.MvvmLight :

using GalaSoft.MvvmLight;

public class VoirClientViewModel : ViewModelBase
{
}
Le service locator

Il est temps de créer le service locator afin de bénéficier d’un couplage faible entre notre vue et le view-model, mais également entre le view-model et le service, ce qui n’est pas indispensable mais ne fait pas de mal. Pour cela, créons une nouvelle classe ViewModelLocator, je la place à la racine du projet :

public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        if (ViewModelBase.IsInDesignModeStatic)
            SimpleIoc.Default.Register<IServiceClient, DesignServiceClient>();
        else
            SimpleIoc.Default.Register<IServiceClient, ServiceClient>();

        SimpleIoc.Default.Register<VoirClientViewModel>();
    }

    public VoirClientViewModel VoirClientVM
    {
        get { return ServiceLocator.Current.GetInstance<VoirClientViewModel>(); }
    }
}

Elle contient un constructeur statique qui s’occupe de l’inversion de dépendance en elle-même, à savoir associer le service client de design avec l’interface lorsqu’on est en mode design, et le vrai service sinon. De même, il enregistre une instance du view-model. Il possède également une propriété permettant d’accéder à l’instance du ViewModel : VoirClientVM.

Ce locator devra être déclaré dans les ressources de l’application, ainsi il sera accessible depuis n’importe qu’elle vue. Modifiez-donc le fichier App.xaml pour avoir :

<Application.Resources>
    <… />

    <vm:ViewModelLocator x:Key="Locator" d:IsDataSource="true" />
</Application.Resources>

Sans oublier de déclarer l’espace de nom :

xmlns:vm="clr-namespace:DemoMvvmLight"

et d’inclure les déclarations suivantes, qu’il n’est pas nécessaire de comprendre :

xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"

Ceci correspond à l’implémentation du locator fourni dans les exemples du toolkit. Il existe plusieurs implémentations possibles de ce locator, plus ou moins parfaites.

Lier la vue au view-model

Maintenant, pour lier le modèle à la vue il suffira de modifier la propriété Datacontext de votre page pour avoir :

<phone:PhoneApplicationPage
    x:Class="DemoMvvmLight.View.VoirClientView"
    …
    DataContext="{Binding VoirClientVM, Source={StaticResource Locator}}">

La liaison s’effectue donc sur la propriété publique VoirClientVM du locator, trouvé en ressource.
Modifions notre vue pour afficher notre client :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock x:Name="PageTitle" Text="Fiche client" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" Background="{Binding BonClient}">
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition Height="auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock Text="Prénom : " />
        <TextBlock Grid.Column="1" Text="{Binding Prenom}" />
        <TextBlock Grid.Row="1" Text="Age : " />
        <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding Age}" />
    </Grid>
</Grid>

Maintenant, passons au view-model. Il ressemble beaucoup à notre précédent view-model :

public class VoirClientViewModel : ViewModelBase
{
    private readonly IServiceClient serviceClient;

    private string prenom;
    public string Prenom
    {
        get { return prenom; }
        set { NotifyPropertyChanged(ref prenom, value); }
    }


    private int age;
    public int Age
    {
        get { return age; }
        set { NotifyPropertyChanged(ref age, value); }
    }

    private SolidColorBrush bonClient;
    public SolidColorBrush BonClient
    {
        get { return bonClient; }
        set { NotifyPropertyChanged(ref bonClient, value); }
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        RaisePropertyChanged(nomPropriete);
        return true;
    }

    public VoirClientViewModel(IServiceClient service)
    {
        serviceClient = service;

        Client client = serviceClient.Charger();
        Prenom = client.Prenom;
        Age = client.Age;
        if (client.EstBonClient)
            BonClient = new SolidColorBrush(Color.FromArgb(100, 0, 255, 0));
        else
            BonClient = new SolidColorBrush(Color.FromArgb(100, 255, 0, 0));
    }
}

La différence vient bien sûr en premier lieu de l’héritage à ViewModelBase mais surtout de l’application de l’inversion de dépendance dans le constructeur du view-model. Ainsi, notre view-model récupère automatiquement une instance de notre service.
Notons aussi la différence dans la méthode NotifyPropertyChanged qui doit appeler maintenant la méthode RaisePropertyChanged de la classe de base, ViewModelBase. Cette méthode correspond à notre méthode NotifyPropertyChanged mais on se demande pourquoi le créateur du toolkit n’a pas écrit de surcharge de cette méthode utilisant les nouveautés du framework 4.5 et notamment l’attribut CallerMemberName, du coup nous sommes toujours obligés d’utiliser une variation de la méthode NotifyPropertyChanged.

N’oubliez pas de faire en sorte que ce soit la bonne vue qui s’affiche lors du démarrage de l’application et vous pourrez observer votre application qui fonctionne grâce au toolkit MVVM Light. Bravo !

Jusque-là, nous ne sommes pas trop dépaysés à part dans l’utilisation du service locator.

Les commandes

Passons maintenant aux commandes sur le bouton. Je vous ai indiqué qu’il est possible de passer un paramètre à un bouton. Pour illustrer ce fonctionnement, nous allons créer une nouvelle page qui affichera une liste de clients. Modifions notre service pour avoir la méthode suivante :

public interface IServiceClient
{
    Client Charger();
    List<Client> ChargerTout();
}

public class ServiceClient : IServiceClient
{
    public Client Charger()
    {
        return new Client { Prenom = "Nico", Age = 30, EstBonClient = true };
    }

    public List<Client> ChargerTout()
    {
        return new List<Client>
        {
            new Client { Age = 30, EstBonClient = true, Prenom = "Nico"},
            new Client { Age = 20, EstBonClient = false, Prenom = "Jérémie"},
            new Client { Age = 30, EstBonClient = true, Prenom = "Delphine"}
        };
    }
}

Et pareil dans la classe DesignServiceClient.

Rajoutons une nouvelle vue dans le répertoire View que nous allons appeler ListeClientsView.xaml. Et enfin, créons un nouveau view-model que nous appellerons ListeClientsViewModel et qui héritera de ViewModelBase. N’oubliez pas de déclarer le nouveau view-model dans le locator :

public class ViewModelLocator
{
    static ViewModelLocator()
    {
        ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

        if (ViewModelBase.IsInDesignModeStatic)
            SimpleIoc.Default.Register<IServiceClient, DesignServiceClient>();
        else
            SimpleIoc.Default.Register<IServiceClient, ServiceClient>();

        SimpleIoc.Default.Register<VoirClientViewModel>();
        SimpleIoc.Default.Register<ListeClientsViewModel>();
    }

    public VoirClientViewModel VoirClientVM
    {
        get { return ServiceLocator.Current.GetInstance<VoirClientViewModel>(); }
    }

    public ListeClientsViewModel ListeClientsVM
    {
        get { return ServiceLocator.Current.GetInstance<ListeClientsViewModel>(); }
    }
}

Votre vue va posséder une ListBox dont le template contiendra un bouton :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <ListBox ItemsSource="{Binding ListeClients}">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Button Content="Qui suis-je ?" Command="{Binding QuiSuisJeCommand}" CommandParameter="{Binding}" />
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

Nous voyons que notre ListBox est liée à une propriété ListeClients qui devra être présente dans notre view-model. Le bouton présent dans le template possède une commande liée à la commande QuiSuisJeCommand. Remarquons la propriété CommandParameter qui permet de positionner l’élément en cours comme paramètre de la commande.
Liez ensuite le view-model à la vue en modifiant la propriété Datacontext avec notre nouvelle propriété dans le locator :

DataContext="{Binding ListeClientsVM, Source={StaticResource Locator}}">

Passons à notre view-model désormais :

public class ListeClientsViewModel : ViewModelBase
{
    private List<Client> listeClients;
    public List<Client> ListeClients
    {
        get { return listeClients; }
        set { NotifyPropertyChanged(ref listeClients, value); }
    }

    private bool NotifyPropertyChanged<T>(ref T variable, T valeur, [CallerMemberName] string nomPropriete = null)
    {
        if (object.Equals(variable, valeur)) return false;

        variable = valeur;
        RaisePropertyChanged(nomPropriete);
        return true;
    }

    public ICommand QuiSuisJeCommand { get; set; }

    public ListeClientsViewModel(IServiceClient service)
    {
        ListeClients = service.ChargerTout();

        QuiSuisJeCommand = new RelayCommand<Client>(QuiSuisJe);
    }

    private void QuiSuisJe(Client client)
    {
        MessageBox.Show("Je suis " + client.Prenom);
    }
}

Remarque, la classe RelayCommand nécessite l’import suivant :

using GalaSoft.MvvmLight.Command;

Nous avons créé la propriété ListeClients que nous avons alimentée dans le constructeur grâce au modèle. Puis nous voyons comment définir une commande qui accepte un paramètre. En l’occurrence, ici le paramètre sera du type Client car c’est le type que nous passons dans :

CommandParameter="{Binding}"

L’extension de balisage {Binding} prend ici l’élément courant de la propriété énumérable ListeClients qui est bien de type Client.
Une fois le bouton cliqué, nous pourrons alors afficher le client sélectionné grâce à la boite de message MessageBox.
Vérifions cela en démarrant l’application. N’oubliez pas de changer le point d’entrée de l’application dans le fichier WMAppManifest.xml afin qu’il démarre sur la vue ListeClientsView.xaml.
Sauf que… cela ne marche pas. :) Le clic sur le bouton ne fait rien !
Si vous observez bien la fenêtre de sortie de Visual Studio, vous pouvez constater l’erreur suivante, erreur que vous aurez l’occasion de souvent voir :

System.Windows.Data Error: BindingExpression path error: 'QuiSuisJeCommand' property not found on 'DemoMvvmLight.Model.Client' 'DemoMvvmLight.Model.Client' 
(HashCode=23288300). BindingExpression: Path='QuiSuisJeCommand' DataItem='DemoMvvmLight.Model.Client' (HashCode=23288300); target element is 'System.Windows.Controls.Button' 
(Name=''); target property is 'Command' (type 'System.Windows.Input.ICommand')..

Une erreur de binding... Argh !
On nous indique que la propriété QuiSuisJeCommand est introuvable sur l’objet Client. Ce qui est vrai en soit, notre classe Client ne possède pas cette commande ! Mais nous, ce que nous voulions c’est que la commande QuiSuisJeCommand soit celle du view-model. Comme ce que nous avions fait précédemment.
C’est rageant, on fait pareil, mais cela ne fonctionne pas ! Essayons de comprendre pourquoi.

Je vous donne un indice : Datacontext.

En effet, dans notre exemple précédent, notre commande est liée à un bouton dont le contexte est celui hérité de celui de la page, car il n’a pas été changé. Nous avions lié la propriété Datacontext de la page au view-model et tous les Datacontext des objets de la page ont hérité de ce contexte.
Ici, nous avons changé le contexte de la ListBox pour lui fournir une liste de clients. Dans chaque template des éléments de la ListBox, nous sommes sur un objet Client et plus sur le view-model. C’est pour cela qu’il ne trouve pas la propriété QuiSuisJeCommand et qu’il ne peut pas faire correctement le binding.
C’est une erreur très classique que l’on peut résoudre de plusieurs façons. La première est de faire en sorte que le contexte courant puisse connaître le view-model. En l’occurrence, on pourrait ajouter une propriété pointant sur le view-model à l’objet Client ou même mieux créer un nouvel objet dédié au binding qui contient cette propriété. Créons donc une classe ClientBinding :

public class ClientBinding
{
    public string Prenom { get; set; }
    public int Age { get; set; }
    public bool EstBonClient { get; set; }
    public ListeClientsViewModel ViewModel { get; set; }
}

Puis changeons le type de la propriété ListeClient pour avoir une List<ClientBinding>. Et modifions le constructeur pour avoir :

public ListeClientsViewModel(IServiceClient service)
{
    ListeClients = service.ChargerTout().Select(c => new ClientBinding { Age = c.Age, EstBonClient = c.EstBonClient, Prenom = c.Prenom, ViewModel = this }).ToList();

    QuiSuisJeCommand = new RelayCommand<ClientBinding>(QuiSuisJe);
}

Remarquez que j’ai aussi changé le type générique de RelayCommand pour avoir un ClientBinding vu que désormais, c’est un objet de ce type qui est lié au paramètre de la commande. Ce qui implique également de changer le type du paramètre de la méthode QuiSuisJe() :

private void QuiSuisJe(ClientBinding client)
{
    MessageBox.Show("Je suis " + client.Prenom);
}

(n’oubliez pas de rajouter le using de l’espace de nom System.Linq;).

Chaque client possède donc une propriété ViewModel contenant le view-model en cours grâce à this. Il ne reste plus qu’à modifier l’extension de balisage pour avoir :

<Button Content="Qui suis-je ?" Command="{Binding ViewModel.QuiSuisJeCommand}" CommandParameter="{Binding}" />

Et voilà, cela fonctionne. Mais cela implique pas mal de changement…
L’autre solution est de lier directement la propriété Command à la propriété QuiSuisJeCommand de la propriété du locator pour accéder au view-model. Plus besoin de classe intermédiaire qui contient une référence vers le view-model. Il suffit d’écrire :

<Button Content="Qui suis-je ?" Command="{Binding Source={StaticResource Locator}, Path=ListeClientsVM.QuiSuisJeCommand}" CommandParameter="{Binding}" />

Et le résultat est le même, ainsi que vous pouvez le constater sur la figure suivante.

Affichage d'un message suite à l'activation de la commande
Affichage d'un message suite à l'activation de la commande

Pouvoir passer un paramètre à une commande est très pratique dans ce genre de situation. La classe RelayCommand du toolkit nous aide bien pour récupérer ce paramètre, que l’on retrouve en paramètre de la méthode QuiSuisJe().
Elle sait faire encore une chose intéressante, à savoir de permettre de savoir si la commande est utilisable ou pas. Rappelez-vous, c’est ce que nous avions vu plus haut. Il s’agissait de la méthode CanExecute de l’interface ICommand. J’avais décidé arbitrairement que cette méthode renverrait toujours vrai. La classe RelayCommand de MVVM Light permet d’associer une condition à la possibilité d’exécuter une commande. Par exemple, on pourrait imaginer qu’on ne puisse cliquer sur le bouton que des clients qui sont des bons clients. C’est de la ségrégation, mais c’est comme ça. Les mauvais clients resteront inconnus !
Pour ce faire, on utilisera le deuxième paramètre du constructeur de la classe RelayCommand qui permet de définir une méthode qui servira de prédicat permettant d’indiquer si la commande peut être exécutée ou non. Ici, nous aurons simplement besoin de renvoyer la valeur du booléen EstBonClient, mais cela pourrait être une méthode plus complexe. Notre instanciation de commande devient donc :

QuiSuisJeCommand = new RelayCommand<Client>(QuiSuisJe, CanExecuteQuiSuisJe);

avec :

private bool CanExecuteQuiSuisJe(Client client)
{
    if (client == null)
        return false;
    return client.EstBonClient;
}

Ainsi, non seulement il ne sera pas possible d’exécuter la commande en cliquant sur le bouton, mais le bouton est également grisé, signe de son état désactivé (voir la figure suivante).

Le bouton est grisé quand la commande n'est pas utilisable
Le bouton est grisé quand la commande n'est pas utilisable

Plutôt pratique quand un bouton doit être désactivé.
Juste avant de terminer ce point, remarquons que le prédicat associé à la possibilité d’exécution d’une commande est exécuté une unique fois. Si jamais notre client venait à devenir un bon client, notre bouton resterait dans un état désactivé car il n’aura pas été mis au courant de ce changement. À ce moment-là, MVVM Light fournit une méthode qui permet à ce prédicat de se réévaluer et ainsi modifier éventuellement l’état du bouton. Il s’agit de la méthode RaiseCanExecuteChanged. On pourra l’utiliser ainsi :

((RelayCommand)QuiSuisJeCommand).RaiseCanExecuteChanged();

Continuons cet aperçu de MVVM Light et de ses commandes en vous indiquant comment relier n’importe quel événement à une commande. Par exemple, l’événement de sélection d’un élément dans une ListBox.
Première chose à faire, modifier ma vue pour ne plus avoir ce bouton, mais simplement pour avoir le prénom du client afin qu’il soit facilement sélectionnable :

<ListBox ItemsSource="{Binding ListeClients}">
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Prenom}" Margin="10 30 0 0" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Avant, pour savoir quand un élément d’un ListBox est sélectionné, on se serait abonné à l’événement SelectionChanged. Grâce à MVVM Light, on peut utiliser l’action EventToCommand :

<ListBox ItemsSource="{Binding ListeClients}">
    <Interactivity:Interaction.Triggers>
        <Interactivity:EventTrigger EventName="SelectionChanged" >
            <Command:EventToCommand Command="{Binding SelectionElementCommand}" PassEventArgsToCommand="True"/>
        </Interactivity:EventTrigger>
    </Interactivity:Interaction.Triggers>
    <ListBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding Prenom}" Margin="10 30 0 0" />
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

Sachant qu’il faudra importer les espaces de noms suivants :

xmlns:Command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP8"
xmlns:Interactivity="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

Le XAML est un peu plus verbeux, je vous le concède. Le principe est d’utiliser les triggers de Blend qui correspondent au déclenchement d’un événement, de préciser l’événement choisi dans la propriété EventName et on pourra alors se brancher sur n’importe quel événement, ici l’événement SelectionChanged.

Reste à définir la commande dans le view-model :

public ICommand SelectionElementCommand { get; set; }

et à l’instancier, par exemple dans le constructeur :

SelectionElementCommand = new RelayCommand<SelectionChangedEventArgs>(OnSelectionElement);

Avec la méthode associée :

private void OnSelectionElement(SelectionChangedEventArgs args)
{
}

Remarquez que nous avons positionné la propriété PassEventArgsToCommand de EventToCommand à true et que nous pouvons ainsi obtenir l’argument de l’événement en paramètre de la commande. En l’occurrence, pour l’événement SelectionChanged, nous obtenons un paramètre du type SelectionChangedEventArgs.

La messagerie

Ça commence à ressembler à quelque chose, mais qu’est-ce qu’on pourrait bien faire une fois un client sélectionné ? Vous me voyez venir… on pourrait naviguer sur la vue VoirClientView.xaml et afficher le détail du client.
Rien de plus simple, nous savons naviguer dans notre application, on l’a vu dans la partie précédente… sauf que nous nous heurtons à un problème de taille.
Vous devinez ?

Le service de navigation est une propriété de la PhoneApplicationPage. À cause de notre séparation des responsabilités, la méthode associée à la commande se trouve dans le view-model qui n’a aucune connaissance de la vue.

Aie. Comment retrouver notre service de navigation ?

Ceci fait partie d’un problème plus général, à savoir : comment faire pour que le view-model puisse agir sur la présentation à part en utilisant les mécanismes du binding ? La navigation se retrouve exactement dans ce cas-là. C’est le code-behind qui aurait normalement pris en charge cette navigation, sauf que là, c’est impossible. On retrouve un cas similaire lorsque l’on cherche à afficher une boite de dialogue, autre que la MessageBox.

MVVM Light propose une solution pour résoudre cet épineux problème à travers son système de messagerie. Ce système offre la possibilité de pouvoir communiquer de manière découplée entre un view-model et sa vue ou entres view-models.
Le principe est que l’émetteur envoie un message au système de messagerie, qui le diffuse à ceux qui s’y sont abonné.
Dans notre cas, il faut donc que le code-behind s’abonne au message :

public partial class ListeClientsView : PhoneApplicationPage
{
    public ListeClientsView()
    {
        InitializeComponent();
        Messenger.Default.Register<Client>(this, AfficheClient);
    }

    private void AfficheClient(Client client)
    {
        PhoneApplicationService.Current.State["Client"] = client;
        NavigationService.Navigate(new Uri("/View/VoirClientView.xaml", UriKind.Relative));
    }
}

Cela se fait grâce à la méthode Register du Messenger, qui se trouve dans l’espace de nom GalaSoft.MvvmLight.Messaging. On indique que l’on s’abonne aux messages qui prendront un client en paramètre et que dans ce cas, la méthode AfficheClient est appelée. La méthode AfficheClient fait une navigation toute simple, comme on l’a déjà vu.

Il faut maintenant que le view-model émette le message, mais avant ça, nous allons ajouter une liaison de données pour récupérer l’élément sélectionné de la ListBox.
Remarque, on pourrait le faire avec la valeur de l’argument, mais c’est plus propre de faire comme ça. Donc changeons notre ListBox pour avoir la liaison sur l’élément sélectionné :

<ListBox ItemsSource="{Binding ListeClients}" SelectedItem="{Binding Selection, Mode=TwoWay}">

Étant donné que notre propriété sera mise à jour à partir de l’interface, le binding doit être dans les deux sens, d’où le mode TwoWay.
Ajoutons la propriété Selection dans le view-model :

private Client selection = null;
public Client Selection
{
    get { return selection; }
    set { NotifyPropertyChanged(ref selection, value); }
}

Il n’y a plus qu’à envoyer le message depuis la commande de sélection. On utilise pour cela la méthode Send du Messenger :

private void OnSelectionElement(SelectionChangedEventArgs args)
{
    Messenger.Default.Send(Selection);
}

Résumons.

  • L’utilisateur sélectionne un élément de la ListBox

  • La commande associée à l’événement est déclenchée sur le view-model

  • Le view-model émet un message avec le client sélectionné

  • Le code-behind de la vue, qui s’est abonné à ce type de message, reçoit le message émit par le view-model et déclenche la navigation

Pour terminer proprement la petite application, il faudrait que la vue qui affiche un client utilise les données positionnées dans le dictionnaire d’état. Alors, comment feriez-vous ?

Il y a plusieurs solutions, je vous propose la plus simple.
Nous allons profiter qu’un message est diffusé à chaque sélection d’un élément pour mettre à jour les propriétés du view-model :

public VoirClientViewModel()
{
    Messenger.Default.Register<Client>(this, MetAJourClient);
    Client client = (Client)PhoneApplicationService.Current.State["Client"];
    MetAJourClient(client);
}

private void MetAJourClient(Client client)
{
    Prenom = client.Prenom;
    Age = client.Age;
    if (client.EstBonClient)
        BonClient = new SolidColorBrush(Color.FromArgb(100, 0, 255, 0));
    else
        BonClient = new SolidColorBrush(Color.FromArgb(100, 255, 0, 0));
}

Il suffit de s’abonner également à la réception de ce message afin de mettre à jour les propriétés avec le nouveau client courant. Ce message est bien celui émit par le view-model de la liste des clients et c’est le view-model qui permet de voir un client qui le reçoit après s’y être abonné.
Il faudra faire attention à l’initialisation où nous utiliserons le dictionnaire d’état pour récupérer la première sélection.

Notons enfin que vous devez réinitialiser la propriété Selection afin que la ListBox ne conserve pas la sélection lors du retour arrière sur la page :

private void OnSelectionElement(SelectionChangedEventArgs args)
{
	if (Selection == null)
		return;
	Messenger.Default.Send(Selection);
	Selection = null;
}

Voilà pour ce petit tour de MVVM Light. Nous avons vu l’essentiel de ce toolkit qui propose des solutions pour aider à la mise en place du patron de conception MVVM. N’oubliez pas que malgré sa dénomination de light, c’est un framework complet qui prend sa place (110 ko). Il est en fait light par rapport à d’autres framework, comme PRISM qui est utilisé avec WPF. A utiliser en connaissance de cause.

D'autres frameworks MVVM

MVVM Light n’est pas le seul framework aidant à la mise en place de MVVM. C’est assurément l’un des plus connus, mais d’autres existent respectant plus ou moins bien le patron de conception et apportant des outils différents.
Citons par exemple :

Je ne peux pas bien sur tous les présenter et d’ailleurs je ne les ai pas tous testés :-° .
J’aime bien l’UltraLight.mvvm qui, via son locator, offre une liaison avec la page ce qui permet facilement de démarrer une navigation sans passer par l’utilisation d’une messagerie.
N’hésitez pas à les tester pour vous faire votre propre opinion et pour vous permettre de voir ce que vous souhaitez garder de MVVM et ce dont vous pouvez vous passer.

Faut-il utiliser systématiquement MVVM ?

MVVM est complexe à appréhender. Pour bien le comprendre, il faut pratiquer. Ce n’est que petit à petit que vous verrez vraiment de quoi vous avez besoin et à quel moment.
J’imagine que pour l’instant, vous avez l’impression que MVVM pose plus de problèmes qu’il n’en résout et qu’on se complique la vie pour pas grand-chose. Franchement, le coup de la navigation et de la messagerie, c’est censé nous simplifier la vie ?
Il est vrai que lorsque l’on réalise des petites applications, respecter parfaitement le patron de conception MVVM est sans doute un peu démesuré. Cela implique toute une mécanique qui est plutôt longue à mettre en place et parfois ennuyeuse, pour un gain pas forcément évident.
N’oubliez cependant pas que le but premier de MVVM est de séparer les responsabilités, notamment en séparant les données de la vue. Cela facilite les opérations de maintenance en limitant l’éternel problème du plat de spaghetti où la moindre correction a des impacts sur un autre bout de code. Mon avis sur MVVM est que peu importe si vous ne respectez pas parfaitement MVVM, le principe de ce pattern est de vous aider dans la réalisation de votre application et surtout dans sa maintenabilité.

L’intérêt également est qu’il devient possible de faire des tests unitaires sur le view-model, sans avoir besoin de charger l’application et de cliquer partout. Cela permet de tester chaque fonctionnalité, dans un processus automatisé, ce qui dans une grosse application est un atout considérable pour éviter les régressions.
En tous cas, n’ayez crainte. Vous n’êtes pas obligés de pratiquer MVVM tout de suite. En tant que débutant, vous aurez plutôt intérêt à commencer à créer des applications et ensuite à chercher à appliquer des bonnes pratiques. Dans ce cas, n’hésitez pas à revenir lire ce chapitre .

  • MVVM est un patron de conception qui aide à la réalisation d’applications d’envergure utilisant le XAML.

  • Des frameworks gratuits nous aident à la mise en place de ce patron de conception en fournissant des bibliothèques remplies de classes éprouvées.

  • Le respect et l’utilité de MVVM se découvrent en pratiquant. Ne soyez pas forcément trop pressés de respecter parfaitement MVVM.

Exemple de certificat de réussite
Exemple de certificat de réussite