Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Building Modular Silverlight Applications

0.00/5 (No votes)
5 Feb 2009 1  
This article presents a flexible and practical reusable control that is essential to modular Silverlight applications. It helps to improve a large Silverlight application's composite structure and run time performance.

Introduction

Lazy loading code packages at runtime (load functional code modules/assemblies on demand) in a medium to large Silverlight RIA can reduce the initial downloading size. This ensures snappier up and running performance and cleaner code structures. The Silverlight Module approach (A Silverlight Module is a collection of features that are bound together within a binary package file. It is different from a .NET Module which is a compilation unit.) presented in this article enables developers to work in parallel on different feature groups. It also promotes the creation of physical and logical modularized applications which can be separately loaded and subsequently rendered at runtime on demand.

There are several articles that discuss the mechanisms for on demand package delivery, including Silverlight how to: On-demand assembly deployment, Downloading Content on Demand, and Managing Dynamic Content Delivery in Silverlight (by Dino Esposito). The modular Silverlight application approach presented here leverages the same low level mechanisms; it also has a built-in mechanism for error handling, status messaging, auto-rendering, and other practical integration capabilities, so developers can focus more on the module logic rather than the plumbing.

You can take a loot at the sample modular Silverlight application, full source code is included.

What is a Silverlight Module

The "module" concept is borrowed from Adobe Flex. It is a standalone deployable binary package that is loaded into the main application on demand. A salient characteristic of the module is that it gets downloaded only when needed. The typical module runs in the context of an application, and a module in turn can reference other modules as sub-modules.

Modularization becomes necessary when a big monolithic application causes undesired latency at start-up or run time. This usually happens because of high memory usage, and often exacerbates with time as more and more features are packed in. Since the initial downloading size becomes bigger, both design time maintainability and run time performance suffer.

Modularity can reduce undesired latency at start-up time, it allows the application pieces (modules) to be brought down from the network and subsequently loaded in logical chunks (as modules). Typically, an application does not require all of its features enabled at start time. Furthermore, not all users require all the features of the application during a session.

Modules improve the high level cohesion of related features in an application through encapsulation of related features. A module is loaded only when the user needs to interact with it. Some features are needed conditionally: the code dynamically determines which modules to load based on business logics.

At design time, modules bring cohesive features together. As they are separated out from the main application, different team members can develop and test different modules in parallel. When rebuilding the application, only the changed modules need to be recompiled rather than the whole application.

At run time, modularization promotes shorter start up time as the application XAP is smaller. Memory usage improves, since only the referenced modules are loaded. Performance is snappier, and browser cache can be leveraged to re-load modules.

A Silverlight module has all the characteristics and benefits listed above. It is created as a Silverlight application without an application entry point. Therefore, it cannot run independent of an application. The module is compiled into a XAP package.

Tips on building a Silverlight Module

The key to building a modular Silverlight application is to use the SilverModule control provided in this article. We recommend the developer to follow certain guidelines to build a modular Silverlight application:

  • Break the application into modules: factors to consider would include module size, within module cohesions, reusability, business requirements, etc.
  • Every module requires a static reference to the SilverModule assembly as a module loader, visual renderer, and error handler only when it needs to load sub-modules at runtime.
  • Every module would have its exposed custom type to be derived from UserControl. The derived custom type can be designed to implement a common interface as needed. Practically, the module interface would be application specific. SilverModule can be easily extended for a common interface.
  • A module needs to be created as a regular Silverlight application (the application entry point can be removed), and Visual Studio will compile it into the XAP format. Note that a XAP file is essentially a ZIP file. It is usually smaller than an uncompressed assembly file.

To dynamically load a Silverlight Module, the calling code fragment creates an instance of the SilverModule and sets its ModuleName to the name of the Module being loaded. The SilverModule control additionally provides flexibility when more granular controls are desired, like the module URL, custom type name, assembly name, etc. From the application perspective, the SilverModule control serves the following functionalities:

  • It is instantiate-able in XAML as a regular User Control.
  • It has built-in logic to infer default module properties based on the Module name.
  • It automatically downloads the binary XAP packages from a remote server.
  • It reports download progress and errors (if any).
  • It auto-extracts the assembly from the downloaded binary stream, instantiates the custom type from the module, and automatically adds the instance to its visual container causing it to render.
  • It handles and reports errors during downloading, extracting, instantiating, and rendering.
  • It cleans up the visual container when a new module needs to replace the existing one.
  • It provides the same mechanism to handle sub-modules (when a module needs to load other modules into itself).

We have provided a Visual Studio 2008 SP1 solution with this article to demonstrate how the SilverModule control helps to build modular Silverlight applications.

Using the SilverModule control

SilverModule is a Silverlight User Control instantiate-able by XAML. It employs the WebClient asynchronous method to download the XAP binary package, uses StreamResourceInfo to extract the assembly, and instantiates the custom type from the loaded module via Reflection. These mechanisms have been discussed in Managing Dynamic Content Delivery In Silverlight, Part 1. We will focus more on the enhanced parts that SilverModule added to facilitate easier lazy-loading and provide error handling and progress reporting.

SilverModule is created as a Silverlight Class Library that can be statically linked to another library, module, or application. When the library is added to an application's reference and the assembly name space is specified, it can be easily instantiated in XML:

Fig.1a. Instantiate the SilverModule control in XAML
<UserControl x:Class="SilverModuleDemo.Page"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:ext="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls"
    xmlns:sm="clr-namespace:SilverModule;assembly=SilverModule"
    Loaded="UserControl_Loaded">
    <!--other XAML markup is omitted-->
    <sm:SilverModule x:Name="moduleLoader" 
      ModuleName="{Binding Path=SelectedModuleName}" />
</UserControl>

In the simplest case, only the ModuleName property needs to be set. The code-behind for SilverModule will try to infer the module's URL, assembly name, and custom type name from ModuleName by assuming they are all the same (with the difference of extension name and namespace name; see the downloaded code for details). For example, in the accompanied demo project, the module name is "SilverModuleTestOne". The module URL is resolved to be "SilverModuleTestOne.xap". The assembly name inside the module is assumed to be "SilverModuleTestOne.dll". And, the custom type name is inferred from the module name to be "SilverModuleTestOne.SilverModuleTestOne". On the other hand, those properties can all be set in XAML too (see Figure 1b). They are taken care of by all the four corresponding Dependency Properties inside the SilverModule control.

Fig 1b. Set the SilverModule control's relative path, assembly name, and type name in XAML
<!--other XAML markup is omitted-->
<sm:SilverModule x:Name="moduleLoader" ModuleRelativePath="." 
   ModuleAssemblyName="SilverModuleTestOne.dll"
   ModuleTypeName="SilverModuleTestOne.SilverModuleTestOne"
   ModuleName="{Binding Path=SelectedModuleName}" />

To use the SilverModule control in your project to dynamically download modules at run time on demand, what is shown above is pretty much all you need to do. You certainly need to build your modules and deploy them with the application, but before we dive into the details on building modules, let's first take a look at what's really inside the SilverModule control.

Inside the SilverModule control

The SilverModule control is derived from UserControl. Its XAML just sets up the "Loaded" event handler and has an empty Grid layout control:

Fig 2a. SilverModule XAML
<UserControl x:Class="SilverModule.SilverModule"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Loaded="OnSilverModuleLoaded">
    <Grid x:Name="LayoutRoot" Background="Transparent">
    </Grid>
</UserControl>

SilverModule exposes the following bindable properties: ModuleName, ModuleRelativePath, ModuleAssemblyName and ModuleTypeName. Only the ModuleName is required: if all the other three properties are omitted, their values will be inferred from ModuleName (more on this later).

When the ModuleName property is set to a target module name, that module will be retrieved from the server and loaded in memory. To accomplish this, SilverModule uses a helper class called ModuleMaker. As ModuleMaker executes, it propagates the status messages to its invoker (in this case, it’s the code-behind of the SilverModule XAML, i.e., SilverModule.xaml.cs). ModuleMaker also handles errors that occur during downloading and instantiating. We will delve into further details of the ModuleMaker in a later section.

Figure 2b
public partial class SilverModule : UserControl
{

    private ModuleMaker  _moduleMaker;

    // UI element that displays downloading progress and error message
    private TextBlock  _loaderTextBox;
    public SilverModule()
    {
        InitializeComponent();
        InitSilverModule();
    }
    
    private void InitSilverModule()
     {
        _moduleMaker = new ModuleMaker();
        _moduleMaker.ModuleContentReady += 
              new EventHandler<modulereadyeventargs>(onModuleContentReady);

        LayoutRoot.DataContext = _moduleMaker;

      if (null == _statusTextBlock)
      {
          //create the instance of TextBlock that shows the downloading process
          _statusTextBlock = new TextBlock();
          _statusTextBlock.TextAlignment = TextAlignment.Center;
          _statusTextBlock.VerticalAlignment = VerticalAlignment.Center;
          _statusTextBlock.Foreground = 
               new SolidColorBrush(Color.FromArgb(0xFF, 0x88, 0, 0));

          //Create the binding description
          _statusTextBlock.SetBinding(TextBlock.TextProperty, 
                  new Binding("StatusMessage"));
         }
      }

      private void onModuleContentReady(object s, ModuleReadyEventArgs e)
      {
           //display the loaded module content
           LayoutRoot.Children.Clear();
           LayoutRoot.Children.Add(e.ModuleContent);
      }
    
      private void OnSilverModuleLoaded(object sender, RoutedEventArgs e)
      {
         InitStatusTextBlock();
      }

      private void InitStatusTextBlock()
      {
         LayoutRoot.Children.Add(_statusTextBlock);
      }

Figure 2b shows how InitSilverModule creates an instance of the ModuleMaker and subsequently sets up the _statusTextBlock so that it can receive asynchronous progress messages from ModuleMaker. Notice that the _statusTextBlock’s text is data bound to the property StatusMessage of the class ModuleMaker. As the DataContext for the Grid instance LayoutRoot is set to the ModuleMaker instance _moduleMaker, the downloaded progress notifications and error messages are automatically displayed on the TextBox, _statusTextBlock. The reason to create the TextBlock control in code rather than in XAML is that it needs to be dynamically added to the LayoutRoot during downloading (or when an error occurs), and needs to be replaced from the LayoutRoot when downloaded module is ready to render.

After the SilverModule has been loaded, the InitStatusTextBlock method is called. It adds the _statusTextBlock to the LayoutRoot of the SilverModule page.

The InitSilverModule method also sets up an event handler for ModuleContentReady. ModuleMaker will raise the ModuleContentReady event when a module has been retrieved from the server and subsequently hydrated (reconstructed). The event handler onModuleContentReady adds the newly created module to the LayoutRoot of SilverModule, by replacing _statusTextBlock with a freshly created target module.

Fig 2c. Add/remove TextBlock and Module content to/from LayoutRoot as status changes
private void onModuleContentReady(object sender, ModuleReadyEventArgs e)
{
    //display the loaded module content
    LayoutRoot.Children.Clear();
    LayoutRoot.Children.Add(e.ModuleContent);
}

public void SetModuleName(string newValue)
{
    if (LayoutRoot.Children.Count > 0)
    {//already had a module loaded, remove it first
        LayoutRoot.Children.Clear();
        InitLoaderText();
    }

    //this property setter will start the module downloading automatically
    _mStatus.ModuleName = newValue;
}

At this moment, no error occurs, and the module is auto-rendered in the layout. The module features are dynamically loaded and ready to interact with the end user.

Now, let’s examine the mechanism that starts the module download. When the bindable ModuleName property of SilverModule gets set to a value, it invokes SetModuleName. When the ModuleName changes, the callback OnModuleNamePropertyChanged event is raised, and the event handler will invoke the SetModuleName method.

Fig. 2d. SilverModule's ModuleName DependencyProperty
#region Bindable ModuleName Property
/// <summary>
/// The assembly name only, no extension (.xap, or .dll),
/// MUST be set before using other properties
/// Also assuming the package name is [ModuleName].xap and
/// it locates at the same folder as where the loading Silverlight app (main xap) is
/// Also assuming the UserControl Type name implemented
/// inside [ModuleName] is also the same as [ModuleName]
/// </summary>
public string ModuleName
{
    get { return (string)GetValue(ModuleNameProperty); }
    set { SetValue(ModuleNameProperty, value); }
}

// Using a DependencyProperty as the backing store for ModuleName.
//  This enables animation, styling, binding, etc...
public static readonly DependencyProperty ModuleNameProperty =
    DependencyProperty.Register("ModuleName", typeof(string), typeof(SilverModule), 
    new PropertyMetadata("", new PropertyChangedCallback(OnModuleNamePropertyChanged)));

// DP: changed callback
private static void OnModuleNamePropertyChanged(DependencyObject d, 
                    DependencyPropertyChangedEventArgs e)
{
    string newValue = (string)e.NewValue;
    if (String.IsNullOrEmpty(newValue))
        return;

    SilverModule ctrl = (SilverModule)d;
    ctrl.SetModuleName(newValue);
}
#endregion

Next, we will see what happens when SetModuleName executes.

Figure 2e
public void SetModuleName(string newValue)
{
    if (LayoutRoot.Children.Count > 0)
    {
        LayoutRoot.Children.Clear();
        InitStatusTextBlock();
    }

    //this property setter will start the module downloading automatically
    _moduleMaker.ModuleName = newValue;
}

As shown above in Figure 2e, the status will be reset and the ModuleName property of the ModuleMaker will be set to the new module name (newValue). This kicks off the download and loading mechanisms, which will be covered in the next section where we will discuss the inner workings of the ModuleMaker.

Module status and Error handling

Module status and error handing during downloading, extracting, instantiating are handled by the ModuleMaker class. The class ModuleMaker has the following responsibilities:

  1. downloading the module (typical of a XAP file) from the server
  2. extracting the module assembly from the XAP binary stream
  3. instantiating a module custom type instance
  4. raise the ModuleContentReady event when it's ready to render
  5. report downloading progress and errors (if any)
Fig.3.
public class ModuleMaker : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged = delegate { };
    public event EventHandler<modulereadyeventargs> 
                 ModuleContentReady = delegate { };
    private WebClient _webClient = null;
        
    private string _statusMessage = "";
    public string StatusMessage 
    {
      get { return _statusMessage; }
      set {
           _statusMessage = value; 
           PropertyChanged(this, 
            new PropertyChangedEventArgs("StatusMessage"));    
      }
    }

    private string _moduleName;
    public string ModuleName 
    {
        get
        {
            return _moduleName;
        }
        set
        {
            if (String.IsNullOrEmpty(value))
            throw new ArgumentNullException("ModuleName should " + 
                      "never be empty or null!");
                
            if (value != _moduleName)
            {
                //new module name is set, needs to start download
                _moduleName = value;
                StartToDownloadModule();
            }
        }
    }
    
    …

    public string  Error 
    {
        set
        {
            this.StatusMessage = String.Format("Failed to load {0}: {1}", 
                                               ModuleName, value);
        }
    }

    private void StartToDownloadModule()
    {
        if (null == _webClient)
        {
            //initialize the downloader
            _webClient = new WebClient();
            _webClient.DownloadProgressChanged += 
            new DownloadProgressChangedEventHandler(onDownloadProgressChanged);

            _webClient.OpenReadCompleted += 
              new OpenReadCompletedEventHandler(onOpenReadCompleted);
        }

        if (_webClient.IsBusy)
        {
            //needs to cancel the previous loading
            this.StatusMessage = "Cancelling previous downloading...";
            _webClient.CancelAsync();
            return;
        }

        Uri xapUrl = new Uri(this.ModuleURL, UriKind.RelativeOrAbsolute);
        _webClient.OpenReadAsync(xapUrl);
    }

    private void onDownloadProgressChanged(object sender, 
                        DownloadProgressChangedEventArgs e)
    {
        LoadingProcess = e.ProgressPercentage;
    }

    private void onOpenReadCompleted(object sender, OpenReadCompletedEventArgs e)
    {
        if (null != e.Error)
        {
            //report the download error
            this.Error = e.Error.Message;
            return;
        }

        if (e.Cancelled)
        {
            // cancelled previous downloading, needs to re-start the loading
            StartToDownloadModule();
            return;
        }

        // Load a particular assembly from XAP
        Assembly aDLL = GetAssemblyFromPackage(this.ModuleAssemblyName, e.Result);
        if (null == aDLL)
        {
            //report the assembly extracting error
            this.Error = "Module downloaded but failed to extract the assembly." + 
                           " Please check the assembly name.";
            return;
        }

        // Get an instance of the XAML object
        UserControl content = aDLL.CreateInstance(this.ModuleTypeName) as UserControl;
        if (null == content)
        {
            //report the type instnatiating error
            this.Error = "Module downloaded and the assembly extracted” + 
            “ but failed to instantiate the custome type.” 
            “ Please check the type name.";
            return;
        }

        //tell the event handler it's ready to display
        ModuleContentReady(this, 
             new ModuleReadyEventArgs() { ModuleContent = content } );
    }
}

Whenever ModuleName changes, the StartToDownloadModule method (Fig. 3) will execute. It makes sure it creates a single instance of a WebClient and wires up the DownloadProgressChanged and OpenReadComplete events. Before calling OpenReadAsync to asynchronously download the binary XAP package, it makes sure to cancel any previous downloading, since a WebClient instance can only be used to download one object asynchronously.

The DownloadProgressChanged event handler simply grabs the percentage integer value, then sets it to the LoadingProcess property (see Fig.3). The LoadingProcess property will turn around to build a user friendly string message, then set it to StatusMessage. Since StatusMessage is data bound to the dynamic TextBlock's Text property, whenever the StatusMessage value changes, the data binding engine will update the TextBlock automatically. This progress updating is useful when the Module package takes some time to download, and the user can see it updates at runtime.

One trick worth noting is how the new module downloading (triggered by the ModuleName property changes) actually starts: it actually starts to download when the StartToDownloadModule (Fig.3) method get called a second time. Because, when StartToDownloadModule first runs, it detects the WebClient is still busy, and it simply calls CancelAsyn and then returns. (Fig. 3). When the OpenReadComplete event is handled (Fig.3), it checks if the canceling is complete. If so, then it would invoke StartToDownloadModule again. When StartToDownloadModule runs for the second time, the WebClient instance is not busy any longer (already cancelled), and OpenReadAsyn finally gets executed.

Fig.3 also shows how the errors are handled. When a downloading error occurs, due to a wrong ModuleName, or no such module package existing on the server, or the URL is wrong, etc., the WebClient instance will raise an OpenReadCompleted event with the Error property set. The event handler will always check the Error property before performing any other task; when it's set, it'll pass Error.Message to the StatusMessage property and then shows it to the user.

When the module is downloaded successfully, GetAssemblyFromPackage will try to extract out the assembly from the downloaded binary stream. When the stream has an error, or ModuleAssemplyName is wrong (or no such named assembly exists inside the package), GetAssemblyFromPackage will return null and the string "Module downloaded but failed to extract the assembly. Please check the assembly name." will be set to StatusMessage to show the error.

After both the downloading and the assembly extracting succeeds, the code will proceed to instantiate the custom type from the extracted assembly. If no such named custom type is defined in the assembly, or the type is not derived from UserControl, the following error message will be shown to the user: "Module downloaded and the assembly extracted but failed to instantiate the custom type. Please check the type name."

When no error occurs there is a reference to the instance of the custom type, SilverModuleStatus will raise a custom event named ModuleContentReady. The event handler is set up when SilverModule is constructed, and the event is handled by the onModuleContentReady method in the SilverModule code-behind class. Again, the onModuleContentReady method will clean up the LayoutRoot; if there was a previously loaded module, it'll be removed from the visual tree together with the TextBlock. The instance of the custom type within the loaded module will be rendered within LayoutRoot.

About the demo project

The SilverModuleDemo project within the solution is the main application. It demonstrate how a new module (SilverModuleTestOne) is downloaded and rendered in the right pane, how a newly downloaded module (SilverModuleTestTwo) replaces the existing one in the right pane, and how a sub-module (SilverModuleTestSub) gets loaded into another module as the user requests. Additionally, it also shows how an error (a request for a non-existing module) is handled too.

SilverModuleDemo: This is the main demo application. It has a list box containing the following string items: "SilverModuleTestOne", "SilverModuleTestTwo", "Non-exist Module". When the user clicks on the first item “SilverModuleTestOne”, the SilverModuleTestOne module will get loaded and renders itself on the right hand panel. When the user clicks on the second item “SilverModuleTestTwo”, the SilverModuleTestTwo module will get loaded and renders itself on the right hand panel. When the user clicks on the third item, the "Non-exist Module", a message will be displayed as there is no module to be found. A short description of the various modules follow:

  • SilverModuleTestOne: This is a dynamic module that simply displays a TextBlock.
  • SilverModuleTestTwo: This is a dynamic module that shows a button that displays the word “Load”. When the user clicks on the “Load” button, the SilverModuleTestSub module gets loaded as a sub-module.
  • SilverModuleTestSub: This module is loaded when the user clicks on the load button from the SilverModuleTestTwo module.
  • SilverModule: As stated earlier, this is the class library that does all the work to dynamically load and render the other modules. SilverModule is implemented here.

If the module you are creating is intended to work within a Shell application only, rather not by itself alone, you can simply remove the Appcalition_Startup event handler from App.XML. All three module projects in the demo solution follow the same practice: both Application_Startup and Application_Exit event handlers are commented out, so they can't run by themselves when referenced directly from a page.

To leverage the default "inferring" logic based on ModuleName only, all the three module projects in the demo solution also intentionally rename the generated Page type to the same name as the project name, then the inferred ModuleTypeName value will be correct. Since the default destination folder value in the "Add Silverlight Application" dialog will make sure the module's XAP output will be copied to the same ClientBin folder as where the Shell application is copied to, there is no need to set the ModuleRelativePath property in the XAML. By default, the assembly name is the same as the project name, so you can leave out the ModuleAssemblyName property to use its inferred value as well. Therefore, in the Shell application (SilverModuleDemo)'s Page.XAML, the <sm:SilverModule> tag can stay as the simplest form (Fig.1). Only the ModuleName property needs to be set. You can try to set other properties in the XAML as shown in Fig.2 to see how it reacts.

When the demo solution runs, selecting the first item will cause the ModuleName property to change via data binding. Eventually, the SilverModuleStatus class will start to download the selected module. You can set the break point within SilverModuleStatus to see how it performs step by step. When downloading, extracting, and instantiating all succeeds, the on-demand loaded module will show up in the right pane. When the second item is selected, a similar process will happen to the new module. The newly downloaded module will essentially replace the previously download module. In a Use Case that you don't want the new module content to replace the existing one, you can simply add multiple <sm:SilverModule> tags into your Shell application's XAML, then each one will have a module loaded into when its ModuleName is set correctly.

Incidentally, modules can not only be loaded by the Shell application, modules can load other modules (let's call them sub-modules) in the same way. To demonstrate this, the SilverModuleTestTwo project in the demo solution references the SilverModule library and has a <sm:SilverModule> in its XAML to set up to load a sub-module on demand (when the 'Load' button is clicked). The sub-module is implemented in the same way as a regular module. Please take a look at the SilverModuleTestSub project for details.

Conclusions

SilverModule provides a low overhead yet practical solution for building modular Silverlight applications. Its built-in inferring logic, mechanisms for auto-downloading, extracting, instantiating, rendering, and progress/error handling make modular Silverlight applications much easier to build.

The module concept is powerful enough to improve large RIA cohesion, encapsulation, runtime performance, memory usage, and ultimately the customer experience. This approach facilitates rapid development in a team environment. The support for submodules makes it possible to break up feature groups in further granularity while keeping their logical relationships.

History

  • 2009.01.16 - First draft.
  • 2009.02.05 - Ready to review.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here