Introduction
This article demonstrates one possible way of integrating Silverlight 3's new navigation functionality (including deep-linking support) with some benefits provided by Prism - in particular, the ability for loosely coupled modules to inject views into navigation pages, and for modules to be loaded on-demand, thereby minimizing the initial download.
Background
The Composite Application Guidance
In February 2009, Microsoft released version 2.0 of the Composite Application Guidance for WPF and Silverlight - commonly known as Prism. Prism provides guidance and reference for a number of topics including multi-targeting (the sharing of code between WPF and Silverlight), UI composition, and modularity.
The UI Composition design concept introduces the idea of displaying a view in a region. A RegionManager
is responsible for maintaining a collection of regions in a Shell application. The views are contained in loosely coupled modules and, when initialised, inject themselves into the regions. The view has no concept of the region or where it is.
The Silverlight 3 Navigation Framework
The Navigation Framework introduced in Silverlight 3 allows developers to create separate pages within the same Silverlight application and navigate to them via the URL. For instance, your Silverlight application may be hosted at http://myapp.com/default.aspx. You could navigate directly to the About page using http://myapp.com/default.aspx#/About.
This "Deep Linking" is a major step forward for rich internet applications and the adoption of Silverlight for line of business scenarios.
In this article, we will walk through the creation of a shell application, with loosely coupled modules that are downloaded on demand, complete with navigation pages and deep linking.
Pre-requisites
To follow this article as a walk through, you will need a few pre-requisites:
- Visual Studio 2008 SP1
- Silverlight Tools for Visual Studio
- Silverlight 3 Developer Runtime
- Microsoft Composite Application Guidance Feb. 2009
The article also presumes a basic knowledge of Silverlight applications, XAML, and a general understanding of Prism.
Creating the Shell
We will start by creating a new Silverlight Navigation Application. Name the application Prism.Shell, and allow Visual Studio to create a new web site to host it.
The navigation application template provides a simple application shell for us to build on, complete with some navigation frames and buttons already wired up for us. Run the application (click Yes when asked to modify the web.config for debugging), and take a look at the result.
A quick look at MainPage.xaml reveals the navigation:Frame
section, complete with uriMapper
- the basis of Silverlight Navigation. The content for the frames is defined in separate XAML files under the Views folder. These locations are mapped from the entered URL by the uriMapper
and the content is rendered in the navigation:Frame
. A few events are fired along the way, such as Navigated
(or NavigationFailed
) and, in the unchanged MainPage, the Navigated
event is used to ensure that the relevant button is highlighted in the menu at the top of the screen.
Adding New Navigation Pages
For our example, we will add two new pages with some static content and two new buttons to allow us to navigate to them. Later in the walk through, we will replace the static content of the new pages with Prism regions that have their contents dynamically loaded and injected.
Open up MainPage.xaml and amend the LinksStackPanel
so that we have a couple more buttons:
<Border x:Name="LinksBorder" Style="{StaticResource LinksBorderStyle}">
<StackPanel x:Name="LinksStackPanel" Style="{StaticResource LinksStackPanelStyle}">
<HyperlinkButton x:Name="Link1"
Style="{StaticResource LinkStyle}"
NavigateUri="/Home"
TargetName="ContentFrame" Content="home"/>
<Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}"/>
<HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}"
NavigateUri="/About"
TargetName="ContentFrame" Content="about"/>
<Rectangle x:Name="Divider2" Style="{StaticResource DividerStyle}"/>
<HyperlinkButton x:Name="Link3"
Style="{StaticResource LinkStyle}"
NavigateUri="/Module1" TargetName="ContentFrame"
Content="module 1"/>
<Rectangle x:Name="Divider3" Style="{StaticResource DividerStyle}"/>
<HyperlinkButton x:Name="Link4" Style="{StaticResource LinkStyle}"
NavigateUri="/Module2"
TargetName="ContentFrame" Content="module 2"/>
</StackPanel>
</Border>
We will also need two new pages for the navigation buttons to navigate to, so start by copying one of the existing pages from the Views folder and pasting two new copies. In the example, they are called Module1 and Module2. Remember to rename the code-behind files and amend the class name at the top of the XAML, the class name in the code-behind, and the constructors. Add some different content to the new pages, so that when we run the application, we can see the navigation in action.
At this point, we should take the opportunity to rename our "MainPage" to "Shell". This is more by convention than necessity, but it does reduce confusion later on when we refer to the "shell". Again, remember to update the class names and constructors.
Integrating Prism
At this point, our example application will refuse to compile because the Application_Startup
method in App.xaml.cs will be unable to find a MainPage from which to bootstrap the application. When adding the Prism components, we will provide the bootstrapper, which will in turn direct the application to a ModulesCatalog.xaml where it will find the details of the modules that may or may not be required. This way, the shell application does not need a hard reference to any modules and they can be loaded on demand.
To hook up Prism, we will need some references to some Prism DLLs:
Once these are added, we can add a Bootstrapper.cs and a ModulesCatalog.xaml:
using System;
using System.Windows;
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.UnityExtensions;
namespace Prism.Shell
{
internal class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
Shell shell = this.Container.Resolve<Shell>();
Application.Current.RootVisual = shell;
return shell;
}
protected override IModuleCatalog GetModuleCatalog()
{
ModuleCatalog catalog = new ModuleCatalog();
return ModuleCatalog.CreateFromXaml(
new Uri("Prism.Shell;component/ModulesCatalog.xaml",
UriKind.Relative));
}
}
}
The XAML:
<m:ModuleCatalog xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System; assembly=mscorlib"
xmlns:m="clr-namespace:Microsoft.Practices.Composite.
Modularity;assembly=Microsoft.Practices.Composite"
>
<m:ModuleInfoGroup Ref="Prism.Module1.xap" InitializationMode="OnDemand">
<m:ModuleInfo ModuleName="Prism.Module1.InitModule"
ModuleType="Prism.Module1.InitModule,
Prism.Module1, Version=1.0.0.0"></m:ModuleInfo>
</m:ModuleInfoGroup>
<m:ModuleInfoGroup Ref="Prism.Module2.xap" InitializationMode="OnDemand">
<m:ModuleInfo ModuleName="Prism.Module2.InitModule"
ModuleType="Prism.Module2.InitModule,
Prism.Module2, Version=1.0.0.0"></m:ModuleInfo>
</m:ModuleInfoGroup>
</m:ModuleCatalog>
Now that we have implemented our own bootstrapper, we can amend the App.xaml.cs, to call that instead of loading a XAML page directly.
private void Application_Startup(object sender, StartupEventArgs e)
{
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
The application is back in a runnable state at this point, waiting for us to include some Prism regions. For our example, we will add one region to each of our new pages. The pages will not have any idea of what content is being injected, and likewise the modules will initialise their views and inject them into a region, not knowing where that region is.
Creating the Modules
The modules contain the functionality of the application itself, the shell being just the framework for displaying them, and in our case, the framework for navigation too.
We will create some simple modules for this example, with some module-specific content to clearly show the views being injected from different locations as we navigate between the pages.
Add a new Silverlight Application project to your solution - we will call it Prism.Module1. Choose your existing web application when asked where you want it to be hosted, but ensure that you untick the option to "Add a test page that references the application", as this is unnecessary.
Add some references to the Prism DLLs and remove the MainPage.xaml that was added for you; add a new class called InitModule.cs instead. It doesn't really matter what it's called, just ensure that the reference to it in the ModulesCatalog.xaml is correct.
We can now add the initialisation code for our module:
using Microsoft.Practices.Composite.Modularity;
using Microsoft.Practices.Composite.Regions;
using Microsoft.Practices.Unity;
using Prism.Module1.Views;
namespace Prism.Module1
{
public class InitModule : IModule
{
IUnityContainer _container;
IRegionManager _regionManager;
public InitModule(IUnityContainer Container,
IRegionManager RegionManager)
{
_container = Container;
_regionManager = RegionManager;
}
public void Initialize()
{
RegisterViewsAndServices();
}
protected void RegisterViewsAndServices()
{
_regionManager.RegisterViewWithRegion("Module1Region",
typeof(Module1View));
}
}
}
For the sake of simplicity, the example uses a plain string to indicate the region that the view (Module1View
) will be injected into. I would recommend abstracting this out into an enum
or similar, probably in its own project - something like Prism.Infrastructure. Note that we haven't created Module1View
yet, so again, if you are following this as a walkthrough, it doesn't matter what it's called, just remember to update it if you change it later.
You will need to remove App.xaml and App.xaml.cs too. InitModule
will do all our initialising for the module.
Now is a good time to add a folder ("Views") to the module project and add a new Silverlight user control ("Module1View
"). Add some content to the view that will clearly demonstrate which module the view is being injected from.
To provide a reasonable example, we will create another module along the same lines. This time called Module2. Follow the same structure as Module1, just remember to consistently rename classes and namespaces appropriately.
Wiring up the Navigation
Back in the Prism.Shell project, open up the Module1.xaml view and add a Prism region to it. This is where the output from Module1 will be injected. Add a namespace for the Prism Regions code ("cal
" in this case), and replace the static text we included earlier with a Prism Region - this takes the form of an ItemsControl
:
<navigation:Page x:Class="Prism.Shell.Module1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;
assembly=Microsoft.Practices.Composite.Presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:navigation="clr-namespace:System.Windows.Controls;
assembly=System.Windows.Controls.Navigation"
mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"
Title="About"
Style="{StaticResource PageStyle}">
<StackPanel Orientation="Vertical" HorizontalAlignment="Left">
<ItemsControl x:Name="Module1Region"
cal:RegionManager.RegionName="Module1Region"/>
</StackPanel>
</navigation:Page>
Repeat this for Module2, remembering to change the class and region names appropriately.
On-Demand Loading
The application will build and run at this stage, but the views from the modules will not yet be injected. This is because there is no hard reference to them from the shell project and they are marked as "OnDemand
" in the ModuleCatalog. The shell doesn't know about them and isn't demanding them.
The key concept behind on-demand loading is to reduce the initial footprint of the application. We could demand the modules up-front so that they have already injected their content into the regions when the pages are navigated to, but that would defeat the object. Instead, we need to demand the relevant module when the appropriate page is navigated to.
One possible solution is to implement a simple "Module Mapper" - a static class that maps the Navigation Page to a module (or modules, there is nothing stopping you from having more than one region on a page, with views injected from more than one module). We can call the "Module Mapper" when the page is navigated to, which will in turn demand the relevant module. The module will then initialise and inject its content.
The example project uses this technique, and is intentionally limited to one module per navigation page, so it could easily be improved. It's not really important how they are mapped, we just need some way of doing it. The Navigation folder contains one class, ModuleMapper.cs.
using System.Collections.Generic;
namespace Prism.Shell.Navigation
{
public static class ModuleMapper
{
public static Dictionary<string, string> ModuleMaps { get; set; }
static ModuleMapper()
{
ModuleMaps = new Dictionary<string, string>();
ModuleMaps.Add("/Module1", "Prism.Module1.InitModule");
ModuleMaps.Add("/Module2", "Prism.Module2.InitModule");
}
}
}
In Shell.xaml.cs, we alter the constructor to accept a Prism ModuleManager
. In the ContentFrame_Navigated
method, before highlighting the navigation button, we call a LoadModule
method that looks up the module map and uses the ModuleManager
to load the required module. Obviously, this is slightly over simplified for our example, with no exception handling for instance, but it shows the concept in action.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Navigation;
using Microsoft.Practices.Composite.Modularity;
namespace Prism.Shell
{
public partial class Shell : UserControl
{
private IModuleManager _moduleManager;
public Shell(IModuleManager ModuleManager)
{
_moduleManager = ModuleManager;
InitializeComponent();
}
private void ContentFrame_Navigated(object sender,
NavigationEventArgs e)
{
LoadModule(e.Uri.ToString());
foreach (UIElement child in LinksStackPanel.Children)
{
HyperlinkButton hb = child as HyperlinkButton;
if (hb != null && hb.NavigateUri != null)
{
if (hb.NavigateUri.ToString().Equals(e.Uri.ToString()))
{
VisualStateManager.GoToState(hb, "ActiveLink", true);
}
else
{
VisualStateManager.GoToState(hb, "InactiveLink", true);
}
}
}
}
private void LoadModule(string uri)
{
if (Navigation.ModuleMapper.ModuleMaps.ContainsKey(uri))
{
_moduleManager.LoadModule(Navigation.ModuleMapper.ModuleMaps[uri]);
}
}
private void ContentFrame_NavigationFailed(object sender,
NavigationFailedEventArgs e)
{
e.Handled = true;
ChildWindow errorWin = new ErrorWindow(e.Uri);
errorWin.Show();
}
}
}
Run the application and navigate to the new pages using the navigation buttons. The modules will be loaded on demand and their views injected into the relevant regions. Now, try navigating to the new pages using Silverlight 3 Deep-Linking. Exactly the same behaviour should be observed, but now, you can navigate to a page injected by a dynamically loaded module, directly from anywhere outside the application.
Improving the Sample
The sample is intentionally simplistic, and there are a few obvious ways that it could be improved.
- Improve the ModuleMapper to allow multiple modules per navigation page and to read the mappings from a configuration file, rather than hard coding them in a class. The existing ModulesCatalog may be used for this.
- Define the region names in a separate DLL, maybe an "infrastructure" project that handles other solution wide tasks, such as service location.
- Add a "Loading" indicator whilst the modules are being retrieved. The wait could be significant over a slow connection, and currently there is no feedback as to what is happening.
Using the Sample Code
To use the sample code:
- Unzip the source code
- Open the solution in Visual Studio
- Re-reference the Prism DLLs
- Set the startup project to Prism.Shell.Web
- Set the startup page to Prism.ShellTestPage.aspx
- Run the application
For a really quick start, create a folder on your C: drive called "Prism Library" and put the Prism DLLs in there. That is where the sample code is expecting to find them.
History
- Original article: 4 September 2009.