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">
-->
<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
<!---->
<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;
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)
{
_statusTextBlock = new TextBlock();
_statusTextBlock.TextAlignment = TextAlignment.Center;
_statusTextBlock.VerticalAlignment = VerticalAlignment.Center;
_statusTextBlock.Foreground =
new SolidColorBrush(Color.FromArgb(0xFF, 0x88, 0, 0));
_statusTextBlock.SetBinding(TextBlock.TextProperty,
new Binding("StatusMessage"));
}
}
private void onModuleContentReady(object s, ModuleReadyEventArgs e)
{
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)
{
LayoutRoot.Children.Clear();
LayoutRoot.Children.Add(e.ModuleContent);
}
public void SetModuleName(string newValue)
{
if (LayoutRoot.Children.Count > 0)
{
LayoutRoot.Children.Clear();
InitLoaderText();
}
_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
public string ModuleName
{
get { return (string)GetValue(ModuleNameProperty); }
set { SetValue(ModuleNameProperty, value); }
}
public static readonly DependencyProperty ModuleNameProperty =
DependencyProperty.Register("ModuleName", typeof(string), typeof(SilverModule),
new PropertyMetadata("", new PropertyChangedCallback(OnModuleNamePropertyChanged)));
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();
}
_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:
- downloading the module (typical of a XAP file) from the server
- extracting the module assembly from the XAP binary stream
- instantiating a module custom type instance
- raise the
ModuleContentReady
event when it's ready to render
- 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)
{
_moduleName = value;
StartToDownloadModule();
}
}
}
…
public string Error
{
set
{
this.StatusMessage = String.Format("Failed to load {0}: {1}",
ModuleName, value);
}
}
private void StartToDownloadModule()
{
if (null == _webClient)
{
_webClient = new WebClient();
_webClient.DownloadProgressChanged +=
new DownloadProgressChangedEventHandler(onDownloadProgressChanged);
_webClient.OpenReadCompleted +=
new OpenReadCompletedEventHandler(onOpenReadCompleted);
}
if (_webClient.IsBusy)
{
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)
{
this.Error = e.Error.Message;
return;
}
if (e.Cancelled)
{
StartToDownloadModule();
return;
}
Assembly aDLL = GetAssemblyFromPackage(this.ModuleAssemblyName, e.Result);
if (null == aDLL)
{
this.Error = "Module downloaded but failed to extract the assembly." +
" Please check the assembly name.";
return;
}
UserControl content = aDLL.CreateInstance(this.ModuleTypeName) as UserControl;
if (null == content)
{
this.Error = "Module downloaded and the assembly extracted” +
“ but failed to instantiate the custome type.”
“ Please check the type name.";
return;
}
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.