Microsoft a récemment publié la Beta 1 de Silverlight 3 ainsi qu’une CTP des .Net RIA Services. Je commencerai d’ailleurs par rappeler que si Silverlight 3 RTW est prévu pour l’été prochain, les RIA Services n’ont pour l’instant aucune date de sortie, et sont sujet à des Breaking Changes TRES conséquents (les plans sont d’en faire une sorte d’extension à ADO.Net Data Services).
Toujours est-il que le modèle est très intéressant et que dès aujourd’hui, nous pouvons commencer à travailler sur l’intégration du DomainContext ainsi que des nouveautés propre à Silverlight 3 avec les Design Pattern existants actuellement pour la création d’application LOB (Line of business) avec Silverlight. Aujourd’hui je vais donc présenter quelques “early”-astuces avec Silverlight 3, RIA Services, Unity Application bloc et le pattern MVVM.
Introduction
L’application présentée est très simple : Il s’agit simplement d’une grille de données affichant une liste de personnes avec leur manager avec pour chaque ligne un bouton “Foo”, qui lorsqu’il est cliqué fait apparaitre une MessageBox contenant le nom de la personne :

Les données sont exposées par le serveur via RIA Services. Pour des raisons de simplicité, il n’y a pas de base de donnée, seulement quelques instances en mémoire.
Afin de pouvoir utiliser le pattern MVVM, il m’a aussi fallu implémenter l’interface ICommand ainsi que créer un modèle d’adapters capable d’associer une commande à un contrôle. Je ne détaillerai pas ce code, car il n’est pas du tout spécifique à Silverlight 3, et si vous travaillez en mode MVVM avec Silverlight, vous avez déjà très certainement ce qu’il vous faut pour associer des commandes à des boutons de manière déclarative.
Rappel sur M-V-VM
M-V-VM représente un modèle à 3 responsabilités :
- Le modèle : Il s’agit de la business logique de l’application cliente, qui doit être indépendante de la technologie d’affichage. En général dans une application Silverlight, il s’agit de business entities échangées avec le serveur + logique de validation côté client + business logique côté client + proxy WCF. Dans notre cas, vu le peu de complexité il s’agit essentiellement du DomainContext RIA Services
- La vue : En Silverlight il s’agira d’un UserControl, ou d’une page (depuis Silverlight 3), avec peu (dans l’idéal pas) de code behind.
- Le ViewModel : littéralement le “modèle de la vue”. Il s’agit d’une classe encapsulant le modèle de manière à le présenter à la vue de la manière la plus “bindable” possible. Le ViewModel expose dans certain cas directement des entités du modèle, dans d’autres il les encapsule pour les rendre plus “Silverlight-aware” (par exemple en présentant les propriétés booléennes sous forme de Visibility, en fournissant les ItemsSource des combobox / listbox etc, et en présentant les comportements sous forme de Commandes sur lesquelles la vue peut se “binder”).
Un des enjeux majeurs est de faire en sorte d’avoir des vues en pur XAML, et des ViewModels complètement indépendants des vues qui leur seront associées, afin d’avoir un XAML entièrement modifiable par un designer sans que le comportement ne soit affecté.
Un autre enjeu est la testabilité des applications, et l’isolation des différents composants. Dans notre exemple, toutes les interdépendances seront résolues par injection de dépendance, à l’aide de Unity Application bloc
Introduction à l’injection de dépendance avec Unity
Le but de l’injection de dépendance est de réduire le couplage entre les différents composants d’une application. Ce pattern repose sur un composant central, le conteneur, qui permet de configurer la façon dont les différents composants seront résolus. Voici quelques exemples d’enregistrements de dépendances avec Unity:
IUnityContainer container = new UnityContainer();
// Map l'interface IMainViewModel à l'implémentation MainViewModel.
// Chaque fois que l'on demandera un IMainViewModel à Unity, il créera une instance
// de MainViewModel :
container.RegisterType<IMainViewModel, MainViewModel>();
// La même chose en mode "Singleton" (une seule instance sera créée
// par le conteneur, qui sera renvoyé à chaque fois :
container.RegisterType<IMainViewModel, MainViewModel>(new ContainerControlledLifetimeManager());
// La même chose avec "une instance par thread" :
container.RegisterType<IMainViewModel, MainViewModel>(new PerThreadLifetimeManager());
// Spécifie comment notre DomainContext doit être construit
// (ici en spécifiant un DomainClient de test plutot que le client Rest/HTTP):
container.RegisterType<PeopleContext, PeopleContext>(new InjectionConstructor(new TestDomainClient()));
Ensuite pour résoudre une dépendance on a juste à faire :
var viewModel = container.Resolve<IMainViewModel>();
Le fonctionnement basique de Unity est vraiment très simple. Il y’a pas mal d’autres fonctionnalités comme l’injection de propriétés, l’interception d’appels de méthode (DynamicProxy…) mais elles ne seront pas présentées dans cet article
Configuration de l’application
Dans cette application, nous n’avons qu’un seul ViewModel : MainViewModel. Ce ViewModel implémente IMainViewModel :
namespace SilverlightSample.ViewModels
{
/// <summary>
/// ViewModel exposing data and commands
/// </summary>
public interface IMainViewModel
{
/// <summary>
/// The RIA Services exposed Data
/// </summary>
PeopleContext DomainContext { get; }
/// <summary>
/// A command that needs a Person instance as a parameter
/// </summary>
ICommand FooCommand { get; }
}
}
L’implémentation de IMainViewModel expose le DomainContext RIA afin de fournir à la vue la possibilité de définir des DomainDataSources permettant aux contrôles DataGrid et DataPager de fonctionner correctement. On expose aussi une commande à laquelle la vue pourra se binder (voir les boutons Foo dans le screenshot en début de post). Le DomainContext sera résolu par Unity, tandis qu’une RelayCommand sera créée pour la propriété FooCommand :
public class MainViewModel : IMainViewModel, IInitialize
{
private PeopleContext _domainContext;
private RelayCommand _fooCommand;
IUnityContainer _unityContainer;
public MainViewModel(IUnityContainer unityContainer)
{
_unityContainer = unityContainer;
_domainContext = unityContainer.Resolve<PeopleContext>();
_fooCommand = new RelayCommand(
obj => DoFoo(obj as Person),
obj => obj is Person);
}
…
}
note : MainViewModel implémente aussi IInitialize. Cette interface est déclarée dans ma boîte à outils MVVM et est utilisée par le contrôle ViewModelHelper décrit un peu plus loin.
Au niveau du App.xaml, nous allons donc configurer notre conteneur de dépendances pour IMainViewModel et PeopleContext :
private void Application_Startup(object sender, StartupEventArgs e)
{
IUnityContainer rootApplicationContainer = new UnityContainer();
rootApplicationContainer.RegisterType<IMainViewModel, MainViewModel>(new ContainerControlledLifetimeManager());
rootApplicationContainer.RegisterType<PeopleContext, PeopleContext>(new InjectionConstructor());
//on spécifie explicitement
// l’utilisation du constructeur par défaut
Resources.Add("RootContainer", rootApplicationContainer);
this.RootVisual = new MainPage();
}
note : vous pouvez constater que l’on rajoute le conteneur aux ressources de l’Application afin de pouvoir y accéder de partout (y compris à partir du Xaml en utilisant une {StaticResource}).
Accéder au conteneur Unity depuis la vue
Afin d’accéder au ViewModel via Unity à partir de ma vue, j’ai créé un contrôle ViewModelHelper. Ce contrôle n’a pas de template d’affichage et permet simple d’accéder à un ViewModel enregistré dans un conteneur Unity (un peu à la manière du DomainDataSource qui permet d’accèder aux entités d’un DomainContext).
ViewModelHelper a les propriétés suivantes :
- UnityContainer : le conteneur Unity utilisé pour résoudre le ViewModel
- ViewModelType : le type du ViewModel (par exemple : “SilverlightSample.ViewModels.IMainViewModel, SilverlightSample, Version=1.0.0.0”)
- ViewModelName (optionnel) : utilisé si l’enregistrement de la dépendance est nommé (paramètre optionnel de UnityContainer.RegisterType() et UnityContainer.Resolve())
- InitParam (optionnel) : paramètre à passer à la méthode Initialize si le ViewModel implémente IInitialize.
- ViewModel (lecture seule) : le ViewModel résolu
Dans notre exemple, la vue principale de l’application possède donc un ViewModelHelper :
<sstuff:ViewModelHelper
UnityContainer="{StaticResource RootContainer}"
ViewModelType="SilverlightSample.ViewModels.IMainViewModel,
SilverlightSample, Version=1.0.0.0"
x:Name="vmh" />
Comme Silverlight 3 introduit le Binding inter-contrôles, on peut alors se Binder au ViewModel ainsi exposé :
<ria:DomainDataSource x:Name="dds"
AutoLoad="True"
LoadMethodName="LoadAllPeople"
DomainContext="{Binding ViewModel.DomainContext, ElementName=vmh}"
/>
Comme la vue n’est pas liée à une implémentation particulière de IMainViewModel, on peut tout à fait imaginer créer des tests unitaires vérifiant par exemple la validité des bindings, en proposant un “mock” du ViewModel en lieu et place de l’implémentation normale (c’est d’ailleurs le cas dans la solution liée au post).
note : Pourquoi ViewModelHelper est un contrôle ? Tout simplement pour pouvoir Binder son InitParam ou son ViewModelName à une propriété du DataContext (ce qui est très utile dans des scénarii de vues Master / Details où chaque vue détail est associé à un ViewModel).
Accéder au ViewModelHelper à partir d’une ligne du DataGrid
Une des limites du Binding inter-éléments de Silverlight, est qu’il ne remonte pas au NameScope parent si il ne trouve pas l’UIElement dans le scope courrant. Hors, dans mon cas, la colonne de commande est réalisée grâce à une DataGridTemplatedColumn, basé sur un template qui génèrera donc pour chaque cellule un NameScope différent. Pour palier à ce problème, j’ai écrit un deuxième contrôle non-graphique utilisé simplement pour explorer l’arbre visuel : le VisualTreeDataSource.
Ce contrôle expose 2 propriétés :
- Query : n’importe quel objet implémentant IVisualTreeQuery (cette interface est vraiment très simple à implémenter, regardez les sources pour vous en convaincre)
- Match (lecture seule) : contrôle retourné par la requète. Cette propriété est maintenue à jour à chaque évènement LayoutUpdated
Voici donc le template utilisé pour la colonne de commandes :
<DataTemplate>
<Grid>
<!-- Element Binding do not work accross NameScopes.
RelativeSource markup extension is lacking the FindAncestor mode-->
<!-- So, the VisualTreeDataSource is used to find an element using VisualTree browsing-->
<sstuff:VisualTreeDataSource x:Name="vds">
<sstuff:VisualTreeDataSource.Query>
<sstuff:ComposedVisualQuery>
<!-- first get the MainPage control-->
<sstuff:FindAncestor AncestorType="SilverlightSample.MainPage,
SilverlightSample,Version=1.0.0.0" />
<!-- then find the control named "vmh"-->
<sstuff:FindName TargetName="vmh" />
</sstuff:ComposedVisualQuery>
</sstuff:VisualTreeDataSource.Query>
</sstuff:VisualTreeDataSource>
<!-- Now we can access the ViewModelHelper via the Match DependencyProperty
of the VisualTreeDataSource and bind to exposed command-->
<Button sstuff:Commanding.Command="{Binding Match.ViewModel.FooCommand, ElementName=vds}"
sstuff:Commanding.CommandParameter="{Binding}"
Content="Foo"/>
</Grid>
</DataTemplate>
Et voilà, nous avons une vue qui accède à son ViewModel via Unity et qui n’a aucun CodeBehind.
Conclusion
Voici les différents points que j’ai abordé dans ce post :
- Intégrer les RIA Services au pattern MVVM (dans ma wish-list, j’aimerais bien pouvoir me passer du DomainDataSource dans les vues tout en gardant la possibilité de tri / pagination côté server et donc ne plus être obligé d’exposé le DomainContext complet, qui n’est pas très simple à mocker)
- Utiliser les nouvelles fonctionnalités de Binding de Silverlight 3 pour créer des vues “CodeBehind-less”
- Réduire le couplage entre la vue et le ViewModel, et entre le ViewModel et le modèle afin d’améliorer la testabilité des composants de façon unitaire (Injection de dépendance avec Unity)
- Accéder au conteneur Unity en pur Xaml pour la résolution des ViewModels
Ce que je n’ai pas abordé dans le post mais que vous pouvez trouver dans la solution téléchargeable : l’écriture des tests unitaires de la vue (par mocking du ViewModel) et du ViewModel (par mocking du DomainClient utilisé par le PeopleContext).