Perspective : gestion dynamique des modules et pages d'une application Silverlight 4

Perspective : gestion dynamique des modules et pages d'une application Silverlight 4

Le chargement depuis un serveur Web d'une grosse application Silverlight monolithique peut s'avérer très long, trop long pour l'utilisateur. Il est alors nécessaire de mettre en place un mécanisme de chargement progressif des assemblys de l'application. Le principe consiste à découper l’application en plusieurs paquets, qui sont chargés à la demande en fonction des besoins.

Le Framework MEF (Managed Extensibility Framework), intégré à Silverlight 4, offre une solution à cette problématique au moyen de la classe DeploymentCatalog . Mais son implémentation ne couvre pas certaines fonctionnalités attendues en mode hors navigateur.

Sur la base de travaux similaires réalisés pour Silverlight 2, j'ai donc développé un mécanisme alternatif dans le framework Perspective 2.0 pour Silverlight avec les fonctionnalités suivantes :

  • Chargement de modules d'extension.
  • Chargement des pages de ces modules (pour affichage dans un élément Frame par exemple).
  • Support du chargement tardif (lazy loading).
  • Fonctionnement en mode navigateur et hors navigateur. En mode hors navigateur, les modules d'extension sont chargés depuis le stockage isolé (isolated storage), où ils auront été installés automatiquement.

Ce système d'extension n'est pas générique : il se limite au chargement de modules et de pages. Mais il reste simple et peut servir d'exemple à l'élaboration d'un système plus évolué, en attendant la prochaine version de MEF.

Dans cet article, nous allons présenter la mise en oeuvre du système d'extension de Perspective au moyen d'une application dédiée, très simple, sans aucune fioriture technique ou graphique. Le lecteur intéressé par un exemple plus riche pourra étudier l'application-exemple de Perspective 2.0 , qui définit un bureau graphique composé des différentes extensions et pages, et qui utilise le databinding, les templates et l'architecture MVVM.

Organisation générales des projets

Afin d'optimiser les temps de chargement, les fonctionnalités d'une application Silverlight doivent être réparties en plusieurs paquets : le paquet de l'application principale, ou application hôte, et les paquets des modules d'extension. Le paquet principal définit le point d'entrée général de l'application (classes App et MainPage). Les paquets d'extension sont des regroupements de pages Silverlight qui peuvent être définis en fonction de leur fréquence d'utilisation, de la taille des assemblys référencés ou de critères fonctionnels.

Dans notre exemple, l'application principale est définie par un projet Silverlight nommé SilverlightHost, hébergé au sein d'une application ASP.NET nommée SilverlightHost.Web. Le projet SilverlightHost est configuré pour pouvoir s'exécuter hors navigateur. Mais le projet de démarrage de la solution doit rester l'application Web (le chargement des extensions en mode hors navigateur requiert en effet du code spécifique, que nous détaillerons plus loin).

Chaque paquet sera lui-même constitué d'une application Silverlight et déployé sous la forme d'un fichier .xap regroupant les assemblys du code applicatif et les assemblys de référence. On essaiera de regrouper dans un même paquet les fonctionnalités ayant une dépendance sur des assemblys externes au runtime Silverlight, comme par exemple ceux du Silverlight Toolkit.

Les fichiers App.xaml, App.xaml.cs, MainPage.xaml et MainPage.xaml.cs seront supprimés des paquets d'extension.

Les assemblys références déjà chargés par des paquets précédents pourront ne pas être embarqués dans les fichiers .xap (pour cela, assigner leur propriété "Copie locale" à false).

Dans notre exemple, pour chaque extension souhaitée, nous créons un projet de type Application Silverlight, nommé SilverlightExtension1, SilverlightExtension2, etc. Pour faciliter le déploiement en débogage, chacun de ces projets doit être ajouté dans les applications Silverlight de l'application Web (propriétés du projet SilverlightHost.Web, onglet Applications Silverlight).

Dans chaque extension, nous créons un dossier View dans lequel nous ajoutons des classes de type "Page Silverlight" : Page1.xaml, Page2.xaml, etc. Pour l'exemple, chaque page doit être modifiée de façon à la rendre reconnaissable (par exemple en assignant des couleurs différentes à la propriété Background du Grid).

Pour exploiter le système d'extension de Perspective, les assemblys Perspective.Core.dll et Perspective.Hosting.dll doivent être copiés dans un dossier local pour pouvoir être référencées depuis les projets Silverlight.

Implémentation des extensions

Pour se représenter, chaque paquet d'extension définit une classe nommée Extension, héritée de la classe Extension de Perspective, dont le constructeur instancie des objets PageLink correspondant aux pages du module, regroupés dans la propriété PageLinks. Un objet PageLink expose une propriété Title (titre) et une propriété PageName (nom de la classe). Au préalable, une référence aura été ajoutée sur Perspective.Hosting. Comme cet assembly sera aussi utilisé par l'application principale, sa propriété "Copie locale" doit être initialisée à false.

namespace SilverlightExtension1
{
  public class Extension : Perspective.Hosting.Extension
  {
    private static string _assemblyName = "SilverlightExtension1";
    public override string AssemblyName
    {
      get
      {
        return _assemblyName;
      }
    }
    public Extension()
      : base()
    {
      PageLinks = new List<PageLink>
      {
        new PageLink(this)
        {
          Title = "Page 1",
          PageName = "Page1"
        },
        new PageLink(this)
        {
          Title = "Page 2",
          PageName = "Page2"
        },
        new PageLink(this)
        {
          Title = "Page 3",
          PageName = "Page3"
        },
      };
      ExtensionManager.Current.RegisterAssembly(_assemblyName);
    }
  }
}

Implémentation de l'application hôte

Le projet de l'application principale, SilverlightHost, doit aussi référencer Perspective.Hosting.

Un objet ExtensionManager (obtenu par la propriété statique Current) gère dans sa propriété ExtensionLinks une collection d'objets ExtensionLink associés chacun à un objet Extension.

Les objets ExtensionLink sont définis au moyen d'un fichier Perspective.Hosting.xaml créé dans le répertoire ClientBin de l'application Web SilverlightHost.Web :

<ph:ExtensionLinkCollection
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:ph="clr-namespace:Perspective.Hosting;assembly=Perspective.Hosting">
  <ph:ExtensionLink 
    Title="Extension 1"   
    Package="SilverlightExtension1" />
  <ph:ExtensionLink 
    Title="Extension 2"   
    Package="SilverlightExtension2" />
</ph:ExtensionLinkCollection>

Le rôle d'un objet ExtensionLink est de définir une extension de l'application à charger dynamiquement. Sa propriété Package indique le nom du paquet (qui doit se trouver dans le même dossier que le paquet principal, ClientBin en l'occurence). Le titre de l'extension est défini au moyen de la propriété Title. La propriété Extension sera initialisée automatiquement à l'exécution, quand l'extension sera chargée.

Le fichier Perspective.Hosting.xaml est lu lors de l'événement Startup de l'objet Application, au moyen de la méthode LoadExtensionLinks de l'objet ExtensionManager, qui instancie donc les objets ExtensionLink. L'événement ExtensionLinksLoaded est alors émis, et le contrôle utilisateur MainPage chargé.

private void Application_Startup(object sender, StartupEventArgs e)
{
  ExtensionManager.Current.ExtensionLinksLoaded +=
    (sender1, e1) =>
    {
      this.RootVisual = new MainPage();
    };
  ExtensionManager.Current.LoadExtensionLinks();
}

Le contrôle utilisateur principal, MainPage, est enrichi avec un TabControl, destiné à présenter dans des onglets les extensions et leurs pages, façon ruban, et avec un Frame pour afficher les pages. Au préalable, des références sur System.Windows.Controls et sur System.Windows.Controls.Navigation auront été ajoutées au projet.

<Grid x:Name="LayoutRoot" Background="White">
  <Grid.RowDefinitions>
    <RowDefinition Height="70"/>
    <RowDefinition />
  </Grid.RowDefinitions>
  <swc:TabControl Name="tabControl" />
  <navigation:Frame 
    Name="frame" 
    Grid.Row="1"/>
</Grid>

Dans l'événement Loaded, nous créons dynamiquement un onglet pour chaque extension et un bouton pour chaque page, permettant de charger la page dans le Frame. Un onglet est créé pour chaque objet ExtensionLink. Lorsque l'onglet est sélectionné la première fois (événement TabControl.SelectionChanged), le paquet correspondant est chargé depuis le serveur Web au moyen de la méthode CheckExtension de l'objet ExtensionManager (encapsulée dans une méthode privée de même nom). Lorsqu'une extension est chargée, cet objet émet l'événement ExtensionLoaded. Au sein de l'onglet correspondant à l'extension, un bouton est alors créé pour chaque page au moyen de l'objet PageLink correspondant. La propriété OriginalUri est utilisée pour charger la page dans le Frame lors du clic sur le bouton.

Pour la simplicité de l'exemple, l'association entre l'onglet et l'objet ExtensionLink d'une part, et l'association entre un bouton et l'objet PageLink d'autre part sont gérées au moyen de la propriété Tag du contrôle. En production, on utilisera des objets dédiés.

public partial class MainPage : UserControl
{
  public MainPage()
  {
    InitializeComponent();
  }
  private void UserControl_Loaded(object sender, RoutedEventArgs e)
  {
    CreateExtensionsUI();
  }
  private void CreateExtensionsUI()
  {
    if (ExtensionManager.Current.ExtensionLinks.Count > 0)
    {
      // Chargement tardif des paquets d'extension
      tabControl.SelectionChanged +=
        (sender, e) =>
        {
          var tabItem = (TabItem)tabControl.SelectedItem;
          CheckExtension(tabItem);
        };
      // Création des boutons correspondant aux pages d'une extension
      ExtensionManager.Current.ExtensionLoaded +=
        (sender, e) =>
        {
          var tabItem = (TabItem)tabControl.SelectedItem;
          var buttonPanel = new StackPanel();
          tabItem.Content = buttonPanel;
          buttonPanel.Orientation = Orientation.Horizontal;
          foreach (var pageLink in e.Extension.PageLinks)
          {
            var pageButton = new Button();
            buttonPanel.Children.Add(pageButton);
            pageButton.Content = pageLink.Title;
            pageButton.Margin = new Thickness(2.0);
            pageButton.Tag = pageLink;
            // Chargement de la page dans le Frame
            pageButton.Click += (sender2, e2) =>
            {
              var pageLink2 = (PageLink)((Button)sender2).Tag;
              frame.Source = pageLink2.OriginalUri;
            };
          }
        };
      // Création des onglets pour les différents modules
      for (int i = 0; i < ExtensionManager.Current.ExtensionLinks.Count; i++)
      {
        var extensionLink = ExtensionManager.Current.ExtensionLinks[i];
        var tabItem = new TabItem();
        tabItem.Header = extensionLink.Title;
        tabItem.Tag = extensionLink;
        tabControl.Items.Add(tabItem);
      }
    }
  }
  private void CheckExtension(TabItem tabItem)
  {
    var extensionLink = (ExtensionLink)tabItem.Tag;
    if (extensionLink.Extension == null)
    {
      ExtensionManager.Current.CheckExtension(extensionLink);
    }
  }
}

Si nous exécutons l'application à ce stade, le chargement d'une page échoue. Une InvalidOperationException est déclenchée, avec un message du type "Le type 'SilverlightExtension1.View.Page1', spécifié dans l'attribut x:Class de '/SilverlightExtension1;component/View/Page1.xaml', est introuvable dans les assemblys chargés." ("The type 'SilverlightExtension1.View.Page1', specified in the x:Class of '/SilverlightExtension1;component/View/Page1.xaml' could not be found in any loaded assembly"). En effet, par défaut, un Frame ne peut charger une page que depuis le paquet principal. Pour autoriser le chargement depuis un autre paquet, il faut développer une classe implémentant INavigationContentLoader, et en assigner une instance à la propriété ContentLoader du Frame. La bibliothèque Perspective fournit bien évidemment une classe supportant cette interface, ExtensionContentLoader, que nous utilisons de la façon suivante :

<Grid x:Name="LayoutRoot" Background="White">
  <Grid.Resources>
    <ph:ExtensionContentLoader x:Key="ExtensionContentLoader" />
  </Grid.Resources>
  ...
  <navigation:Frame 
    Name="frame" 
    ...
    ContentLoader="{StaticResource ExtensionContentLoader}"/>
</Grid>

Onglet d'extension

Mode hors navigateur

Un des principaux avantages du système d'extension de Perspective par rapport à MEF est son fonctionnement en mode hors navigateur. Lors de l'installation (passage en mode hors navigateur), la méthode Install de l'objet ExtensionManager copie les extensions (et les fichiers annexes) dans le stockage isolé. A l'exécution en mode hors navigateur, les extensions sont ainsi chargées depuis le stockage isolé. Elles en sont supprimées lors de la désinstallation par la méthode Uninstall de l'objet ExtensionManager. Ces méthodes sont appelées lors de l'événement InstallStateChanged de l'objet Application.

private void Application_Startup(object sender, StartupEventArgs e)
{
  this.InstallStateChanged +=
    (sender1, e1) =>
    {
      switch (this.InstallState)
      {
        case System.Windows.InstallState.Installing:
          ExtensionManager.Current.Install();
          break;
        case System.Windows.InstallState.NotInstalled:
          ExtensionManager.Current.Uninstall();
          break;
      }
    };
  ...
}

Icones

Le système d'extension de Perspective permet d'associer une icone à chaque extension et à chaque page. Les icones peuvent être définies dans des fichiers .png déployés dans le même dossier que les paquets. Les icones des extensions doivent être référencés dans le fichier Perspective.Hosting.xaml au moyen de la propriété IconFile des objets ExtensionLink. Les icones des pages sont définies dans le constructeur de la classe Extension au moyen de la propriété IconFile des objets PageLink. L'application-exemple de Perspective 2.0 présente leur mise en oeuvre.

Icones d'extensions et de pages

Code source

Le code source de l'exemple présenté dans cet article peut être téléchargé ici (Visual Studio 2010, Silverlight 4).

A propos de cet article

Auteur : Olivier Dewit.

Historique :