Tuesday, November 17, 2009

Dynamic Module Loading with Silverlight Navigation using Prism

I started a project to update my personal website using Silverlight. The goal is to use Prism/Composite Application Guidance to build out a nice site that demonstrates some Silverlight capabilities and make the source code available as a reference. One of the first pieces I chose to tackle was ensuring I could facilitate deep linking using Silverlight navigation and still take advantage of dynamic module loading using the Prism framework.

The initial research I did was not promising. Most sites claimed that Prism broke the Silverlight navigation, while others had partial solutions that didn't really cut it for me. Mapping a separate view in the main module to have a named region for every dynamically loaded module seems to be overkill. So, I set out to see for myself if it could be done. The answer, of course, is yes!

While the project is a simple pair of pages, I've built in a few more advanced pieces of functionality to extend its richness as a reference project. In addition to dynamic loading the module for the "biography" page, I've trimmed the size of the XAP files, sprinkled in a little search engine optimization, and used the visual state manager to handle some animations to boot.

You can download the full source code by clicking here. For a working demo, click here. You can use a sniffer or proxy like Fiddler 2 to confirm for yourself that the Biography module doesn't load until you click that tab, BUT you can also navigate directly:

I do apologize in advance for the vanity built into the project ... it is a project with the goal of replacing my biography website so it made since to name it after, well, me.

The first step in creating a deep linking navigation website is to go ahead and create a Silverlight Navigation Application. I called mine JeremyLikness.Main and it gave me JeremyLikness.Web to host it. Go ahead and trash everything in the views folder but the error window (I like that functionality, some might find it annoying), leave the App pieces alone but rename the main page to Shell.xaml.

Here's when things start to get interesting. First, I've seen a lot of implementations that want the pure "my module gets added magically" functionality, so they do things like inject the link to the module when the module itself gets loaded, etc. This is all great but then they also stuff a view in the main application that maps one-to-one with the module, give a new named region per module, and leave me scratching my head wondering, "What did we gain?" To me, the main portion of my app is a single region that can have multiple modules inject their views into it, so that's how I approached this project.

I'm going to be a bit more pragmatic and go ahead and map my links in the main shell because I'm assuming right now the shell is like the overall "controller" and is aware of the other modules. It should, however, be very easy and minimal overhead for me to add a new module later on. I'm not going to worry about the modules injecting the links because then I have to load the module before I can show the link, and that defeats the purpose of dynamically loading the modules. Ideally, your browser doesn't fetch that extra 100K of XAP download if you don't care about reading my biography!

I end up keeping the generated code for the links (I'll go back and rearrange and style it later ... getting it functioning for me comes before making it pretty). I'm starting with two links, so that section looks like this:


 <Border x:Name="LinksBorder" Style="{StaticResource LinksBorderStyle}">
                <StackPanel x:Name="LinksStackPanel" Style="{StaticResource LinksStackPanelStyle}">

                    <HyperlinkButton x:Name="homeLink" Style="{StaticResource LinkStyle}" 
                                     NavigateUri="/Home" TargetName="ContentFrame" Content="Home"/>
          
                    <Rectangle x:Name="Divider1" Style="{StaticResource DividerStyle}"/>
     
                    <HyperlinkButton x:Name="Link2" Style="{StaticResource LinkStyle}" 
                                     NavigateUri="/Bio" TargetName="ContentFrame" Content="Bio"/>
                </StackPanel>
            </Border>

Not bad. Now we're going to start butchering the generated template, so bear with me.

Dynamic Modules

Setting up modules to be dynamic is fairly straightforward. While there are many ways to approach this, the method I used has two key features:

  1. I'm using a XAML configuration for the module, not programmatic, and
  2. to add the module in a way that Prism can dynamically load it, I add it as a Silverlight Application, not a Class Library

That's it! We're going to work a bit backwards and live without the project compiling for a bit until we get all of the pieces put into place. I know I'm going to have a Home and a Bio module. For the sake of simplicity and this example, I'm doing one view per module, but you could obviously do more. You'll see why this makes it easy for me in a minute. First, let's set up the XAML that will describe the modules.

Right click on the main project (the one with your shell) and add a new Resources File. Name it ModuleCatalog.xaml. Blow away everything in there and instead put in the Prism notation for a module, like this:


<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="JeremyLikness.ModuleHome.xap" InitializationMode="WhenAvailable">
        <m:ModuleInfo ModuleName="JeremyLikness.ModuleHome.InitModule"
                      ModuleType="JeremyLikness.ModuleHome.InitModule, JeremyLikness.ModuleHome, Version=1.0.0.0"/>
    </m:ModuleInfoGroup>
    <m:ModuleInfoGroup Ref="JeremyLikness.ModuleBio.xap" InitializationMode="OnDemand">
        <m:ModuleInfo ModuleName="JeremyLikness.ModuleBio.InitModule"
                      ModuleType="JeremyLikness.ModuleBio.InitModule, JeremyLikness.ModuleBio, Version=1.0.0.0"/>
    </m:ModuleInfoGroup>
</m:ModuleCatalog>
                 

I've purposefully set the home page to load when available and the bio to load on demand to show the difference between the methods ... I could just as easily load both modules on demand. This simply points to the class that defines the module, and the assembly to find it in. Prism will assume it's available for download in the same directly as the hosted XAP and kindly pull down the XAP when it's needed for us.

We're now referring to some modules we don't have yet. At this point, you could go ahead and create the shells for the applications. In order for the project to be set up correctly for dynamic loading, instead of adding a Silverlight Class Library, you're going to add a new Silverlight Application. Pick the same web page to host it but don't bother with the generation of a test page as it will load into the same test page we used for the main. I called my projects JeremyLikness.ModuleBio and JeremyLikness.ModuleHome.

Once the projects are created, blow away the all of the generated XAML files. You won't need the App object because this will be hosted in our main application. I simply added the references needed for Prism, then created a Views folder and added a Home.xaml in my home project and a Bio.xaml in my bio project. These are UserControl types, not pages. In the root, I created InitModule classes for both projects (note that I referenced this in the ModuleCatalog.xaml). The code looks almost identical - here is the bio:


namespace JeremyLikness.ModuleBio
{
    public class InitModule : IModule
    {
        private IRegionManager _regionManager;

        public InitModule(IRegionManager regionManager)
        {
            _regionManager = regionManager;
        }

        #region IModule Members

        public void Initialize()
        {
            _regionManager.RegisterViewWithRegion("ModuleRegion", typeof(Bio));
        }

        #endregion
    }
}

Simple: reference the region manager in the constructor. The Prism will use Unity to resolve the reference and inject it. On the Initialize method, we register our view with the region. The home project will register to the same region, but with typeof(Home) home instead.

Now we've got the catalog and the modules, what's next?

Bootstrapping

We need to tell the Prism framework how to "get started." This means a bootstrapper. Back to JeremyLikness.Main, I add a class called Bootstrapper and base it on UnityBootstrapper. I need to create the shell and set it as the root visual:


protected override DependencyObject CreateShell()
{
    Shell shell = Container.Resolve<Shell>();
    Application.Current.RootVisual = shell;
    return shell;
}

I also need to supply a module catalog. For this, I'll point to the XAML file we created earlier:


protected override Microsoft.Practices.Composite.Modularity.IModuleCatalog GetModuleCatalog()
{
    return Microsoft.Practices.Composite.Modularity.ModuleCatalog.CreateFromXaml(
        new Uri("JeremyLikness.Main;component/ModuleCatalog.xaml", UriKind.Relative));
}

You can see I'm telling it to look to the XAML to create the catalog, then specifying the path to it. The last thing I need to do is go into the App.cs code behind and change the application start up to just run the boot strapper:


private void Application_Startup(object sender, StartupEventArgs e)
{
   new Bootstrapper().Run();
}

Adapting to the Region

Prism works with defined regions that can host views. There are a number of different ways those regions can be implemented. I would ask that you refer to the Prism documentation for this, but basically something like a ContentControl can have a single view active at any given time, while something like a ItemsContentControl can hold multiple views. One source of confusion is what "active" really means. Active views doesn't necessarily mean "visible" or "hidden." I had two challenges for this application: the first was how to host the view, and the second was how to manage the visibility of the views. Ideally, something would hold all of the views as they are selected, then simply make them visible or invisible as they are selected/deselected.

We'll tackle visibility in a minute. ContentControl wasn't an option because it only holds one view at a time. The problem with ItemsControl was related to layout. As each view is injected, it takes up space in the container (this is the same whether it's a items control or a stack panel, etc). Even when I'd collapse the visibility of a control, the original space would still cause the other controls (views) to be shifted, which was not the effect I wanted. I needed something like a Grid where I could stack the views one on top of the other.

Fortunately, making your own type of region is easy. After reading John Papa's excellent post, I made a PrismExtensions folder and built my GridRegionAdapter to allow Prism to use a grid as a container. You can read his article to understand the how/why and see how the adapter is constructer. I had to revisit the boot strapper and tell Prism how to map the adapter to the grid:


protected override RegionAdapterMappings ConfigureRegionAdapterMappings() 
{ 
    RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings(); 
    mappings.RegisterMapping(typeof(Grid), Container.Resolve()); 
    return mappings; 
}

Now I was ready to give the solution a go!

The Problem with Navigation

The first iteration I tried was to declare a single page with the region adapter to process the requests. In my Views folder in the main project, I created a Silverlight Page (Page, not UserControl) and called it Module.xaml. The XAML for the module looked like this:


<navigation:Page x:Class="JeremyLikness.Main.Views.Module" 
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
           xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
           xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
           mc:Ignorable="d"
           xmlns:cal="clr-namespace:Microsoft.Practices.Composite.Presentation.Regions;assembly=Microsoft.Practices.Composite.Presentation"
           xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
           xmlns:local="clr-namespace:JeremyLikness.Main.Views"
                 d:DesignWidth="640" d:DesignHeight="480"
           Title="Module Page">
    <Grid x:Name="MainGrid" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" cal:RegionManager.RegionName="ModuleRegion">
    </Grid>
</navigation:Page>
      

Going back to the main shell, I mapped all requests to the module, like this:


<Border x:Name="ContentBorder" Style="{StaticResource ContentBorderStyle}">

    <navigation:Frame x:Name="ContentFrame" Style="{StaticResource ContentFrameStyle}" 
                      Source="/Home" Navigated="ContentFrame_Navigated" NavigationFailed="ContentFrame_NavigationFailed">
        <navigation:Frame.UriMapper>
          <uriMapper:UriMapper>
            <uriMapper:UriMapping Uri="" MappedUri="/Views/Module.xaml"/>
            <uriMapper:UriMapping Uri="/{pageName}" MappedUri="/Views/Module.xaml"/>
          </uriMapper:UriMapper>
        </navigation:Frame.UriMapper>
    </navigation:Frame>
</Border>

As you can see, pretty much every request is handled by the Module page. In the code behind, I had to work a little bit of magic to ensure the appropriate module would be loaded. First, I added a reference to the module manager:


public partial class Shell : UserControl
{
    private IModuleManager _moduleManager;

    public Shell(IModuleManager moduleManager)
    {
        _moduleManager = moduleManager;
        InitializeComponent();
    }

    private void ContentFrame_Navigated(object sender, NavigationEventArgs e)
    {
    
        string module = e.Uri.ToString().Substring(1);
        string moduleName = string.Format("JeremyLikness.Module{0}.InitModule", module); 
       
        _moduleManager.LoadModule(moduleName);

...

}

The key here was getting the reference to the module manager, then calling the module load on the request. The module manager is smart enough to know when a module has already been loaded, so I didn't have to worry about that. The idea here is that when I request the "bio" tab, the string manipulation maps it to JeremyLikness.ModuleBio.InitModule, then calls the manager. The manager checks to see if it is loaded. If not, it will download the required XAP file and initialize the module. The module would then register the view with the region and it would magically appear!

There were two problems that arose immediately. The first issue was visibility: the different pages were stacking on each other. I'd load the application and see the home page, then click on the bio tab and watch the bio appear overlaid on top of the home page. Definitely not the right solution! We'll address visibility in a minute.

The second was a little more disturbing. The pages were behaving a little strangely so I fired up the debugger and found something disturbing: every time I would navigate to a link, the navigation framework would generate a new instance of the Module.xaml. This, in turn, would create new instances of the views. Just a few clicks of the tab and suddenly I had lots of instances hanging out that I just didn't need.

The solution for this was to create a static view that would live between navigation requests and hold the views as they are stacked in place. The first step was to build the container for the views. Under the Views folder, I created ViewContainer.xaml. The XAML contained the region mapping:


<UserControl x:Class="JeremyLikness.Main.Views.ViewContainer"
    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"
   >
    <Grid x:Name="LayoutRoot" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
          cal:RegionManager.RegionName="ModuleRegion">
       </Grid>
</UserControl>

The code behind implemented the singleton pattern:


public partial class ViewContainer : UserControl
{
    private static readonly ViewContainer _viewContainer = new ViewContainer();

    private ViewContainer()
    {
        InitializeComponent();
    }

    public static ViewContainer GetViewContainer()
    {
        if (_viewContainer.Parent != null)
        {
            Grid grid = _viewContainer.Parent as Grid;
            grid.Children.Remove(_viewContainer); 
        }

        return _viewContainer;
    }
}

Note that an element can only have one parent, and cannot be added as a child to another element. The GetViewContainer method first detaches the view from its previous parent before returning the instance so that the consumer of the method can use the orphaned view. I removed the region reference from the Module.xaml and added this to the code behind:


public Module()
{
    InitializeComponent();
    MainGrid.Children.Add(ViewContainer.GetViewContainer()); 
}

Now the navigation will still create a new instance of Module every time we navigate, but the module will simply reuse the same container for the views. This ensures that the views persist once they are added and don't have to be reloaded or recreated.

Next, I needed to manage showing and hiding the views so they don't remain stacked on top of each other.

Managing Visibility

To manage the views, I decided to create a service that the views will register to. The service is called when navigation takes place, and then calls back to the views to manage their visibility.

First, I created a new project that would be common across other projects. This was named, ironically, JeremyLikness.Common. First was an interface that the views could use. They provide a name for themselves as well as a method to call to either show or hide. This offloads the need for the view manager to understand how to show or hide a view ... since the view is a UserControl, it should have a pretty good idea.


public interface IPrismView
{
    void Show();

    void Hide();

    string ViewName { get; }
}

Next, I added a reference to the common project in both modules and implemented IPrismView. This is what the Bio.xaml.cs looked like:


#region IPrismView Members

public void Show()
{
    this.Visibility = Visibility.Visible; 
}

public void Hide()
{
    this.Visibility = Visibility.Collapsed;
}

public string ViewName
{
    get { return "Bio"; }
}

#endregion

Now I can create my view manager. In the same common project, I created a class called ActivationManager (continuing the Prism concept of "activating" a view although it is probably misnamed in retrospect). ActivationManager contains a collection of views. It allows a view to register with it, then exposes a method called SwapToView that iterates the views and calls their Show or Hide methods. It looks like this:


public class ActivationManager
{
    List<WeakReference> _views = new List<WeakReference>();

    public void Register(IPrismView view)
    {
        _views.Add(new WeakReference(view));
        view.Show();
    }

    public void SwapToView(string viewName)
    {
        foreach (WeakReference weakRef in _views)
        {
            if (weakRef.Target != null)
            {
                IPrismView view = (IPrismView)weakRef.Target;
                
                if (view != null)
                {
                    if (view.ViewName.Equals(viewName))
                    {
                        view.Show();
                    }
                    else
                    {
                        view.Hide();
                    }
                }
            }
        }
    }
}

Now we need to wire the activation manager in. We only need one copy, so in the boot strapper in the main project, I set up the instance:


protected override DependencyObject CreateShell()
{
    Container.RegisterInstance<ActivationManager>(
        Container.Resolve<ActivationManager>());
    Shell shell = Container.Resolve<Shell>();
    Application.Current.RootVisual = shell;
    return shell;
}

Notice I did this before creating the shell. There is a reason: we need the shell to coordinate the views. The shell has a method that fires when the navigation changes. If you recall, we used this method to tell the module manager to load the appropriate module. Now we'll need to pass in the activation manager and ask it to activate the views. These are the changes to the Shell code-behind:


private ActivationManager _activationManager;

public Shell(IModuleManager moduleManager, ActivationManager activationManager)
{
    _moduleManager = moduleManager;
    _activationManager = activationManager;
    InitializeComponent();
}

private void ContentFrame_Navigated(object sender, NavigationEventArgs e)
{

    string module = e.Uri.ToString().Substring(1);
    string moduleName = string.Format("JeremyLikness.Module{0}.InitModule", module); 
   
    _moduleManager.LoadModule(moduleName);

    _activationManager.SwapToView(module); 

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

The last step is to take each module and pass the activation manager into the constructor so the view can register itself. This could probably be moved back into the module initialization (i.e. when the view is injected into the region) and perhaps even something extended on the framework to handle it. However, this works for now, using Bio.xaml.cs as our example:


public Bio(ActivationManager activationManager) : this()
{
    activationManager.Register(this);
}

Notice that I use this() to ensure it calls the default parameterless constructor to InitializeComponent etc etc.

At this point, I claimed victory. After compiling and publishing the project, I was able to navigate between the home and bio tabs, and watch the bio module loaded dynamically when I navigated to the tab. The views swapped in and out nicely. If you were reading this for dynamic module loading ... there it is!

To round out the example, I wanted to show two more things. First, I wanted to take the idea of Show and Hide a step further, and second, I don't believe navigation or deep linking makes sense to discuss unless we also cover search engine optimization.

Visual State Manager

The visual state manager is the key to managing visual states on controls (seems like it was well-named, huh?). I have to thank Justin Angel for pointing this out to me when I was doing a lot of behaviors and triggers for animations that truly belonged in the VSM instead. Really, when we're swapping our views, we are changing the state of the view ("show me" or "hide me"). If we do this programmatically, then we're stuck with the attributes we set. However, if we do this through the VSM, then all of the the transition becomes customizable through the designer and in Expression Blend, so our designers can then tinker with the transitions without touching the code behind.

To demonstrate this, I decided to take both views and give them a ShowState and a HideState. To show the power of the Visual State Manager, I created two different transitions: the home page will explode in using a scale transform, while the biography page will fade in using an opacity animation.

Because there are a million examples that show how to add custom view states using Blend, I decided to document the code behind approach. The main container in our views is a grid, so that's what we're going to manipulate. I want the view to hide immediately, but when it goes to show, it should have a nice animation. This is what the Bio.xaml ends up looking like:


<Grid x:Name="LayoutRoot" Background="White">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="VisualStates">
                <VisualState x:Name="ShowState">
                    <Storyboard>
                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:01.0000000" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Opacity)">
                            <EasingDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
                        </DoubleAnimationUsingKeyFrames>
                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:01.0000000" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Visibility)">
                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Visible</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
                <VisualState x:Name="HideState">
                    <Storyboard>
                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0000100" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Opacity)">
                            <EasingDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
                        </DoubleAnimationUsingKeyFrames>
                        <ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:01.0000100" Storyboard.TargetName="LayoutRoot" Storyboard.TargetProperty="(UIElement.Visibility)">
                            <DiscreteObjectKeyFrame KeyTime="00:00:00">
                                <DiscreteObjectKeyFrame.Value>
                                    <Visibility>Collapsed</Visibility>
                                </DiscreteObjectKeyFrame.Value>
                            </DiscreteObjectKeyFrame>
                        </ObjectAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBlock>This is my bio. It tells people about me and my background.</TextBlock>
    </Grid>

So there are a few things going on here. First, we define a "group" of visual states that I call, sure enough, VisualStates. I am defining two states: a HideState and a ShowState. The hide state has an animation with a zero duration and simply sets the Opacity to 0. The show state animates for a second (you'd normally make this faster, but it helps demonstrate my point) and fades in. While many people are used to the color and double animations, did you know there existed an object animation as well? This allows you to manipulate almost any property on your target control using the visual state. In this case, I won't even have to set the Visibility property in my code behind - I simply define the value in the view state! Notice in the ShowState that the property is set immediately so it becomes visible in time for the animation to fire and fade it in.

So now we simply go to the code-behind for our control and set the state. The VSM expects a control when you set the state, but it will traverse the hierarchy and find the state you specify. So while we set the state on our control, it will use the state definitions that are contained within the grid object. We also make sure we set a default state in the constructor. In this case, it will be hidden because the activation manager will call show when it is registered. This is what our new code-behind looks like:


public partial class Bio : UserControl, IPrismView
{
    public Bio()
    {
        InitializeComponent();
        VisualStateManager.GoToState(this, "HideState", false);
    }

    public Bio(ActivationManager activationManager) : this()
    {
        activationManager.Register(this);
    }

    #region IPrismView Members

    public void Show()
    {
        VisualStateManager.GoToState(this, "ShowState", true);
    }

    public void Hide()
    {
        VisualStateManager.GoToState(this, "HideState", true);
    }

    public string ViewName
    {
        get { return "Bio"; }
    }

    #endregion
}

As you can see, we simply move to a new state and leave it up to the designer to add any fancy animations or other transitions when that happens.

Getting Along with Search Engines

The final piece here is to get along with search engines. Deep linking is nice if you just want to add it to your favorites and jump right in, but doesn't really do much for search engines when they are scanning the page. The search enginges need to have some points like a page title and description to properly index your pages.

In the common project, I created a class called SEOHelper to contain my title, description, and keywords. It exposes a method called PageUpdate that then parses these values into the HTML page. Let's take a look at the class:


public class SEOHelper
{
    private const string TEMPLATE = "metatags = document.getElementsByTagName(\"meta\");"
        + "for (x=0;x<metatags.length;x++) {{"
        + "var name = metatags[x].getAttribute(\"name\");"
        + "if (name == '{0}'){{"
        + "var content = metatags[x].getAttribute(\"content\");" 
        + "metatags[x].setAttribute('{0}','{1}');break;}}}}";

    private List<string> _keywords = new List<string>();

    public string Title { get; set; }

    public string Description { get; set; }

    public void AddKeyword(string keyword)
    {
        _keywords.Add(keyword);
    }

    public void UpdatePage()
    {
        HtmlPage.Document.SetProperty("title", Title);

        string titleSet = string.Format(TEMPLATE, "title", Title);
        HtmlPage.Window.Eval(titleSet);

        string descriptionSet = string.Format(TEMPLATE, "description", Description);
        HtmlPage.Window.Eval(descriptionSet);

        if (_keywords.Count > 0)
        {
            StringBuilder keywordList = new StringBuilder();
            bool first = true;
            foreach (string keyword in _keywords)
            {
                keywordList.Append(keyword);
                if (first)
                {
                    first = false;
                }
                else
                {
                    keywordList.Append(",");
                }
            }
            string keywordSet = string.Format(TEMPLATE, "keywords", keywordList);
            HtmlPage.Window.Eval(keywordSet);
        }
    }
}

I'm not a big fan of burying JavaScript inside of C# classes, so I included it here only to help keep the solution in one place. Ordinarily I'd have some methods defined in a .js file that this would link to.

Setting the title is a call into the document object. Setting the meta tags is a little more convuluted. You could create the tags but in this example, I'm stubbing out empty title, description, and keywords meta-tags, then finding them and updating the content attribute. The TEMPLATE contains the code to iterate the tags, then update the content attribute. The PageUpdate method simply updates the meta tag name and content then asks the page to evaluate the javascript.

Now we just place the object inside of the appropriate view and call it when the view becomes visible. Here is the new Home.xaml.cs:


public partial class Home : UserControl, IPrismView
{
    SEOHelper _helper = new SEOHelper { Title = "Home Page", Description="Home page for the website." }; 

    public Home()
    {
        InitializeComponent();
        VisualStateManager.GoToState(this, "HideState", false);
        _helper.AddKeyword("Home");
        _helper.AddKeyword("SEO"); 
    }

    public Home(ActivationManager activationManager)
        : this()
    {
        activationManager.Register(this);
    }

    #region IPrismView Members

    public void Show()
    {
        VisualStateManager.GoToState(this, "ShowState", true);
        _helper.UpdatePage();
    }

    public void Hide()
    {
        VisualStateManager.GoToState(this, "HideState", true);
    }

    public string ViewName
    {
        get { return "Home"; }
    }

    #endregion
}

As you can see, the helper takes in the settings for the page, then whenever the view is shown (the Show) method, the helper is called to update the page. For testing, this could be abstracted even further to a ISEOHelper to avoid trying to actually access the html page when its not needed.

Important Note: as pointed out by Alexey Zakharov in the comments, most search engines won't be able to fire up the Silverlight plug-in and run the XAP to even affect the page to "see" the meta data. To be able to truly have it all indexed, you'll still either need to have a separate page per link (which isn't too efficient with XAP files loading) or have a JavaScript stub independent of the Silverlight plugin that updates this information ... the search engines DO understand javascript for the most part. Thanks for that and I'll probably blog a post about it later, unless I suddenly start seeing my content show up on Google links.

Trimming the XAP Size

Last but not least is trimming the XAP size. I put most of my references in the main XAP file, so they aren't needed in the modules. By going to the project properties, I can check "Reduce XAP size by using appliciation library caching." This will create a new ZIP file that contains some of the referenced controls on my modules when they build, and not include them in the main XAP. Fortunately, these are already loaded by the main XAP, so there is nothing more to do when the XAP is loaded.

There it is ... I know this was a long blog post for what, in the end, is a simple page with two tabs. However, I hope this has proven that you can combine Prism with Silverlight Navigation and use dynamic module loading will keeping your pages search engine friendly. The VSM piece might have made sense in a separate post but I also wanted to get a good example out there that shows how to wire it in when you're not making a custom control or using Blend. Enjoy!

Download the source

View the demo

Jeremy Likness

36 comments:

  1. Hi Jeremy,

    Your SEO helper won't work. Google bot (And i think bing bot too) cannot use Silverlight. That is why bot won't get any meta.

    Best regards,
    Alexey Zakharov.
    Blog: http://weblogs.asp.net/alexeyzakharov/

    ReplyDelete
  2. Absolutely right. It looks nice to the end user but the search engines don't get that far. So I'm guessing best case scenario is to have a javscript stub that updates these indepedent of the Silverlight host based on the entry URL, and have some "helper links" on the main page. Will blog about that too, unless you have a solution you wanted me to point to. Thanks for bringing up the key piece, important for people to know! I've updated the blog content as well.

    ReplyDelete
  3. The link to your demo appears to be broken. otherwise, this was a nice quick runthrough.

    ReplyDelete
  4. I was unable to get the download code to run. The following exception is thrown:


    Microsoft JScript runtime error: Unhandled Error in Silverlight Application
    Code: 4004
    Category: ManagedRuntimeError
    Message: Microsoft.Practices.Composite.Modularity.ModuleTypeLoadingException: Failed to load type for module JeremyLikness.ModuleHome.InitModule. Error was: An exception occurred during a WebClient request.. ---> System.Net.WebException: An exception occurred during a WebClient request. ---> System.NotSupportedException: The URI prefix is not recognized.
    at System.Net.WebRequest.Create(Uri requestUri)
    at System.Net.WebClient.GetWebRequest(Uri address)
    at System.Net.WebClient.OpenReadAsync(Uri address, Object userToken)
    --- End of inner exception stack trace ---
    --- End of inner exception stack trace ---
    at Microsoft.Practices.Composite.Modularity.ModuleManager.HandleModuleTypeLoadingError(ModuleInfo moduleInfo, Exception exception)
    at Microsoft.Practices.Composite.Modularity.ModuleManager.OnModuleTypeLoaded(ModuleInfo typeLoadedModuleInfo, Exception error)
    at Microsoft.Practices.Composite.Modularity.XapModuleTypeLoader.OnDownloadCompleted(Object o, DownloadCompletedEventArgs e)
    at Microsoft.Practices.Composite.Modularity.FileDownloader.WebClient_OpenReadCompleted(Object sender, OpenReadCompletedEventArgs e)
    at System.Net.WebClient.OnOpenReadCompleted(OpenReadCompletedEventArgs e)
    at System.Net.WebClient.OpenReadOperationCompleted(Object arg)

    ReplyDelete
  5. The reason for the error in my previous post was that the Main project required a reference to the Bio and Home projects. Once thopse were added the solution ran as expected.

    ReplyDelete
    Replies
    1. thanks...with all the 'loose coupling' talks, I didn't guess that immediately. also because the screen seems to load right before the error...

      Delete
  6. Question

    How do the views get destroyed ?

    You are opening more than one view in the same region and only setting the view as this.Visibility = Visibility.Collapsed.

    This means that if you navigate to Bio, you will still have the home view instantiated and in the region, not visible, but collapsed.

    ReplyDelete
  7. The views expose a show and hide which uses the visual state manager to transition to a show and hide state. The hide state collapses the visibility of the control. The navigation manager broadcasts the navigation event, and views that are part of it call their show method, and views that aren't, call their hide method.

    ReplyDelete
  8. With this example...
    How sent parameters between views?
    Example:
    "/Map/21"
    Where 21 es Identity
    This URL is friendly with SEO ;)

    ReplyDelete
  9. Hi Jeremy - I'm working on implementing Dynamic loading of my PRISM modules as explained in your post.
    Trouble is when I run your sample, I get an error.

    - AbsoluteUri 'uri.AbsoluteUri' threw an exception of type 'System.InvalidOperationException' string {System.InvalidOperationException}

    I've spent ages trying to figure out what the problem is but it's not obvious to me. I've tried many different combinations of th Uri string but nothing is working.

    Can you help?
    Thanks - Simon

    ReplyDelete
  10. There could be two issues with it, first, it may be an issue with running interactive vs. publishing to a web site and running. Second, it may be struggling with your Uri. Have you tried debugging and looking at the value you are passing as a uri? It may be improperly formatted.

    ReplyDelete
  11. Thanks Jeremy,

    I've tried running in IIS and using dev web server but both fail.
    I'm simply running your example trying to get that to work (my app has same problem) - and the line that fails is...

    Uri uri = new Uri("/JeremyLikness.Main;component/ModuleCatalog.xaml", UriKind.Relative);

    I've tried many options but this also fails...

    Uri uri = new Uri("JeremyLikness.Main;component/ModuleCatalog.xaml", UriKind.Relative);

    the uri returns an InvalidOperationException.

    It looks like it could be something with my environment setup?

    ReplyDelete
  12. Hi Jermy,

    I had implemented the progress Bar in Prism SL app by taking a rectangle(as u said) and setting a property to its visibilty. Every thing is fine but i need to show the progressbar more than 10 times(Views) different places.

    I think its not a good option to make Rectangle(progressBar) to cover view each and every time.

    Any Help to proceed

    ReplyDelete
  13. Further to this Jeremy. the application failing to load problem was not with the Uri - that worked fine. Unbelievably it appears it was simply due to do not having set a startup project and start page!

    On another note, my application is dynamically loading modules now, however for some reason and I think it's because I'm not using the Navigation Framework (I built my own navigation system before it came out), I have to click the button which calls LoadModule and publishes the event to indicates the module needs to be loaded, twice. Simply what I need is to hook into an event that indicates when the module is loaded, but there's not one publicly available. ModuleTypeLoadedCallback is private in the CAL.
    Do you know how I could pass a delegate to indicate when a module has been loaded? LoadModule returns nothing.
    Thanks - Simon

    ReplyDelete
  14. Simon,

    What version are you coding in? I'd suggest looking into the DeploymentCatalog from MEF. This has a nice set of events that update the progress of the download as well as provide a download completed event. I have more recent posts to show how to swap the PRISM module loading with MEF.

    ReplyDelete
  15. Hi Jeremy,

    Thanks for the great tutorials. I am going building my application following your tutorials and will swap prism for mef too :).

    One question though, did the Module projects have to be Navigation applications as well.

    Thanks.
    CC.

    ReplyDelete
  16. Also any reason why the a resx file template was used for the ModuleCatalog.

    Thanks,
    CC

    ReplyDelete
  17. No, they can be regular Silverlight applications. We don't use a RESX file as the template but a Resource Dictionary (the Silverlight Resource Dictionary is a XAML) because it generates the shell without code behind.

    ReplyDelete
  18. Jeremy,
    Can you inject multiple views in a single region, or is it have to be a 1-to-1 mapping.
    If yes, can you have a small demo for me.
    Thnx,
    Zullu.

    ReplyDelete
  19. You can. Use an ItemsControl, stack panel or other control that allows multiple children and then write a navigation manager that swaps the views in/out of visibility.

    Sorry, no time to write the demo but hope you get the gist of the architecture.

    ReplyDelete
  20. Hi Jeremy, Thanks for this article. The InitModule.Initialize methods in your example all use view discovery. If I swap this :

    _regionManager.RegisterViewWithRegion("ModuleRegion", typeof(Bio));

    for this:

    //get the module region
    IRegion moduleRegion = this._regionManager.Regions["ModuleRegion"];

    //get a view to inject into the moduleregion
    var moduleView = new Home();

    //inject it!
    IRegionManager moduleRegionManager = moduleRegion.Add(moduleView, "Home", true);
    moduleRegion.Activate(moduleView);

    then the views fail to load correctly as this :

    public Bio(ActivationManager activationManager) : this()
    {
    activationManager.Register(this);
    }

    never gets called.

    I would like to view use view injection. Do you know why view injection results in the constructor with the activationManager parameter not to be called?

    Thanks
    Kevin

    ReplyDelete
  21. Hi Jeremy, I figures this out. I needed to do this :
    var moduleView = _container.Resolve();

    instead of this :

    var moduleView = new Home();

    Thanks
    Kevin

    ReplyDelete
  22. Hi Jeremy

    Above you state ..

    "Now the navigation will still create a new instance of Module every time we navigate, but the module will simply reuse the same container for the views. This ensures that the views persist once they are added and don't have to be reloaded or recreated."

    What if DO NOT want some of my views to persist?

    Maybe some I do, because the data displayed on them will not change, but some I do not, because I need to create a new instance each time.

    E.g I have a prism module that contains a view that lists employees. I choose an employee and click a button to navigate to edit the employee. Another prism module is used to display a employee edit view. I then navigate back to the list of employees view (which has been persisted) and pick another employee. Because the employee edit view has also been persisted it just shows the previous employee edit view with the previously selected employee. I wanted a new instance with the newly selected employee.

    Any ideas?

    Thanks
    Kevin

    ReplyDelete
  23. That scenario I typically handle via data-binding. There is no need to recycle the view, it is just a prism through which you project the "light" of your data to split it into "information" for the user. Therefore, when the user selects a new employee, you simply need to notify the view model for the employee detail view that the selection has changed, so when it is navigated back to, it reflects the newly selected employee.

    ReplyDelete
  24. to Jeremy Likness:
    In my case, I want to separate into a separate module menu - LinksStackPanel. So how do I do? Please show me how to solve. Thanks a lot.

    ReplyDelete
  25. I was able to convert this solution to Visual Studio 2010 and Silverlight 4 by doing the following:

    - Open the solution in Vs2010.

    - Follow the conversion wizard, answer yes when it asks to convert the ASP.Net site to .Net 4.0.

    - In each Silverlight project, remove references to the Prism assemblies included in JeremyLikness.Main\CAL and replace them with references to the latest Prism 2.2 assemblies.

    - Change each Silverlight project to target Silverlight 4.

    - In JeremyLikness.Main\Bootstrapper.cs, add "
    using Microsoft.Practices.Unity;" to the top of the file so that the IUnityContainer extension methods work (Container.Resolve() and Container.RegisterInstance()).

    ReplyDelete
  26. Hello Jeremy,

    Thank you for the post.

    However my main application is downloading another xap that has got its own navigation with the left menu items.
    Now when I click on these left menu item it is reporting for the XAML not found.

    Assuming it is using one NavigationService, how can we go about it?

    Please give me a direction and I am sure many other would want to know that.


    Please do help me.

    ReplyDelete
  27. I am stil using the Silverlight 3 example and i have a question. When i add a button to the home page directing to the Bio page i get the bio page but the nav bar is never changed it still has the home highlighted. how could i get around this? or better what am i missing?

    ReplyDelete
  28. Thanks for this - it's a very useful tutorial.

    Am wondering if it's possible to dynamically load modules from the client side? I need to write an out-of-browser app which can work disconnected from the server, but which also can be easily extended - i.e. by adding more modules to it dynamically. If I use the above approach it seems it pulls the modules direct from the server, when required. Hopefully there is a possibility of downloading the xaps/dlls at the OOB install time then storing them in the isolated storage and loading dynamically from there?

    Any ideas would be very much appreciated.

    ReplyDelete
  29. Great article.

    I was able to make Silverlight Navigation framework, MVVM and Prism work together in my application.

    Thanks.

    ReplyDelete
  30. Hi Jeremy,

    I have download attached code and it is working fine for me. I have 1 question here:
    In addition to provided demo, i want to use Domain Services, Could you please let me know where I have to add Domain Service so that I can get Domain Service Reference in ModuleHome or ModuleBio where I needed.

    Regards,
    Neeraj

    ReplyDelete
  31. Thank you so much for this amazing post. Following the explanation I could setup my project to have dynamic module loading without the caveat of memory leak. I modified your code a little bit to get it to work without using a GridRegionAdapter following the lead from NVenhola at John Papa's site.

    ReplyDelete
  32. Hi Jeremy,
    I have multiple modules and they will be saved remotely in the same domain. The ure of the module would be like:
    http://mysite/modules/modulea.xap.

    How do I need to configure ModuleInfo in ModuleCatalog in order to aciewe that?

    ReplyDelete
  33. Hi Jeremy,

    Great article. I see in your module catalogue you have hard coded the version to 1.0.0.0. Any idea how to handle this on a build server? I have a solution using PRISM, and Castle Windsor to do the IOC and dymanic module loading. It works but I've also had to hard code the version number in the module catalogue which means that it never works on the build server when it is overriden. I've searched all over and a solution doesn't seem to be documented apart from to create a post build event to interrogate the xaml and change it which sounds.. well nasty!

    ReplyDelete
  34. @Phil,

    Part of why I wrote Jounce was because I found the module implementation in Prism to be way too complex for what it does in Silverlight. Now I simply use dynamic XAPs and have a far easier time of it.

    ReplyDelete
  35. Is any version in plan with using PRISM 4 and Silverlight 5?

    ReplyDelete