Download Source Code

Task-It Series

This post is part of a series of blog posts and videos about the Task-It (task management) application that I have been building with Silverlight 4 and Telerik's RadControls for Silverlight 4. For a full index of these resources, please go here. One of the posts listed in the index provides a full source download for the application.

The need for lazy loading

In my MEF into post, MEF to the rescue in Task-It, I outlined a couple of issues I was facing and explained why I chose MEF (the Managed Extensibility Framework) to solve these issues.

The first issue was the fact that my application startup time was getting slower and slower as the size of the app grew. My app currently only has 4 pages, but as a user enters the app, they may only hit only one of those pages, so why load the code for all 4 if they will never be used. If they could be ‘lazy loaded’, it would increase the startup time (and I wouldn’t have to wait as long every time I start up the app during the development process).

Other posts to check out

There are a few other resources out there around dynamic XAP loading that you may want to review (by the way, Glenn Block is the main dude when it comes to MEF):

Glenn Block’s 3-part series on a dynamically loaded dashboard

Glenn and John Papa’s Silverlight TV video on dynamic xap loading

These provide some great info, but didn’t exactly cover the scenario I wanted to achieve in Task-It…and that is dynamically loading each of the app’s pages the first time the user enters a page.

The code

In the code I provided for download above, I created a simple solution that shows the technique I used for dynamic XAP loading in Task-It, but without all of the other code that surrounds it. Taking all that other stuff away should make it easier to grasp. Having said that, there is still a fair amount of code involved. I am always looking for ways to make things simpler, and to achieve the desired result with as little code as possible, so if I find a better/simpler way I will blog about it, but for now this technique works for me.

When I created this solution I started by creating a new Silverlight Navigation Application called DynamicXAP Loading. I then added the following line to my UriMappings in MainPage.xaml:

<uriMapper:UriMapping Uri="/{assemblyName};component/{path}" MappedUri="/{assemblyName};component/{path}"/>

In the section of MainPage.xaml that produces the page links in the upper right, I kept the Home link, but added a couple of new ones (page1 and page 2). These are the pages that will be dynamically (lazy) loaded:

<StackPanel x:Name="LinksStackPanel" Style="{StaticResource LinksStackPanelStyle}">
     <HyperlinkButton Style="{StaticResource LinkStyle}" NavigateUri="/Home" TargetName="ContentFrame" Content="home"/>
     <Rectangle Style="{StaticResource DividerStyle}"/>
     <HyperlinkButton Style="{StaticResource LinkStyle}" Content="page 1" Command="{Binding NavigateCommand}" CommandParameter="{Binding ModulePage1}"/>
     <Rectangle Style="{StaticResource DividerStyle}"/>
     <HyperlinkButton Style="{StaticResource LinkStyle}" Content="page 2" Command="{Binding NavigateCommand}" CommandParameter="{Binding ModulePage2}"/>
 </StackPanel>

In App.xaml.cs I added a bit of MEF code. In Application_Startup I call a method called InitializeContainer, which creates a PackageCatalog (a MEF thing), then I create a CompositionContainer and pass it to the CompositionHost.Initialize method. This is boiler-plate MEF stuff that allows you to do 'composition' and import 'packages'. You're welcome to do a bit more MEF research on what is happening here if you'd like, but for the purpose of this example you can just trust that it works. :-)

private void Application_Startup(object sender, StartupEventArgs e)
{
    InitializeContainer();
    this.RootVisual = new MainPage();
}
 
private static void InitializeContainer()
{
    var catalog = new PackageCatalog();
    catalog.AddPackage(Package.Current);
    var container = new CompositionContainer(catalog);
    container.ComposeExportedValue(catalog);
    CompositionHost.Initialize(container);
}

Infrastructure

In the sample code you'll notice that there is a project in the solution called DynamicXAPLoading.Infrastructure. This is simply a Silverlight Class Library project that I created just to move stuff I considered application 'infrastructure' code into a separate place, rather than cluttering the main Silverlight project (DynamicXapLoading). I did this same thing in Task-It, as the amount of this type of code was starting to clutter up the Silverlight project, and it just seemed to make sense to move things like Enums, Constants and the like off to a separate place.

In the DynamicXapLoading.Infrastructure project you'll see 3 classes:

  • Enums - There is only one enum in here called ModuleEnum. We'll use these later.
  • PageMetadata - We will use this class later to add metadata to a new dynamically loaded project.
  • ViewModelBase - This is simply a base class for view models that we will use in this, as well as future samples. As mentioned in my MVVM post, I will be using the MVVM pattern throughout my code for reasons detailed in the post. By the way, the ViewModelExtension class in there allows me to do strongly-typed property changed notification, so rather than OnPropertyChanged("MyProperty"), I can do this.OnPropertyChanged(p => p.MyProperty). It's just a less error-prown approach, because if you don't spell "MyProperty" correctly using the first method, nothing will break, it just won't work.

Adding a new page

We currently have a couple of pages that are being dynamically (lazy) loaded, but now let's add a third page.

1. First, create a new Silverlight Application project:

New Silverlight Application

In this example I call it Page3. In the future you may prefer to use a different name, like DynamicXAPLoading.Page3, or even DynamicXAPLoading.Modules.Page3. It can be whatever you want. In my Task-It application I used the latter approach (with 'Modules' in the name). I do think of these application as 'modules', but Prism uses the same term, so some folks may not like that. Use whichever naming convention you feel is appropriate, but for now Page3 will do.

When you change the name to Page3 and click OK, you will be presented with the Add New Project dialog:

New Silverlight Application

It is important that you leave the 'Host the Silverlight application in a new or existing Web site in the solution' checked, and the .Web project will be selected in the dropdown below. This will create the .xap file for this project under ClientBin in the .Web project, which is where we want it.

2. Uncheck the 'Add a test page that references the application' checkbox, and leave everything else as is.

3. Once the project is created, you can delete App.xaml and MainPage.xaml.

4. You will need to add references your new project to the following:

  • DynamicXAPLoading.Infrastructure.dll (this is a Project reference)
  • DynamicNavigation.dll (this is in the Libs directory under the DynamicXAPLoading project)
  • System.ComponentModel.Composition.dll
  • System.ComponentModel.Composition.Initialization.dll
  • System.Windows.Controls.Navigation.dll

If you have installed the latest RC bits you will find the last 3 dll's under the .NET tab in the Add Referenced dialog. They live in the following location, or if you are on a 64-bit machine like me, it will be Program Files (x86).

      C:\Program Files\Microsoft SDKs\Silverlight\v4.0\Libraries\Client

Now let's create some UI for our new project.

5. First, create a new Silverlight User Control called Page3.dyn.xaml

6. Paste the following code into the xaml:

    xmlns:dyn="clr-namespace:DynamicNavigation;assembly=DynamicNavigation"
    xmlns:my="clr-namespace:Page3;assembly=Page3">
    <my:Page3Host />
</dyn:DynamicPageShim>

This is just a 'shim', part of David Poll's technique for dynamic loading.

7. Expand the icon next to Page3.dyn.xaml and delete the code-behind file (Page3.dyn.xaml.cs).

8. Next we will create a control that will 'host' our page. Create another Silverlight User Control called Page3Host.xaml and paste in the following XAML:

<dyn:DynamicPage x:Class="Page3.Page3Host"
    xmlns:dyn="clr-namespace:DynamicNavigation;assembly=DynamicNavigation"
    xmlns:Views="clr-namespace:Page3.Views" 
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Title="Page 3">
 
    <Views:Page3/>
 
</dyn:DynamicPage>

9. Now paste the following code into the code-behind for this control:

using DynamicXAPLoading.Infrastructure;
 
namespace Page3
{
    [PageMetadata(NavigateUri = "/Page3;component/Page3.dyn.xaml", Module = ModuleEnums.Page3)]
    public partial class Page3Host
    {
        public Page3Host()
        {
            InitializeComponent();
        }
    }
}

Notice that we are now using that PageMetadata custom attribute class that we created in the Infrastructure project, and setting its two properties.

  • NavigateUri - This tells it that the assembly is called Page3 (with a slash beforehand), and the page we want to load is Page3.dyn.xaml...our 'shim'. That line we added to the UriMapper in MainPage.xaml will use this information to load the page.
  • Module - This goes back to that ModuleEnum class in our Infrastructure project. However, setting the Module to ModuleEnum.Page3 will cause a compilation error, so...

10. Go back to that Enums.cs under the Infrastructure project and add a 3rd entry for Page3:

public enum ModuleEnum
{
    Page1,
    Page2,
    Page3
}

11. Now right-click on the Page3 project and add a folder called Views.

12. Right-click on the Views folder and create a new Silverlight User Control called Page3.xaml. We won't bother creating a view model for this User Control as I did in the Page 1 and Page 2 projects, just for the sake of simplicity. Feel free to add one if you'd like though, and copy the code from one of those other projects. Right now those view models aren't really doing anything anyway...though they will in my next post. :-)

13. Now let's replace the xaml for Page3.xaml with the following:

<dyn:DynamicPage x:Class="Page3.Views.Page3"
    xmlns:dyn="clr-namespace:DynamicNavigation;assembly=DynamicNavigation"
    mc:Ignorable="d"
    d:DesignHeight="300" d:DesignWidth="400"
    Style="{StaticResource PageStyle}">
 
    <Grid x:Name="LayoutRoot">
        <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
            <StackPanel x:Name="ContentStackPanel">
                <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}" Text="Page 3"/>
                <TextBlock x:Name="ContentText" Style="{StaticResource ContentTextStyle}" Text="Page 3 content"/>
            </StackPanel>
        </ScrollViewer>
    </Grid>
 
</dyn:DynamicPage>

14. And in the code-behind remove the inheritance from UserControl, so it should look like this:

namespace Page3.Views
{
    public partial class Page3
    {
        public Page3()
        {
            InitializeComponent();
        }
    }
}

One thing you may have noticed is that the base class for the last two User Controls we created is DynamicPage. Once again, we are using the infrastructure that David Poll created.

15. OK, a few last things. We need a link on our main page so that we can access our new page. In MainPage.xaml let's update our links to look like this:

<StackPanel x:Name="LinksStackPanel" Style="{StaticResource LinksStackPanelStyle}">
    <HyperlinkButton Style="{StaticResource LinkStyle}" NavigateUri="/Home" TargetName="ContentFrame" Content="home"/>
    <Rectangle Style="{StaticResource DividerStyle}"/>
    <HyperlinkButton Style="{StaticResource LinkStyle}" Content="page 1" Command="{Binding NavigateCommand}" CommandParameter="{Binding ModulePage1}"/>
    <Rectangle Style="{StaticResource DividerStyle}"/>
    <HyperlinkButton Style="{StaticResource LinkStyle}" Content="page 2" Command="{Binding NavigateCommand}" CommandParameter="{Binding ModulePage2}"/>
    <Rectangle Style="{StaticResource DividerStyle}"/>
    <HyperlinkButton Style="{StaticResource LinkStyle}" Content="page 3" Command="{Binding NavigateCommand}" CommandParameter="{Binding ModulePage3}"/>
</StackPanel>

16. Next, we need to add the following at the bottom of MainPageViewModel in the ViewModels directory of our DynamicXAPLoading project:

public ModuleEnum ModulePage3
{
    get { return ModuleEnum.Page3; }
}

17. And at last, we need to add a case for our new page to the switch statement in MainPageViewModel:

switch (module)
{
    case ModuleEnum.Page1:
        DownloadPackage("Page1.xap");
        break;
    case ModuleEnum.Page2:
        DownloadPackage("Page2.xap");
        break;
    case ModuleEnum.Page3:
        DownloadPackage("Page3.xap");
        break;
    default:
        break;
}

Now fire up the application and click the page 1, page 2 and page 3 links. What you'll notice is that there is a 2-second delay the first time you hit each page. That is because I added the following line to the Navigate method in MainPageViewModel:

Thread.Sleep(2000); // Simulate a 2 second initial loading delay

The reason I put this in there is that I wanted to simulate a delay the first time the page loads (as the .xap is being downloaded from the server). You'll notice that after the first hit to the page though that there is no delay...that's because the .xap has already been downloaded. Feel free to comment out this 2-second delay, or remove it if you'd like. I just wanted to show how subsequent hits to the page would be quicker than the initial one.

By the way, you may want to display some sort of BusyIndicator while the .xap is loading. I have that in my Task-It appplication, but for the sake of simplicity I did not include it here. In the future I'll blog about how I show and hide the BusyIndicator using events (I'm currently using the eventing framework in Prism for that, but may move to the one in the MVVM Light Toolkit some time soon).

Wrap up

Whew, that felt like a lot of steps, but it does work quite nicely. As I mentioned earlier, I'll try to find ways to simplify the code (I'd like to get away from having things like hard-coded .xap file names) and will blog about it in the future if I find a better way.

In my next post, I'll talk more about what is actually happening with the code that makes this all work.


Related Posts

Comments