1.Introduction

Très à la mode, le pattern M-V-VM se présente comme la meilleure solution pour développer des applications avec Silverlight et WPF. Sans rentrer dans ses avantages, il permet notamment de séparer les responsabilités de l'IHM des traitements métiers.
Nous allons voir dans cet article une introduction à son adoption en utilisant le MVVM Light Toolkit de Laurent Bugnion.

Tout au long de ce tutoriel, j'utiliserai Visual Studio 2010 et la version 3 de Silverlight.

Notons que Silverlight 4 apporte quelques évolutions qui permettent de développer plus facilement avec un pattern comme M-V-VM, comme le support natif des commandes, mais qui ne sont pas nécessaire lorsqu'on utilise MVVM Light Toolkit.

2.Présentation M-V-VM

2.1.Le design Pattern

M-V-VM signifie Model View ViewModel. Ce design pattern a été présenté de nombreuses fois, je vais donc le présenter très brièvement et tenter plutôt une métaphore pour l'expliquer. Pour plus d'informations, consulter par exemple WPF Apps With The Model-View-ViewModel Design Pattern, Wikipédia, ...

- M : Modèle => grosso modo les données, les services qui mettent à disposition des objets de données. (Une classe "Client" par exemple)
- V : Vue => ce que l'on voit, il s'agit en l'occurrence du xaml.
- VM : View Model => la colle entre les deux. Le view model est une classe C# et ne peut fonctionner que grâce à la puissance du binding xaml et à l'interface INotifyPropertyChanged.

Pour l'expliquer, essayons de voir ca comme un jeu, disons une machine à sous d'un casino.

- Mon modèle => les différentes valeurs des images de la machine à sous, dont le fameux 7 qui fait gagner le gros lot.
- Ma vue => la carcasse de la boite à sous, ce qu'on voit.
- Mon ViewModel : les engrenages qui relient les images à 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 images ont changé) et la vue est mise à jour automatiquement. Je peux voir que les trois sept sont alignés. JACKPOT.

Je suis bien conscient que la métaphore n'est surement pas la meilleure mais j'espère qu'avec ça, vous aurez plus de facilité à appréhender ce design pattern.

2.2.MVVM Toolkit

MVVM Toolkit est un ensemble de bibliothèques, créé par Laurent Bugnion. Elles fournissent des classes qui facilitent l'adoption de M-V-VM. Vous pouvez télécharger ce toolkit sur le site de Galasoft.

MVVM Toolkit n'est absolument pas obligatoire pour développer une application en utilisant le pattern M-V-VM, mais ce toolkit facilite grandement son utilisation. J'apprécie particulièrement sa simplicité et malgré sa dénomination de "light", ce toolkit dispose de toutes les bases pour développer une application utilisant ce pattern.

3.Exemple d'utilisation de M-V-VM

Pour illustrer l'utilisation du toolkit, je vais m'attacher à transformer le projet qui est généré lors de la création d'un nouveau projet de type Silverlight Navigation Application pour qu'il respecte le pattern M-V-VM.

3.1.Création du projet

Commençons par créer le nouveau projet : New Project -> Silverlight -> Silverlight Navigation Application, et nommons notre application : DemoMvvmLight.

Image non disponible
Figure 1: Création du nouveau projet.

Première chose, compilons cette application et lançons là pour vérifier que tout va bien.

Image non disponible
Figure 2: Première exécution du projet.

3.2.Vers M-V-VM

Certes, l'application créée n'est pas très aboutie. Maintenant, transformons celle-ci pour qu'elle respecte le pattern M-V-VM, et pour ce faire, on va utiliser le MVVM Light Toolkit de Laurent Bugnion.

3.2.1.Installer MVVM Toolkit

L'installation est très simple, rendez vous sur la page dédiée.
Récupérer ensuite les binaires et les templates pour la version de votre choix.

Pour les binaires, Le zip proposé encourage à positionner les dll dans Program Files, ce qui n'est pas obligatoire.
Pour les templates, il faudra :
- récupérer la racine du path utilisé pour les templates (Tools --> Options --> Project and Solutions )

Image non disponible
Figure 3: Récupérer le path des templates.

- extraire le contenu du zip dans le répertoire

Tout à été fait par Laurent pour que ce soit simple.
Il ne restera plus qu'à fermer Visual Studio pour voir apparaitre les nouveaux templates, comme on le verra dans le chapitre 5.

3.2.2.Référencer MVVM Toolkit

Récupérer les trois dll suivantes depuis les binaires :

- GalaSoft.MvvmLight.dll
- GalaSoft.MvvmLight.Extras.dll
- System.Windows.Interactivity.dll

et les référencer dans le projet :

Image non disponible
Figure 4: Référencement des DLL du toolkit.

3.2.3.Créer le modèle

Dans un premier temps, ouvrons le fichier Views/Home.xaml, nous pouvons voir notamment :

Home.xaml
Sélectionnez

<Grid x:Name="LayoutRoot">
    <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
        <StackPanel x:Name="ContentStackPanel">
            <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                               Text="Home"/>
            <TextBlock x:Name="ContentText" Style="{StaticResource ContentTextStyle}"
                               Text="Home page content"/>
        </StackPanel>
    </ScrollViewer>
</Grid>

On trouve deux TextBlock à qui ont a affecté deux valeurs statiques. Dans une vraie application, on devra charger ces valeurs à partir de la base de données et les rendre disponibles grâce au modèle.
Profitons-en pour créer un modèle minimal, ce qui correspondra à l'objet échangé et au service qui permet de le récupérer. Dans une vraie application, on ferait appel à un web service pour aller charger les données de son modèle ; simulons ce fonctionnement et créons un nouveau projet de type Sylverlight Class Library qu'on appelle DemoMvvm.NavigationService.

Image non disponible
Figure 5: Création du projet NavigationService.

Et rajoutons une classe NavigationService :

NavigationService.cs
Sélectionnez

public class NavigationService
{
    public delegate void GetNavigationNodeHanlder(GetNavigationNodeCompletedEventArgs args);
    public event GetNavigationNodeHanlder OnGetNavigationNodeCompleted;

    public void GetNavigationNodeAsync(string nodeName)
    {
        NavigationNode node = GetNodeFrom(nodeName);
        if (OnGetNavigationNodeCompleted != null)
            OnGetNavigationNodeCompleted(new GetNavigationNodeCompletedEventArgs { Result = node });
    }

    private NavigationNode GetNodeFrom(string nodeName)
    {
        switch (nodeName)
        {
            case "Home" :
                return new NavigationNode { Title = "Accueil", Content = "Bienvenue sur la page d'accueil" };
            case "About":
                return new NavigationNode { Title = "A propos", Content = "Ici on parle de nous" };
            default :
                throw new NotImplementedException();
        }
    }
}

Avec

GetNavigationNodeCompletedEventArgs.cs
Sélectionnez

public class GetNavigationNodeCompletedEventArgs : EventArgs
{
    public NavigationNode Result { get; set; }
}

et :

NavigationNode.cs
Sélectionnez

public class NavigationNode
{
    public string Title { get; set; }
    public string Content { get; set; }
}

Notre simulation de modèle est terminée.

3.2.4.Créer un viewmodel et utiliser le pattern Service Locator

Nous allons créer notre ViewModel qui va nous permettre de charger le modèle. Nommons-le HomeViewModel et plaçons le dans le répertoire ViewModel.

Pour fonctionner avec MVVM Light Toolkit, le ViewModel doit dériver de la classe ViewModelBase.

Une fois que le ViewModel aura chargé les données, il pourra les mettre à disposition de la vue. La vue devra donc être liée au ViewModel, grâce au DataContext.
Pour ce faire et pour éviter le couplage fort, on va utiliser le pattern Service Locator en créant une nouvelle classe :

ViewModelLocator.cs
Sélectionnez

public class ViewModelLocator
{
    private static HomeViewModel _homeViewModel;
    public static HomeViewModel HomeViewModelStatic
    {
        get
        {
            if (_homeViewModel == null)
                _homeViewModel = new HomeViewModel();
            return _homeViewModel;
        }
    }
    public HomeViewModel HomeViewModel
    {
        get { return HomeViewModelStatic; }
    }
}

On déclarera ce Locator dans les ressources de l'application, ainsi il sera accessible depuis n'importe quelle vue :

app.xaml
Sélectionnez

<Application  
  x:Class="DemoMvvmLight.App"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:vm="clr-namespace:DemoMvvmLight"
  mc:Ignorable="d">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Assets/Styles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
        <vm:ViewModelLocator x:Key="Locator"
                             d:IsDataSource="True" />
    </Application.Resources>
</Application>

3.2.5.Lier le viewmodel à la vue

Afin de lier le viewmodel à la vue, on déclarera le viewmodel dans le xaml de la vue :

Home.xaml
Sélectionnez

DataContext="{Binding HomeViewModelStatic, Source={StaticResource Locator}}"

Il ne restera plus qu'à binder les deux textbox au ViewModel :

Home.xaml
Sélectionnez

<Grid x:Name="LayoutRoot">
	<ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
		<StackPanel x:Name="ContentStackPanel">
			<TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
							   Text="{Binding NavigationNode.Title}"/>
			<TextBlock x:Name="ContentText" Style="{StaticResource ContentTextStyle}"
							   Text="{Binding NavigationNode.Content}"/>
		</StackPanel>
	</ScrollViewer>
</Grid>

Notez que Visual Studio 2010 arrive à charger les valeurs dans son designer :

Image non disponible
Figure 6: Aperçu du ViewModel dans le designer de Visual Studio 2010.

Faisons pareil maintenant pour la Vue About et créons son viewmodel :

AboutViewModel.cs
Sélectionnez

public class AboutViewModel: ViewModelBase
{
    public AboutViewModel()
    {
        NavigationService navigationService = new NavigationService();
        navigationService.OnGetNavigationNodeCompleted += navigationService_OnGetNavigationNodeCompleted;
        navigationService.GetNavigationNodeAsync("About");
    }

    void navigationService_OnGetNavigationNodeCompleted(GetNavigationNodeCompletedEventArgs args)
    {
        NavigationNode = args.Result;
    }

    public const string NavigationNodePropertyName = "NavigationNode";
    private NavigationNode _navigationNode = null;

    public NavigationNode NavigationNode
    {
        get
        {
            return _navigationNode;
        }

        private set
        {
            if (_navigationNode == value)
                return;
            _navigationNode = value;
            RaisePropertyChanged(NavigationNodePropertyName);
        }
    }
}

On n'oubliera bien sûr pas de le déclarer dans le ViewModelLocator :

ViewModelLocator.cs
Sélectionnez

private static AboutViewModel _aboutViewModel;
public static AboutViewModel AboutViewModelStatic
{
    get
    {
        if (_aboutViewModel == null)
            _aboutViewModel = new AboutViewModel();
        return _aboutViewModel;
    }
}
public AboutViewModel AboutViewModel
{
    get
    {
        return AboutViewModelStatic;
    }
}

Notez que pour être propre, cette classe devrait permettre de libérer ses ressources, créons donc une méthode de libération de ressources :

ViewModelLocator.cs
Sélectionnez

public static void Cleanup()
{
    Cleanup(ref _homeViewModel);
    Cleanup(ref _aboutViewModel);
}

private static void Cleanup<T>(ref T viewModelBase) where T : ViewModelBase
{
    if (viewModelBase != null)
    {
        viewModelBase.Cleanup();
        viewModelBase = null;
    }
}

Que l'on appellera dans l'événement Exit de l'application :

app.xaml.cs
Sélectionnez

public partial class App : Application
{
    public App()
    {
        this.Startup += this.Application_Startup;
        this.UnhandledException += this.Application_UnhandledException;
        Exit += App_Exit;

        InitializeComponent();
    }

    private void App_Exit(object sender, EventArgs e)
    {
        ViewModelLocator.Cleanup();
    }
   
    [...]
}

Il ne reste plus qu'à lier le viewmodel à la vue :

About.xaml
Sélectionnez

DataContext="{Binding AboutViewModelStatic, Source={StaticResource Locator}}"

et à faire le binding des valeurs :

About.xaml
Sélectionnez

<Grid x:Name="LayoutRoot">
    <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
        <StackPanel x:Name="ContentStackPanel">
            <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                       Text="{Binding NavigationNode.Title}"/>
            <TextBlock x:Name="ContentText" Style="{StaticResource ContentTextStyle}"
                       Text="{Binding NavigationNode.Content}"/>
        </StackPanel>
    </ScrollViewer>
</Grid>

Si on compile et qu'on exécute l'application, on voit bien les valeurs issues de notre modèle :

Image non disponible
Figure 7: Les vues Home et About utilisent M-V-VM.

Voilà pour ces deux vues, cela s'avère plutôt simple.

3.2.6.Lier les événements à des commandes

Maintenant, ouvrons MainPage.xaml et modifions la propriété Source du contrôle navigation:Frame et remplaçons "/Home" par "/Home2".
Lorsqu'on relance l'application, on obtient la fenêtre suivante :

Image non disponible
Figure 8: Erreur de chargement de la vue Home2.

Il s'agit de la vue ErrorWindow.xaml ; ouvrons son code behind, que voyons-nous :
En fonction du constructeur de la classe ErrorWindow, on remplit un Textbox avec un message. C'est bien beau, mais pas très M-V-VM. On aimerait pouvoir exploiter la puissance du binding xaml et associer un viewmodel à cette vue.

Créons donc un ViewModel pour cette vue :

ErrorWindowViewModel.cs
Sélectionnez

public class ErrorWindowViewModel : ViewModelBase
{
    public ErrorWindowViewModel()
    {
    }

    public const string ErrorMessagePropertyName = "ErrorMessage";

    private string _errorMessage;
    public string ErrorMessage
    {
        get
        {
            return _errorMessage;
        }
        set
        {
            if (_errorMessage == value)
                return;
            _errorMessage = value;
            RaisePropertyChanged(ErrorMessagePropertyName);
        }
    }
}

Vous êtes habitués maintenant, on doit rajouter ce viewmodel dans le localisateur de service et modifions le xaml pour changer le textbox ErrorTextBox pour avoir :

ErrorWindow.xaml
Sélectionnez

<TextBox x:Name="ErrorTextBox" Height="90" TextWrapping="Wrap" IsReadOnly="True"
    VerticalScrollBarVisibility="Auto" Text="{Binding ErrorMessage}"/>

(Laissons les autres chaines en dur dans le xaml)
Maintenant, voyons comment est intanciée cette ErrorWindow. Dans le code behind de MainPage.xaml, on voit :

ErrorWindow.xaml.cs
Sélectionnez

// If an error occurs during navigation, show an error window
private void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
    e.Handled = true;
    ChildWindow errorWin = new ErrorWindow(e.Uri);
    errorWin.Show();
}

On constate qu'on instancie la vue ChildWindow et qu'on passe un objet dans le constructeur. C'est ce constructeur qui mettait à jour le textblock, ce qu'on cherche à éviter.

3.2.7.Communiquer grâce au Mediator

Comment indiquer alors au viewmodel que l'on souhaite mettre une valeur dans sa propriété ErrorMessage ? Comment accéder à ce viewmodel ?
On sait que le viewmodel est lié à la vue grâce à la propriété DataContext et on serait tenté d'utiliser cette propriété pour récupérer ce viewmodel. Mais cela nous obligerait à caster ce DataContext et créerait un couplage fort entre la vue et le viewmodel, ce qui est contraire à M-V-VM.

Ce que nous propose le mvvm toolkit est un système de messages, sur base du pattern mediator. Dans cet exemple, nous allons plutôt utiliser le mediator de Marlon Grech.

Nous allons créer un nouveau projet Class Library pour importer les sources : DemoMvvm.Toolkit.
Vous trouverez dans l'archive en téléchargement à la fin de l'article les sources du mediator où j'ai passé un petit coup de resharper.
J'ai également créé un Singleton pour faciliter l'accès au mediator.

Le but est de pouvoir informer le viewmodel qu'on lui passe une Uri, pour ce faire, on va utiliser le SingletonMediator et remplacer la méthode du dessus par :

ErrorWindow.xaml.cs
Sélectionnez

// If an error occurs during navigation, show an error window
private void ContentFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
    e.Handled = true;
    ChildWindow errorWin = new ErrorWindow();
    SingletonMediator.Instance.NotifyColleagues(MediatorMessages.UriMessage, e.Uri);
    errorWin.Show();
}

Une classe MediatorMessages a été créé pour l'occasion et contient les messages que l'on va utiliser pour communiquer :

MediatorMessages.cs
Sélectionnez

public static class MediatorMessages
{
    public const string ExceptionObjectMessage = "ExceptionObjectMessage";
    public const string UriMessage = "UriMessage";
}

Il faut maintenant que le viewmodel s'abonne à ce message. Dans le constructeur, il s'enregistre auprès du mediator par :

ErrorWindowViewModel.cs
Sélectionnez

SingletonMediator.Instance.Register(this);

et on pourra déclarer une méthode qui sera appelée lorsqu'on recevra le message

ErrorWindowViewModel.cs
Sélectionnez

[MediatorMessageSink(MediatorMessages.UriMessage, ParameterType = typeof(Uri))]
public void OnErrorMessage(Uri uri)
{
    ErrorMessage = "Page non trouvée : \"" + uri + "\"";
}

On constate l'utilisation d'un attribut pour indiquer que cette méthode sera appelée lors de la réception du message UriMessage, le paramètre envoyé étant du type Uri.
Ainsi, on pourra mettre à jour la propriété ErrorMessage du viewmodel qui est bindée à un textbox dans le xaml.

Cela nous permet d'épurer le fichier ErrorWindow.xaml.cs pour ne garder que le minimum :

ErrorWindow.xaml.cs
Sélectionnez

public partial class ErrorWindow : ChildWindow
{
    public ErrorWindow()
    {
        InitializeComponent();
    }

    private void OKButton_Click(object sender, RoutedEventArgs e)
    {
        this.DialogResult = true;
    }
}

Il reste à faire la même chose dans Application_UnhandledException, c'est le même principe, je ne le présenterai pas.

En relançant l'application, on peut constater le résultat en voyant que la fenêtre affiche un "Page non trouvée", comme fait dans la méthode OnErrorMessage.

On a pu voir que l'instanciation de la classe ChildWindow se faisait dans l'événement ContentFrame_NavigationFailed, dans le code behind de la vue. N'y aurait-il pas moyen de rendre ceci un peu plus mvvm-friendly ?

Bien sur que si, grâce à la behavior EventToCommand.
Rajoutons 2 nouveaux namespaces dans MainPage.xaml :

MainPage.xaml.cs
Sélectionnez

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

Et transformons le contrôle navigation:Frame :

MainPage.xaml.cs
Sélectionnez

<navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}"
                  Source="/Home2" Navigated="ContentFrame_Navigated">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="NavigationFailed">
            <cmd:EventToCommand PassEventArgsToCommand="True"
                                Command="{Binding NavigateFailedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <navigation:Frame.UriMapper>
      <uriMapper:UriMapper>
        <uriMapper:UriMapping Uri="" MappedUri="/Views/Home.xaml"/>
        <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/{pageName}.xaml"/>
      </uriMapper:UriMapper>
    </navigation:Frame.UriMapper>
</navigation:Frame>

On peut voir que j'ai supprimé l'événement NavigationFailed et que je l'ai remplacé par un Trigger Blend. On utilise ensuite EventToCommand pour lier l'événement à une commande. Notez l'utilisation de PassEventArgsToCommand à true pour passer les arguments à la commande.

Il faudra donc créer un viewmodel pour MainPage comme on sait faire désormais et lui ajouter une commande NavigateFailedCommand.

MainPageViewModel.cs
Sélectionnez

public class MainPageViewModel : ViewModelBase
{
    public MainPageViewModel()
    {
        NavigateFailedCommand = new RelayCommand<NavigationFailedEventArgs>(e =>
        {
            e.Handled = true;
            ChildWindow errorWin = new ErrorWindow();
            SingletonMediator.Instance.NotifyColleagues(MediatorMessages.UriMessage, e.Uri);
            errorWin.Show();
        });
    }

    public ICommand NavigateFailedCommand { get; internal set; }
}

On crée donc une commande en déclarant une propriété de type ICommand qui est un objet du Framework.net. On l'instanciera grâce à la classe RelayCommand du MVVM Toolkit et on pourra spécifier le type du paramètre attendu.
Ici je crée une commande dans sa forme la plus simple. Le toolkit permet de préciser éventuellement une méthode permettant la détermination de l'activation d'une commande.

On peut désormais supprimer l'événement en question dans le code behind de MainPage.xaml.cs et repositionner la propriété Source du navigation:frame à "/Home".

3.2.8.Lier les animations

Passons désormais à l'événement ContentFrame_Navigated. Ici ça se corse. Que fait la méthode ?

MainPage.xaml.cs
Sélectionnez

foreach (UIElement child in LinksStackPanel.Children)
{
    HyperlinkButton hb = child as HyperlinkButton;
    if (hb != null && hb.NavigateUri != null)
    {
        if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))
        {
            VisualStateManager.GoToState(hb, "ActiveLink", true);
        }
        else
        {
            VisualStateManager.GoToState(hb, "InactiveLink", true);
        }
    }
}

En cas de navigation, elle regarde tous les HyperlinkButton du LinksStackPanel, et effectue une animation sur l'élément de navigation grâce au VisualStateManager pour le passer d'actif à inactif.

Pour rendre ceci conforme à M-V-VM, on utilise dans un premier temps L'EventToCommand pour brancher l'événement Navigated sur une commande :

MainPage.xaml
Sélectionnez

<navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}"
                  Source="/Home">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Navigated">
            <cmd:EventToCommand PassEventArgsToCommand="True"
                                Command="{Binding NavigatedCommand}" />
        </i:EventTrigger>
        <i:EventTrigger EventName="NavigationFailed">
            <cmd:EventToCommand PassEventArgsToCommand="True"
                                Command="{Binding NavigateFailedCommand}" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <navigation:Frame.UriMapper>
      <uriMapper:UriMapper>
        <uriMapper:UriMapping Uri="" MappedUri="/Views/Home.xaml"/>
        <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/{pageName}.xaml"/>
      </uriMapper:UriMapper>
    </navigation:Frame.UriMapper>
</navigation:Frame>

On pourra définir la commande dans le viewmodel :

MainPageViewModel.cs
Sélectionnez

public ICommand NavigatedCommand { get; internal set; }

public MainPageViewModel()
{
    NavigatedCommand = new RelayCommand<NavigationEventArgs>(e =>
    {
    });
}

Mais comment va-t-on pouvoir interagir depuis le viewmodel avec le VisualStateManager sur les contrôles qui nous sont inconnus ?
La solution que je vais exposer ici n'est sans doute pas la meilleure, si vous connaissez un meilleur moyen d'y parvenir, je suis preneur.

La première chose à faire va être de se doter d'une classe qui fourni un propriété attachée pour chaque contrôle afin de permettre la transition jusqu'à un nouvel état.
Nous allons pour ce faire récupérer la classe VisualStates d'Alex van Beek.

VisualStates.cs
Sélectionnez

public static class VisualStates
{
    public static readonly DependencyProperty CurrentStateProperty = 
		DependencyProperty.RegisterAttached("CurrentState", typeof(String), 
		typeof(VisualStates), new PropertyMetadata(TransitionToState));

    public static string GetCurrentState(DependencyObject obj)
    {
        return (string)obj.GetValue(CurrentStateProperty);
    }

    public static void SetCurrentState(DependencyObject obj, string value)
    {
        obj.SetValue(CurrentStateProperty, value);
    }

    private static void TransitionToState(object sender, DependencyPropertyChangedEventArgs args)
    {
        Control c = sender as Control;
        if (c != null)
            VisualStateManager.GoToState(c, (string)args.NewValue, true);
        else
            throw new ArgumentException("CurrentState is only supported on the Control type");
    }
}

On pourra définir cette propriété attachée sur les HyperlinkButton :

MainPage.xaml
Sélectionnez

<HyperlinkButton x:Name="Link1" Style="{StaticResource LinkStyle}"
                 NavigateUri="/Home" TargetName="ContentFrame" Content="home"
                 mvvm:VisualStates.CurrentState="{Binding HomeCurrentState}"/>
                 
<Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}"/>

<HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}"
                 NavigateUri="/About" TargetName="ContentFrame" Content="about"
                 mvvm:VisualStates.CurrentState="{Binding AboutCurrentState}"/>

et les lier à des propriétés du viewmodel :

MainPageViewModel.cs
Sélectionnez

public const string HomeCurrentStatePropertyName = "HomeCurrentState";
private string _homeCurrentState;
public string HomeCurrentState
{
    get
    {
        return _homeCurrentState;
    }
    set
    {
        if (_homeCurrentState == value)
            return;
        _homeCurrentState = value;
        RaisePropertyChanged(HomeCurrentStatePropertyName);
    }
}

public const string AboutCurrentStatePropertyName = "AboutCurrentState";
private string _aboutCurrentState;
public string AboutCurrentState
{
    get
    {
        return _aboutCurrentState;
    }
    set
    {
        if (_aboutCurrentState == value)
            return;
        _aboutCurrentState = value;
        RaisePropertyChanged(AboutCurrentStatePropertyName);
    }
}

Ainsi, notre commande pourra s'écrire :

MainPageViewModel.cs
Sélectionnez

NavigatedCommand = new RelayCommand<NavigationEventArgs>(e =>
{
    if (e.Uri.ToString() == "/Home")
    {
        HomeCurrentState = "ActiveLink";
        AboutCurrentState = "InactiveLink";
    }
    else
    {
        HomeCurrentState = "InactiveLink";
        AboutCurrentState = "ActiveLink";
    }
});

On pourra supprimer la méthode ContentFrame_Navigated dans le code behind de MainPage.xaml.
Et voilà, le tour est joué.

4.Un peu plus loin dans les commandes

Nous avons eu un aperçu des commandes dans le chapitre 3 avec des commandes relativement simples où nous transformions un événement en une commande grâce à EventToCommand.
Le MVVM Light Toolkit nous permet de créer des commandes plus évoluées, avec passage de paramètres à la commande. Voyons ceci d'un peu plus près.

4.1.Une simple commande sur un bouton

Créons un nouveau TextBlock qui sera lié à l'heure courante :

Home.xaml
Sélectionnez

<TextBlock Style="{StaticResource ContentTextStyle}" 
                                   Text="{Binding CurrentTime}"/>

Avec dans le ViewModel :

HomeViewModel.cs
Sélectionnez

public string CurrentTime
{
	get { return DateTime.Now.ToLongTimeString(); }
}

Créons maintenant un nouveau bouton sur la vue Home. Ce bouton nous permettra de forcer le rafraichissement du TextBlock. Il sera associé à une commande qui permettra de notifier à la vue qu'elle doit rafraichir le TextBlock.
La commande sera :

MainViewModel.cs
Sélectionnez

public ICommand ChangeCommand { get; internal set; }

public HomeViewModel()
{
	ChangeCommand = new RelayCommand(() => RaisePropertyChanged("CurrentTime"), () => true);
}

Le premier paramètre de RelayCommand correspond à l'action, ici on demande au mécanisme de binding de mettre à jour la propriété liée CurrentTime. Le deuxième paramètre permet d'indiquer si la commande est active ou pas, ici on indique qu'elle est toujours active. Mais on pourrait imaginer de permettre l'activation de la commande uniquement pour les trois premiers clicks ...

Pour relier la commande au click du bouton, le MVVM Light Toolkit nous fournit une propriété dépendante, spécifique au bouton. On l'utilisera de cette façon :

MainViewModel.cs
Sélectionnez

<Button cmd:ButtonBaseExtensions.Command="{Binding ChangeCommand}" 
	Content="Mettre à jour l'heure" Width="150" Height="35" />

Ainsi, en ré-exécutant l'application, chaque click provoquera le rafraichissement de l'heure.

Image non disponible
Figure 09: La commande rafraichit l'heure une fois activée.

4.2.Une simple commande sur un bouton avec un paramètre

La classe RelayCommand dispose d'une version générique RelayCommand<T> qui, associée à l'attribut CommandParameter permet de passer un paramètre à la commande. On pourra s'en servir par exemple pour créer une zone de recherche (un TextBox, un Button et un TextBlock pour le résultat de la recherche) :

Home.xaml
Sélectionnez

<TextBox x:Name="search" HorizontalAlignment="Left" Width="150"/>
<Button cmd:ButtonBaseExtensions.Command="{Binding SearchCommand}" 
	cmd:ButtonBaseExtensions.CommandParameter="{Binding ElementName=search, Path=Text}" 
	Content="Rechercher" Width="100" Height="35" />
<TextBlock Style="{StaticResource ContentTextStyle}" Text="{Binding SearchResult}"/>

J'utilise ici la propriété ButtonBaseExtensions.CommandParameter pour indiquer que le paramètre à passer à la commande sera à trouver dans la propriété Text de l'élément search, le TextBox. Ceci m'évite d'avoir une propriété de mon ViewModel bindée au TextBox.
Je crée par contre une propriété SearchResult dans le ViewModel pour afficher le résultat :

MainViewModel.cs
Sélectionnez

public const string SearchResultPropertyName = "SearchResult";
private string _searchResult = null;
public string SearchResult
{
	get
	{
		return _searchResult;
	}

	private set
	{
		if (_searchResult == value)
			return;
		_searchResult = value;
		RaisePropertyChanged(SearchResultPropertyName);
	}
}

Et la commande SearchCommand :

MainViewModel.cs
Sélectionnez

public ICommand SearchCommand { get; internal set; }

public HomeViewModel()
{
	SearchCommand = new RelayCommand<string>(textToSearch => 
		SearchResult = "Recherche sur " + textToSearch, 
		textToSearch => textToSearch != "abc");
}

On remarque l'utilisation de la version générique de RelayCommand. On pourra noter également que j'ai légèrement changé la condition qui rend active ou inactive la commande. Si vous tapez "abc" dans le TextBox, la commande sera inactive. On pourra observer que le bouton se grise.

Image non disponible
Figure 10: La commande se grise lorsque la condition de désactivation est remplie.

5.Utiliser les templates pour créer un nouveau projet M-V-VM

Lors de l'installation, nous avons eu l'occasion d'extraire les templates Visual Studio pour MVVM Light Toolkit.
Après avoir redémarré Visual Studio, on peut constater les nouveaux templates disponibles :

Image non disponible
Figure 11: Création d'un projet avec le template MVVM Light Toolkit.

Si vous créez un nouveau projet avec ce template, on retrouve une application avec l'arborescence suivante :

Image non disponible
Figure 12: Arborescence d'un projet créé avec le template MVVM Light Toolkit.

On retrouve notamment la classe ViewModelLocator, utilisée dans le cadre du pattern Service Locator.
On retrouve également un ViewModel : MainViewModel.
Dans ce viewmodel, un nouvel élément dans le constructeur :

MainViewModel.cs
Sélectionnez

public MainViewModel()
{
    if (IsInDesignMode)
    {
        // Code runs in Blend --> create design time data.
    }
    else
    {
        // Code runs "for real"
    }
}

Il s'agit d'une propriété qui vient de la classe ViewModelBase qui permet d'indiquer si l'on est en mode Design ou pas. Cela nous permettra de fournir un comportement par défaut lorsque le ViewModel est utilisé lorsque la vue est chargée dans Blend.

N'utilisant pas Blend, je ne vous présenterai pas l'intéraction du MVVM Light toolkit avec. Pour ceux qui sont intéressés par plus d'informations, sachez qu'il est possible de créer un nouveau projet MVVM Light depuis Blend et qu'il est possible d'utiliser blend pour lier les éléments de son modèle aux éléments de sa vue.

6.Téléchargement et démo

Vous pouvez télécharger ici les sources du projet : version rar (70 Ko) , version zip (80 Ko).

L'application construite dans le tutoriel est visible à cet emplacement.

7.Conclusion

Ce tutoriel constitue une introduction au design pattern M-V-VM en utilisant le MVVM Toolkit. Sans revenir sur les avantages de ce design pattern, on a pu voir qu'il était relativement facile à adopter grâce aux classes fournies par le toolkit.

Cet article a d'abord présenté rapidement le pattern, puis je me suis attaché à transformer l'application exemple d'un projet de navigation Silverlight pour respecter ce pattern. On a pu voir la création de ViewModel ainsi que le binding de ses propriétés à la vue. Le Service Locator a été abordé pour montrer comment lier le ViewModel au DataContext. On a également pu voir comment utiliser les commandes, comment transformer un événement en commande et comment utiliser le pattern Mediator pour communiquer entre les ViewModel. Une solution a également été présentée pour utiliser M-V-VM avec les transitions d'états. Nous avons enfin exploré les commandes basiques qui manquaient à notre application exemple et avons vu les templates de création de projet fournis par le Toolkit.

J'espère que ce tutoriel a pu vous être utile et vous a donné envie d'utiliser M-V-VM, à travers du MVVM Toolkit.

Remerciements

Je remercie l'équipe Dotnet pour leurs relectures attentives du document et particulièrement Skyounet, Thomas Lebrun et dev01 pour leurs remarques.

Contact

Si vous constatez une erreur dans le tutorial, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.