[Silverlight / MEF] Développer un système de plugins pour une application Silverlight avec le Microsoft Extensibility Framework
Introduction
MEF (Microsoft Extensibility Framework) est un framework proposé par Microsoft permettant de développer des applications modulaires, composées de plugins qu’il est possible de charger/décharger à la demande de l’utilisateur, en fonction du contenu d’un répertoire, etc.
Les applications modulaires sont de plus en plus répandues car elles offrent plusieurs avantages:
- Simplicité du code de l’application principale, qui ne sert que de “lanceur” pour les modules
- Modularité
- Extensibilité
- Etc.
Lorsque l’on travaille avec une application “client lourd” (WPF, WindowsForms), ce type d’applications est très simple à réaliser: on dépose ses extensions dans un répertoire physique, l’application est notifée et se met à jour automatiquement.
Cependant, dans le cas de Silverlight, c’est un peu plus compliqué. En effet, Silverlight s’exécutant sur le poste client, mais dans une sandbox (contexte sécurisé), il est impossible de faire un drag/drop de ses extensions sur un répertoire de la machine. Mais vous avez tout à fait la possibilité de mettre vos extensions dans le même répertoire d’où est téléchargée l’application Silverlight (le répertoire ClientBin) puis, avec un peu de code, vous faîtes en sorte de télécharger vos plugins et vous les exécutez ! Ce mécanisme est déjà possible grâce à Prism mais il s’agit d’un framework qui peut paraitre démesuré ou trop complexe à appréhender. Je vous propose donc, au travers de ce post, de vous apprendre comment créer votre propre “framework” de développement d’applications Silverlight modulaires.
Mise en place des bases
Pour commencer, nous allons créer un projet de base, qui contiendra tous les éléments (interfaces, attributs, etc.) qui seront commun aux différents projets de notre solution:
La classe ExportModulePageAttribute est en fait un attribut, que l’on appliquera sur chacun de nos modules, et qui servira à identifier nos modules:
1: [MetadataAttribute]
2: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
3: public class ExportModulePageAttribute : ExportAttribute, IModuleMetadata
4: {
5: public ExportModulePageAttribute()
6: : base(typeof(Page))
7: {
8: }
9:
10: public string XapFilename { get; set; }
11: public string NavigateUri { get; set; }
12: public string Title { get; set; }
13: public int Position { get; set; }
14: }
Création des plugins/modules
Un plugin, vu par MEF, est simplement un élément décoré avec l’attribut ExprtAttribute. Dans notre cas, nous allons créer 2 modules:
Chaque module est en fait une application Silverlight comprendant 1 ou plusieurs Page, que l’on a décoré avec l’attribut créé précédemment:
1: [ExportModulePage(XapFilename = "TL.Silverlight.Demos.Extensibility.Module1.xap", NavigateUri = "/Welcome", Title = "Welcome Page", Position = 1)]
2: public partial class WelcomePage : Page
3: {
4: public WelcomePage()
5: {
6: InitializeComponent();
7: }
8:
9: // Executes when the user navigates to this page.
10: protected override void OnNavigatedTo(NavigationEventArgs e)
11: {
12: }
13: }
Chacun de ces modules étant indépendant, il est possible d’utiliser toutes les techniques de programmation que l’on connait (pattern MVVM, injection de dépendances, etc.). Une fois les modules créés, il suffit de les déposer dans le répertoire ClientBin du serveur. Cependant, il reste encore à faire en sorte de pouvoir récupérer ces modules, faire la composition MEF et utiliser notre application.
Création du Shell
Le Shell, c’est l’application Silverlight qui va “hoster” les différentes modules qui auront été écrits, afin de pouvoir les exécuter. Pour cela, il va être nécessaire de se créer un petit loader:
1: public class ModulesLoader : IModulesLoader, IPartImportsSatisfiedNotification
2: {
3: #region Events
4:
5: public event NotifyOnModulesAvailabilityHandler NotifyOnModulesAvailability;
6:
7: #endregion
8:
9: #region Public Contructor
10:
11: public ModulesLoader()
12: {
13: DeploymentCatalogService.Instance.Initialize();
14:
15: CompositionInitializer.SatisfyImports(this);
16:
17: this.LoadModules();
18: }
19:
20: #endregion
21:
22: #region Properties
23:
24: [ImportMany(AllowRecomposition = true)]
25: public Lazy<Page, IModuleMetadata>[] Modules { get; set; }
26:
27: #endregion
28:
29: #region IPartImportsSatisfiedNotification Members
30:
31: public void OnImportsSatisfied()
32: {
33: var handler = this.NotifyOnModulesAvailability;
34: if (handler != null)
35: {
36: handler(this.Modules);
37: }
38: }
39:
40: #endregion
41:
42: #region Private Methods
43:
44: private void LoadModules()
45: {
46: var wc = new WebClient();
47: wc.OpenReadCompleted += (s, e) =>
48: {
49: var streamInfo = e.Result;
50:
51: var xElement = XElement.Load(streamInfo);
52:
53: var modulesList = from m in xElement.Elements("ModuleInfo")
54: select m;
55: if (modulesList.Any())
56: {
57: foreach (var module in modulesList)
58: {
59: var moduleName = module.Attribute("XapFilename").Value;
60:
61: DeploymentCatalogService.Instance.AddXap(moduleName);
62: }
63: }
64: };
65: wc.OpenReadAsync(new Uri("ModulesCatalog.xml", UriKind.Relative));
66: }
67:
68: #endregion
69: }
On constate plusieurs chose sur cette classe. Tout d’abord, on fait appel, à plusieurs moments, à une classe nommée DeploymentCatalogService. Il s’agit simplement d’une classe qui se charge de télécharger un module (fichier xap) et de l’ajouter à un DeploymentCatalog si celui-ci n’est pas déjà présent dedans. Ainsi, on dispose d’un container possédant chacun des modules exportés et avec lesquels on peut travailler. De plus, la classe implémente l’interface IPartImportsSatisfiedNotification, offerte par MEF, qui permet d’être notifier lorsque la composition est terminée. Maintenant, il reste nécessaire de savoir quels seront les modules qui devront être chargés ! Et pous cela, on utilise la méthode LoadModules qui télécharge un fichier nommé ModulesCatalog.xml, contenant des informations sur les xap. Une fois le nom des fichiers récupérés, on les ajoute au DeploymentCatalog à l’aide de la méthode AddXap de la classe DeploymentCatalogService.
A présent, il est nécessaire d’utiliser notre loader de modules. On peut, par exemple, l’utiliser dans un de nos ViewModels:
1: var modulesLoader = UnitySingleton.RootContainer.Resolve<IModulesLoader>();
2: modulesLoader.NotifyOnModulesAvailability += modules =>
3: {
4: this.ModulesList = new ObservableCollection<Module>();
5:
6: var orderedModulesList = modules.OrderBy(m => m.Metadata.Position).ToList();
7:
8: for (int i = 0; i <= orderedModulesList.Count() - 1; i++)
9: {
10: var module = orderedModulesList
;
11:
12: this.ModulesList.Add(new Module { Uri = module.Metadata.XapFilename + module.Metadata.NavigateUri, Title = module.Metadata.Title, IsLast = i == modules.Count() - 1 });
13: }
14: };
Qui, lui-même, sera bindé à notre interface graphique Silverlight:
1: <ItemsControl ItemsSource="{Binding ModulesList}">
2: <ItemsControl.ItemsPanel>
3: <ItemsPanelTemplate>
4: <StackPanel Orientation="Horizontal" />
5: </ItemsPanelTemplate>
6: </ItemsControl.ItemsPanel>
7: <ItemsControl.ItemTemplate>
8: <DataTemplate>
9: <StackPanel Orientation="Horizontal">
10: <HyperlinkButton Style="{StaticResource LinkStyle}"
11: NavigateUri="{Binding Uri}"
12: TargetName="ContentFrame"
13: Content="{Binding Title}" />
14:
15: <Rectangle Visibility="{Binding IsLast, Converter={StaticResource BooleanToVisibilityConverter}, ConverterParameter=Inverse}"
16: Style="{StaticResource DividerStyle}" />
17: </StackPanel>
18: </DataTemplate>
19: </ItemsControl.ItemTemplate>
20: </ItemsControl>
A l’exécution, le loader de module récupère le contenu du fichier ModulesCatalog.xml, télécharge les XAPs, les ajoute au DeployementCatalog et déclenche l’évènement qui lui permet d’indiquer que les modules sont chargés. Du coup, on voit bien le nom de nos modules apparaitre dans notre application:
Affichage de la page demandée
Arrivé là, on pourrait se dire que tout est terminé. Mais il reste toujours un problème. A l’heure actuelle, lorsque je clique sur un des éléments de la barre de navigateur, Silverlight affiche la page correspondante, page qui se trouve dans le répertoire Views de l’application principale. Hors, dans le cas présent, les pages ne se trouvent pas dans l’application principale mais dans les plugins ! Comment faire donc pour indiquer au runtime Silverlight l’emplacement de nos pages ? Et bien cela passe par une des nouveautés, pas ou peu connue, qui concernce l’extensibilité du modèle de navigation de Silverlight. Si vous voulez en savoir plus sur le sujet, je vous recommance le blog de David Poll (http://www.davidpoll.com) qui en parle très longuement ! Dans notre cas, on va récupérer le MEFContentLoader mis à disposition par David, afin de permettre de charger les pages depuis les modules exportés/importés par MEF:
A l’exécution, les pages sont correctement chargées depuis leur module respectifs, et affichés dans l’application:
Point intéressant, l’historique fonctionne toujours:
Et les URLs vous permettent de savoir quel module contient la page demandée:
Si l’on veut désactiver un module, il suffit de mettre en commentaire la ligne correspondant dans le fichier ModulesCatalog.xml et de relancer l’application:
Conclusions
Avec cet article, vous avez pû avoir un petit aperçu de ce qu’il était possible de faire en mélangeant un peu de MEF avec du Silverlight. Bien sur, des points d’optimisations seraient à envisager mais cela vous permet d’avoir une première idée 
Les sources de l’article sont ici: http://morpheus.developpez.com/silverlight/TL.Silverlight.Demos.Extensibility.zip
A+
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :