Click here to Skip to main content
15,861,172 members
Articles / Desktop Programming / XAML

Using Silverlight 3 Navigation with Prism and On-Demand Loading

Rate me:
Please Sign up or sign in to vote.
4.93/5 (20 votes)
8 Sep 2009CPOL9 min read 111K   1K   53   42
An article demonstrating one way of integrating Silverlight 3 Navigation with Prism 2, including on-demand loading of modules.

The finished sample

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.

The basic navigation template application

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:

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

Prism references

Once these are added, we can add a Bootstrapper.cs and a ModulesCatalog.xaml:

C#
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:

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

C#
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:

C#
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.

Solution structure

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:

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

C#
using System.Collections.Generic;

namespace Prism.Shell.Navigation
{
    public static class ModuleMapper
    {
        public static Dictionary<string, string> ModuleMaps { get; set; } 
        
        static ModuleMapper()
        {
            // if any navigation pages have prism regions
            // then put the map to the relevant
            // module here. The module will then be
            // dynamically loaded when necessary.
            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.

C#
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();
        }

        // After the Frame navigates, ensure the HyperlinkButton
        // representing the current page is selected
        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 link requires a module then load it
            if (Navigation.ModuleMapper.ModuleMaps.ContainsKey(uri))
            {
                _moduleManager.LoadModule(Navigation.ModuleMapper.ModuleMaps[uri]);
            }
        }

        // If an error occurs during navigation, show an error window
        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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
New Zealand New Zealand
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
AnswerRe: Frame in Module Pin
Nigel Ferrissey7-Oct-09 10:10
Nigel Ferrissey7-Oct-09 10:10 
GeneralRuntime error Pin
Joel Palmer8-Sep-09 6:07
Joel Palmer8-Sep-09 6:07 
GeneralRe: Runtime error Pin
Nigel Ferrissey8-Sep-09 10:11
Nigel Ferrissey8-Sep-09 10:11 
GeneralRe: Runtime error Pin
Joel Palmer8-Sep-09 10:37
Joel Palmer8-Sep-09 10:37 
GeneralRe: Runtime error Pin
Nigel Ferrissey8-Sep-09 10:50
Nigel Ferrissey8-Sep-09 10:50 
GeneralRe: Runtime error Pin
fire_birdie15-Sep-09 3:11
fire_birdie15-Sep-09 3:11 
GeneralRe: Runtime error Pin
Nigel Ferrissey15-Sep-09 17:19
Nigel Ferrissey15-Sep-09 17:19 
GeneralArticle Correction Pin
Joel Palmer8-Sep-09 4:49
Joel Palmer8-Sep-09 4:49 
Thanks for the article. The other articles I've seen get too caught up in the MVVM design pattern and end up very muddled.

I'm stepping through your article and found your article does not match your code in the CreateShell() method.

Update:
Shell shell = this.Container.Resolve();

To:
Shell shell = this.Container.Resolve<Shell>();

Likely, you just did a copy/paste. The lt & gt need to follow xml syntax.

Joel Palmer
Data Integration Engineer

modified on Tuesday, September 8, 2009 12:04 PM

GeneralRe: Article Correction Pin
Nigel Ferrissey8-Sep-09 10:00
Nigel Ferrissey8-Sep-09 10:00 
GeneralSuperb article.. but one question Pin
Dodge@18-Sep-09 4:10
Dodge@18-Sep-09 4:10 
GeneralRe: Superb article.. but one question Pin
Nigel Ferrissey8-Sep-09 10:39
Nigel Ferrissey8-Sep-09 10:39 
GeneralRe: Superb article.. but one question Pin
Dodge@18-Sep-09 10:56
Dodge@18-Sep-09 10:56 
Generalby the way Pin
r5tyrtyrtyrtyrtyrtyrtyrtyrty6-Sep-09 5:46
r5tyrtyrtyrtyrtyrtyrtyrtyrty6-Sep-09 5:46 
GeneralRe: by the way Pin
Nigel Ferrissey7-Sep-09 21:10
Nigel Ferrissey7-Sep-09 21:10 
GeneralGreat article Pin
r5tyrtyrtyrtyrtyrtyrtyrtyrty6-Sep-09 5:45
r5tyrtyrtyrtyrtyrtyrtyrtyrty6-Sep-09 5:45 
GeneralRe: Great article Pin
Nigel Ferrissey7-Sep-09 21:08
Nigel Ferrissey7-Sep-09 21:08 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.