Perspective : Gestion dynamique des pages d'une application Silverlight

Perspective : Gestion dynamique des pages d'une application Silverlight

Silverlight 2.0 n'intègre pas de fonctionnalités de haut-niveau pour gérer les pages d'une application conséquente. Le chargement d'une grosse application peut s'avérer très long, et il est alors nécessaire de mettre en place un mécanisme de chargement progressif des assemblys de l'application.

Le framework Perspective pour Silverlight offre une solution à cette problématique.

Créé dans le même esprit pédagogique que son grand frère, Perspective pour WPF, ce framework propose notamment les fonctionnalités suivantes :

  • Chargement des paquets, assemblys et pages sur demande,
  • Système simple mais efficace de navigation visuelle parmi les pages de l'application.
  • Gestion des paramètres de l'application.

Organisation des sources de l'application

Afin d'optimiser les temps de chargement, les fonctionnalités d'une application Silverlight doivent être réparties en plusieurs paquets.

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.

Le premier paquet chargé sera le plus léger possible. Son rôle sera d'afficher un écran d'accueil (splash screen) contenant une animation Silverlight et de lancer le chargement de l'interface utilisateur principale de l'application, potentiellement issue d'un autre paquet.

Les fichiers App.xaml et App.xaml.cs seront supprimés des paquets (sauf du premier).

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).

Ainsi, l'application PerspectiveDemo (application-exemple de Perspective pour Silverlight) est organisée de la façon suivante :

  • Paquet PerspectiveDemo.xap : point d'entrée de l'application (App.xaml) et écran d'accueil (Splash.xaml). Référence sur Perspective.Core.Deployment.dll.
  • Paquet PerspectiveDemo.UI.xap : MainPage.xaml, page principale de l'application, destinée à gérer la navigation dans ses différentes pages. Référence sur Perspective.Core.Deployment.dll, Perspective.Wpf.dll et sur Microsoft.Windows.Controls (Silverlight Toolkit).
  • Paquet PerspectiveDemo.UI.Pages.xap : pages contenant les fonctionnalités de l'application.

PackageManager

La classe PackageManager permet de charger dynamiquement un paquet (fichier .xap), ses assemblys et un objet Silverlight (typiquement un UserControl).

Cette classe est contenue dans l'assembly Perspective.Core.Deployment.dll, qui ne pèse que 13 Ko et qui doit être référencé par le paquet principal de l'application. Sa méthode LoadObjectAsync() permet de charger un objet Silverlight depuis un paquet donné. Elle est utilisée dans l'événement Startup de l'objet Application (fichier App.xaml.cs) pour charger la page principale de l'application, après affichage de l'écran d'accueil (splash screen). Cette méthode est asynchrone. Elle prend comme arguments l'espace de noms et le nom de la classe. L'événement ObjectStateChanged est émis quand l'objet est chargé.

private void Application_Startup(object sender, StartupEventArgs e)
{
    // Chargement de l'écran d'accueil
    this.RootVisual = new ContentControl();
    (this.RootVisual as ContentControl).HorizontalContentAlignment = HorizontalAlignment.Stretch;
    (this.RootVisual as ContentControl).VerticalContentAlignment = VerticalAlignment.Stretch;
    (this.RootVisual as ContentControl).Content = new Splash();
...
    // Chargement de la page principale
    PackageManager mainPackageManager = new PackageManager();
    mainPackageManager.ObjectStateChanged += new EventHandler<ObjectInfoEventArgs>(mainPackageManager_ObjectStateChanged);
    mainPackageManager.LoadObjectAsync("PerspectiveDemo.UI", "MainPage");
}
void mainPackageManager_ObjectStateChanged(object sender, ObjectInfoEventArgs e)
{
    // Affichage de la page principale
    (this.RootVisual as ContentControl).Content = e.ObjectInfo.Instance;
}

Navigation parmi les pages de l'application

Les fonctionnalités de navigation offertes par Perspective sont les suivantes :

  • Une seule page peut être affichée à la fois, dans un conteneur visuel qui affiche aussi la liste des pages ouvertes. Chaque élément de cette liste contient un bouton qui permet de décharger la page.
  • Les pages sont des UserControl Silverlight standard. Il n'est pas nécessaire d'implémenter d'interface ou de mettre en oeuvre un mécanisme particulier à leur niveau.
  • La navigation est synchronisable avec le système de menu (par exemple un Treeview).

2 types de conteneur visuel sont fournis :

  • ComboFrame : conteneur gérant la liste des pages dans une ComboBox.

ComboFrame

  • ListFrame : conteneur gérant la liste des pages dans une ListBox (ce qui est proche d'un système à onglets, mais plus adapté à un grand nombre d'éléments).

ListFrame

Ces classes disposent d'une méthode LoadPageAsync() et d'un événement PageStateChanged qui encapsulent les fonctionnalités correspondantes (LoadObjectAsync() et ObjectStateChanged) d'un PackageManager. L'événement PageStateChanged est également émis lorsque la page redevient active ou lorsqu'elle est déchargée.

MainPage.xaml doit donc contenir l'un de ces 2 conteneurs et un système de menu :

<UserControl x:Class="PerspectiveDemo.UI.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:pc="clr-namespace:Perspective.Wpf.Controls;assembly=Perspective.Wpf"
    xmlns:mwc="clr-namespace:Microsoft.Windows.Controls;assembly=Microsoft.Windows.Controls">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="3*"/>
        </Grid.ColumnDefinitions>
        <mwc:TreeView Name="tvMenu" Grid.Column="0">
            <mwc:TreeViewItem Header="Page1" IsExpanded="True" Selected="TreeViewItem_Selected"/>
            <mwc:TreeViewItem Header="Page2" IsExpanded="True" Selected="TreeViewItem_Selected"/>
            <mwc:TreeViewItem Header="Page3" IsExpanded="True" Selected="TreeViewItem_Selected"/>
        </mwc:TreeView>
        <Border Grid.Column="1" BorderBrush="Red" BorderThickness="4" CornerRadius="4">
            <pc:ComboFrame 
                Name="frame" 
                Margin="5"
                PageStateChanged="Frame_PageStateChanged" />
        </Border>
    </Grid>
</UserControl>

Lorsqu'un élément du menu est sélectionné, la page correspondante est chargée depuis un paquet :

private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
    TreeViewItem tvi = (TreeViewItem)sender;
    string namespaceBase = "PerspectiveDemo.UI.Pages";
    frame.LoadPageAsync(
        namespaceBase, 
        tvi.Header.ToString(), 
        tvi.Header.ToString());
}

MainPage

Une clé peut être passée en dernier argument à la méthode LoadPageAsync() pour identifier la page. La clé est utilisée comme titre de la page dans la liste du conteneur. Un appel d'une page avec une clé déjà utilisée déclenche l'événement PageStateChanged avec un argument ObjectState de valeur Activated. Dans ce cas en effet, l'objet existant est activé, c'est à dire réaffiché. Le code de l'application exemple de perspective répond alors à cet événement en resynchronisant le TreeView avec la page active.

Si la clé passée est nulle, l'identifiant correspondra au nom de la classe.

Pour permettre l'instanciation multiple d'une même classe, il faut passer à chaque appel une clé différente :

private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
    TreeViewItem tvi = (TreeViewItem)sender;
    string namespaceBase = "PerspectiveDemo.UI.Pages";
    frame.LoadPageAsync(
        namespaceBase, 
        tvi.Header.ToString(), 
        String.Format("{0} - {1}", tvi.Header.ToString(), DateTime.Now));
}

Multiinstanciation

Gestion des paramètres de l'application

La classe ParametersHelper facilite la gestion des arguments passés à l'application Silverlight.

Les paramètres personnalisés du plug-in peuvent être spécifiés dans la chaîne initParms de l'élement HTML object, ou dans la propriété InitParameters du contrôle serveur ASP.NET Silverlight. Ces paramètres ne peuvent être lus dans l'application Silverlight que sur l'événement Startup de l'objet Application. La méthode LoadInitParametersFrom() de la classe ParametersHelper permet alors de les récupérer pour les tenir à disposition tout au long de la durée de vie de l'application dans la propriété dictionnaire InitParameters.

private void Application_Startup(object sender, StartupEventArgs e)
{
...
    ParametersHelper.LoadInitParametersFrom(e.InitParams);
...
}

Des paramètres peuvent aussi être spécifiés dans l'URL.

La méthode ReadParameterValueFromUrlFirst() de ParametersHelper lit une valeur de paramètre en la recherchant d'abord dans l'URL, puis dans le dictionnaire InitParameters. Ainsi, des paramètres codés en dur dans le HTML peuvent être surchargés dans l'URL.

Des noms de paramètres prédéfinis sont spécialement gérés par ParametersHelper pour en faciliter la lecture au moyen de propriétés utilisant ReadParameterValueFromUrlFirst() :

  • ns (propriété NamespaceValue) : espace de noms de base du paquet,
  • page (propriété PageValue) : nom de la classe de la page, incluant l'espace de noms partiel à l'intérieur de l'assembly,
  • key (propriété ElementKeyValue) : clé de l'objet (facultative).

Ainsi, l'URL http://localhost:49658/PerspectiveDemoTestPage.aspx?page=Page1&ns=PerspectiveDemo.UI.Pages permet d'afficher directement la page spécifiée (Page1.xaml) grâce au code suivant dans le constructeur de MainPage :

public MainPage()
{
    InitializeComponent();
    string partialClassName = ParametersHelper.PageValue;
    string namespaceBase = ParametersHelper.NamespaceValue;
    string elementKey = ParametersHelper.ElementKeyValue;
    if (!String.IsNullOrEmpty(partialClassName)
        && !String.IsNullOrEmpty(namespaceBase))
    {
        frame.LoadPageAsync(namespaceBase, partialClassName, elementKey);
    }
}

A propos de cet article

Auteur : Olivier Dewit.

Historique :

  • 25 janvier 2009 : 1ère publication (Perspective pour Silverlight, version 0.9)