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écessaires 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 ça 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.
Première chose, compilons cette application et lançons-la pour vérifier que tout va bien.
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 )
- extraire le contenu du zip dans le répertoire.
Tout a é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 :
3.2.3.Créer le modèle▲
Dans un premier temps, ouvrons le fichier Views/Home.xaml, nous pouvons voir notamment :
<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.
Et rajoutons une classe NavigationService :
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
public
class
GetNavigationNodeCompletedEventArgs :
EventArgs
{
public
NavigationNode Result {
get
;
set
;
}
}
et :
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 :
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 :
<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 :
DataContext="{Binding HomeViewModelStatic, Source={StaticResource Locator}}"
Il ne restera plus qu'à binder les deux textbox au ViewModel :
<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 :
Faisons pareil maintenant pour la Vue About et créons son viewmodel :
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 :
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 :
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 :
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 :
DataContext="{Binding AboutViewModelStatic, Source={StaticResource Locator}}"
et à faire le binding des valeurs :
<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 :
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 :
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 :
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 :
<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 instanciée cette ErrorWindow. Dans le code behind de MainPage.xaml, on voit :
// 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 :
// 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éée pour l'occasion et contient les messages que l'on va utiliser pour communiquer :
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 :
SingletonMediator.
Instance.
Register
(
this
);
et on pourra déclarer une méthode qui sera appelée lorsqu'on recevra le message
[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 :
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 sûr que si, grâce à la behavior EventToCommand.
Rajoutons deux nouveaux namespaces dans MainPage.xaml :
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 :
<
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.
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 ?
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 :
<
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 :
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 fournit une 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.
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 :
<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 :
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 :
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 :
<TextBlock
Style
=
"{StaticResource ContentTextStyle}"
Text
=
"{Binding CurrentTime}"
/>
Avec dans le ViewModel :
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 :
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 clics…
Pour relier la commande au clic du bouton, le MVVM Light Toolkit nous fournit une propriété dépendante, spécifique au bouton. On l'utilisera de cette façon :
<
Button cmd:
ButtonBaseExtensions.
Command=
"{Binding ChangeCommand}"
Content=
"Mettre à jour l'heure"
Width=
"150"
Height=
"35"
/>
Ainsi, en réexécutant l'application, chaque clic provoquera le rafraichissement de l'heure.
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) :
<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 :
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 :
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.
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 :
Si vous créez un nouveau projet avec ce template, on retrouve une application avec l'arborescence suivante :
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 :
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'interaction 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 projets 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 tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.