Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Thomas Lebrun

Tout sur WPF, LINQ, C# et .NET en général !

Actualités

[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:

image

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:

image

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.

image

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 = orderedModulesListIdea;
  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:

image

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:

image

A l’exécution, les pages sont correctement chargées depuis leur module respectifs, et affichés dans l’application:

image image

Point intéressant, l’historique fonctionne toujours:

image

Et les URLs vous permettent de savoir quel module contient la page demandée:

image

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:

image

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 Wink

 

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 :
Posted: mardi 23 mars 2010 10:11 par Thomas LEBRUN
Classé sous : , , ,

Commentaires

Kikuts a dit :

Salut Thomas !! Ahhhh si tu savais comment je suis content de lire enfin un post sur MEF en français ^^

Super article ! Ta solution correspond plus à mes besoins que ce que j'ai pu trouver sur les blogs de Glenn Block, Jeremy Lickness et bien d'autres crack du genre.

J'ai une ou deux questions auxquelles tu pourras peut être me répondre :

- Est-ce que par exemple, j'ai chargé dynamiquement le module_1.xap, et qu'ensuite je dois télécharger le module_2.xap, je vais retélécharger les dll communes ?

-Est-ce que le shell principal peut être en library caching ? OOB ? Même question pour les modules/widgets comme tu veux :)

-Sais tu comment cela se déroule pour une application OOB en mode desktop : si je charge un xap une fois, sera t il rapatrié sur le disque "C" ou alors il sera rechargé à chaque ouverture du programme ? (par exemple, j'ai chargé le module_1.xap hier, je relance shell_1_main_application, est-ce que je re télécharge le module ou il est déjà dans le clientbin du client ?)

J'espère que mes questions sont claires,

En espérant que tu trouveras le temps de répondre à mes questions, je te souhaite une bonne continuation et pleins d'articles aussi fabuleux que celui ci !

Merci, Nk54

# mai 11, 2010 10:57

Thomas LEBRUN a dit :

@Nk54:

1) Si ton XAP contient des DLLs communes, alors si tu télécharges 2 XAPs, par défaut, tu vas télécharger 2x les DLLs communes. Après, tu peux regarder du coté des fichiers extmap (de Silverlight) pour ca.

2) Pas testé :)

3) Hum... Si tu as téléchargé le Shell en local et que celui-ci a fait appel à un module, alors le module est en local. D'un autre coté, si le Shell télécharge les modules à la volée, ils sont à priori retéléchargé à chaque fois.

A+

# mai 11, 2010 11:15

Kikuts a dit :

Dans la vidéo de Eric Mork :

http://development-guides.silverbaylabs.org/Video/Silverlight-MEF#videolocation_7

jete un coup d'oeil à partir de 8min30 jusqu'à 9min10

Apparemment on peut déjà réduire le poids des xap en mettant la propriété copyLocal à false sur les références. Ce qui fait qu'en mettant les dll communes sur le shell principal (où dans un xap prévu à cet effet) puis en mettant des copyLocal false aux réf des autres xap, on simule un genre de libraryCaching pour les modules si j'ai bien compris :)

Bon reste à voir comment ça fonctionne sous le capot en OOB pour le shell ou les plugin :)

(je pense que si un module peut être OOB, ça va être dure de gérer le cross-xap sans le shell ^^)

Merci de ta réponse rapide !

# mai 11, 2010 11:27

Kikuts a dit :

Je me trompe où les xap ne sont pas chargés dynamiquement mais tous au démarrage ?

Question bête hein ... Si c'est le cas, est-ce difficile de modifier le code pour que ce soit le cas ?

merci

# mai 11, 2010 14:17

Thomas LEBRUN a dit :

Oui, les xap sont chargés au démarrage.

Pour le cas contraire, faut modifier le code et ca doit pas être bien sorcier ;)

# mai 11, 2010 14:32

Kikuts a dit :

Oh c'est sur que lorsqu'on a ton niveau je ne doute pas un instant que ce soit difficile ^^

Mais pour un figurant comme moi ... :) mais j'aime les défies autant que les belles architectures :D

Je suis en train de regarder avec la classe DeploymentCatalog et sa fameuse méthode DownloadAsync()

Si j'arrive à un résultat concret, (mais ne nous emballons pas ^^) je te fais suivre tout ça sur codesource !

# mai 11, 2010 14:38

Kikuts a dit :

Au fait une précision (désolé de flooder ton mur, n'hésite pas à faire le ménage)

On peut installer le shell principal en OOB, mais pas les modules.

Il est possible d'activer le library caching pour le shell et les modules.

Et chose intéressante : il est possible d'installer le shell principal et d'activer le library caching sur les modules !! Ce qui est impossible sans MEF (prism je ne sais pas) car on doit choisir entre OOB et library caching.

J'ai vérifier avec module 1 en library caching et module 2 sans. Le xap de module 1 est vraiment plus petit et fonctionne tout de même en OOB. (Ce qui me laisse penser que les modules sont rechargés à chaque chaque execution du shell.)

# mai 11, 2010 14:59

padpatat a dit :

Malheureusement, tous les xap sont chargés au démarrage de l'application (et avant que la page d'acceuil ne soit visible).

Une implémentation qui charge les xap "à la demande" serait bcp plus utile et performante. (temps de chargement réduit, encombrement réduit de la mémoire)

# décembre 20, 2010 14:10

padpatat a dit :

Dans mon commnentaire précédent, je dis que la page principale de l'application n'est visible qu'après avoir chargé tous les xap. Mea culpa ! En effet, j'ai vérifié avec httpwatch et les download s'exécutent bien de manière asynchrone APRES l'affichage de la page principale. La solution proposée est donc intéressante car elle permet de réduire le temps de réponse de la première page.

Néanmoins, j'ai constaté qu'à chaque reload de l'application les xap sont retéléchargés sans passer par le cache du browser. Quelqu'un a une idée pour éviter de recharger les xap qui sont déjà dans le cache du browser ?

Merci.

# décembre 21, 2010 08:14

SlimH2S a dit :

Merci pour ce billet sur MEF

Par contre impossible de voir le design si on a la déclaration ViewModelHelper dans le xaml.

J'utilise VS2008.

# février 22, 2011 23:58

SlimH2S a dit :

Erreur sur le commentaire précédent j'utilise VS2010.

# février 23, 2011 00:02
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01

- SharePoint Online: Script PowerShell pour supprimer une colonne dans tous les sites d’une collection par Blog Technique de Romelard Fabrice le 11-27-2018, 18:01