Perspective : dynamic modules and pages management of a Silverlight 4 application

Perspective : dynamic modules and pages management of a Silverlight 4 application

Loading a large Silverlight application from a Web server can be very long. So it is necessary to establish a mechanism for application assemblies progressive loading. The principle consists in dividing the application into multiple packages that are loaded on demand.

MEF, the Managed Extensibility Framework, integrated with Silverlight 4, offers a solution to this problem using the DeploymentCatalog class. But its implementation does not cover some features expected in Out-Of-Browser mode (OOB).

Based on similar work for Silverlight 2, I developed an alternative mechanism in the Perspective 2.0 for Silverlight framework with the following features :

  • Plug-ins loading.
  • Plug-ins pages loading (i.e. for display in a Frame element).
  • Lazy loading support.
  • Works in and outside the browser. Out-Of-Browser plug-ins are loaded from the isolated storage, where they have been installed automatically.

This extension system is not generic: it is limited to loading modules and pages. But it remains simple and can serve as an example developing a more sophisticated system, while waiting for the next MEF version.

In this article, we will present the implementation of the Perspective extension system using a dedicated application, very simple, without any technical or graphical flourish. Readers interested in a richer example may study the example application of Perspective 2.0 that defines a graphical desktop composed of different extensions and pages, and uses databinding, templates and MVVM architecture.

General projects organization

To optimize the loading time, the features of a Silverlight application should be divided into several packages: main application package, or host application, and packages of extension modules. The main package defines the general entry point of the application (classes App and MainPage). Extension packages are collections of Silverlight pages that can be defined according to their frequency of use, to the size of the referenced assemblies or to functional criteria.

In our example, the main application is defined by a Silverlight project named SilverlightHost, housed within an ASP.NET application named SilverlightHost.Web. The SilverlightHost project is configured to run outside the browser. But the solution's start-up project should remain the Web application (loading extensions out of browser indeed requires specific code, which we will detail below).

Each package is itself composed of a Silverlight application and deployed as an .xap file grouping application code assemblies and reference assemblies. We will try to regroup in a single package the features with a dependency on assemblies external to the Silverlight runtime, such as those of the Silverlight Toolkit.

The App.xaml, App.xaml.cs, MainPage.xaml and MainPage.xaml.cs will be deleted from the extension projects.

References assemblies already loaded by previous packages can not be embedded in .xap files (for this, assign their "Copy Local" property to false).

In our example, for each wanted extension, we create a Silverlight Application project type, named SilverlightExtension1, SilverlightExtension2, etc. To facilitate the debugging deployment, each project must be added in the Silverlight applications parameter of the Web application (SilverlightHost.Web project properties, Silverlight Applications tab).

In each extension, we create a View folder in which we add classes of type "Silverlight Page" : Page1.xaml, Page2.xaml, etc. In our case, each page must be amended to make it recognizable (for example by assigning different colors to the Background property of the Grid).

To use the Perspective extension system, assemblies Perspective.Core.dll and Perspective.Hosting.dll must be copied to a local folder to be referenced from the Silverlight projects.

Extensions implementation

To represent itself, each extension project defines a class named Extension, inherited from the Extension class of Perspective, whose constructor instantiates PageLink objects corresponding to the module pages, grouped in the PageLinks prperty. A PageLink object exposes a Title property and a PageName property (class name). Previously, a reference should have been added to Perspective.Hosting. As this assembly is also used by the main application, the property "Copy Local" must be initialized to 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);
    }
  }
}

Host application implementation

The main application's project must also refrence Perspective.Hosting.

An ExtensionManager object (obtained by the Current static property) manages in his ExtensionLinks property a collection of ExtensionLink objects each associated with an Extension object.

ExtensionLink objects are defined using a Perspective.Hosting.xaml file created in the ClientBin directory of the SilverlightHost.Web application :

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

The role of an ExtensionLink object is to define an extension of the application to load dynamically. His Package property indicates the package name (which must be in the same folder as the main package, ClientBin). The title of the extension is defined using the Title property. The Extension property will be automatically initialized at runtime, when the extension will be loaded.

The Perspective.Hosting.xaml file is read during the Startup event of the Application object, using the LoadExtensionLinks method of the ExtensionManager object, which then instantiates ExtensionLink objects. The ExtensionLinksLoaded event is then fired, and the MainPage UserControl is loaded.

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

The main UserControl, MainPage, is enriched with a TabControl, intended to present as tabs the extensions and their pages, like a ribbon, and with a Frame to display the pages. Previously, references to System.Windows.Controls and System.Windows.Controls.Navigation should have been added to the project.

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

In the Loaded event, we dynamically create a tab for each extension and a button for each page to load the page in the frame. A tab is created for each ExtensionLink. When the tab is selected the first time (TabControl.SelectionChanged event), the package is loaded from the Web server using the CheckExtension method of the ExtensionManager object (encapsulated in a private method of the same name). When an extension is loaded, this object fires the ExtensionLoaded event. Within the tab of the extension, a button is created for each page using the corresponding PageLink object. The OriginalUri property is used to load the page in the frame when clicking on the button.

For simplicity reasons in this article, the association between the tab and the ExtensionLink object on one hand, and the association between a button and the PageLink object on the other hand, are managed using the Tag property of control. In real life, use dedicated objects instead.

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)
    {
      // Extension packages lazy loading
      tabControl.SelectionChanged +=
        (sender, e) =>
        {
          var tabItem = (TabItem)tabControl.SelectedItem;
          CheckExtension(tabItem);
        };
      // Creating buttons for the pages of an 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;
            };
          }
        };
      // Creating tabs for different 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);
    }
  }
}

If we execute the application at this stage, the page load fails. An InvalidOperationException is raised, with a message like "The type 'SilverlightExtension1.View.Page1', specified in the x:Class of '/SilverlightExtension1;component/View/Page1.xaml' could not be found in any loaded assembly". Indeed, by default, a Frame can not load a page from the main package. To allow loading from another package, one must develop a class implementing INavigationContentLoader, and assign an instance of it to the Frame's ContentLoader property. The Perspective library obviously provides a class that supports this interface, ExtensionContentLoader, that we can use like this :

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

Extension tab

Out-Of-Browser mode

From MEF, a major advantage of the Perspective extension system is that it works fine in Out-Of-Browser mode. During installation (switching Out-Of-Browser mode), the Install method of the ExtensionManager object copy the extensions (and related files) in isolated storage. So at runtime in Out-Of-Browser mode, extensions are loaded from isolated storage. They are removed when uninstalling using the Uninstall method of the ExtensionManager object. These methods are called during the InstallStateChanged event of the Application object.

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;
      }
    };
  ...
}

Icons

The Perspective extension system can associate an icon for each extension and each page. The icons can be defined in .png files deployed in the same folder as the packages. Icons for extensions must be referenced in the Perspective.Hosting.xaml file through the IconFile property of the ExtensionLink objects. The icons of the pages are set in the constructor of the Extension class through the IconFile property of PageLink objects. The example application of Perspective 2.0 shows their usage.

!img!Extensions and pages icons!PerspectiveDemo.png

Source code

The source code of the example presented in this article can be downloaded here (Visual Studio 2010, Silverlight 4).

About this article

Author : Olivier Dewit.

History :