Wednesday, September 15, 2010

Best Practices for Themes in Modular Silverlight Applications

When building large Silverlight applications, it makes sense to build sets of styles that can be shared across the application. Many examples on the web illustrate placing these in the App.xaml file and then you are off to the races. Unfortunately, when you are building modular applications, it's not that simple. The dependent modules still require design-time friendly views, and to make that happen requires a few steps.

The Central Theme

The first step is to take your theme for the application and place it in a separate project. These are themes that only have dependencies on the core Silverlight controls. What you want is a simple project that any application or module can reference to pull in the theme.

Create a new Silverlight class library, then add a ResourceDictionary to house your themes. In larger projects, it makes sense to break out themes into smaller pieces, like this:

Resource Dictionaries

Then, you can aggregate these into your main dictionary, called Theme.xaml or something similar. It is important that you load the child dictionaries in order. For example, building blocks like colors and gradients will come before more complicated control templates that use the colors and gradients. Your Theme.xaml might look like this:

<ResourceDictionary
   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">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="ColorStyles.xaml" />
        <ResourceDictionary Source="BrushStyles.xaml" />
        <ResourceDictionary Source="TextStyles.xaml" />
        <ResourceDictionary Source="ButtonStyles.xaml" />
        <ResourceDictionary Source="ComboBoxStyles.xaml" />
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

A Point of Reference

As you build your styles, you may end up including references to things like the Silverlight Toolkit or some third-party set of custom controls. This is fine, but you may be surprised when you fire up your project and suddenly the application crashes because it's unable to resolve any styles that have prefixed targets (i.e. data:). The reason has to do with how Silverlight resolves and evaluates references.

The easiest way to manage this is simple:

In your theme project, for every reference you add, right click and choose copy local: false. The references will work, but they won't be associated with that project. In your main project, the one that contains the App.xaml, add every reference that the theme depends on (and leave them as copy local: true). This ensures the DLLs are available when the theme is pulled down.

Setting up the Main Module

In the main module that drives your application, the one with the App.xaml, you can simply reference the theme project and then merge the resources in. Your App.xaml will contain something like this:

<Application.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>                                
            <ResourceDictionary Source="/MyApp.MyThemeProject;component/Theme.xaml"/>
        </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

The path is known as pack format in its abbreviated form. The first part is the full name of the assembly that the theme resides in. The second part always has component/ followed by the path to your main theme. In this case, ours is in the root. This is all it takes to pull that theme into your application.

Dynamically Loaded Themes

What if you are trying to cut down on load time, so your main application (the one with App.xaml), doesn't have a reference to the theme or the dependent assemblies? This is often the case because the control libraries are rather large. You won't be able to merge the theme in the App.xaml because the required references don't exist.

The solution is to move all of the theme logic to the main module you load after your startup module. A common pattern is to have a small "stub" that is the root application, then show a friendly message to the user while you load the main application components.

First, move the reference to the theme to the module you are dynamically loading. Second, add the dependent references (such as the toolkit) to that module instead of your main module. Finally, when the module is loaded, you'll have to perform some magic to get the theme into the application.

Once the dynamic module is loaded, you will execute this code to pull the theme into the application:

const string MAINTHEME = "/MyApp.MyThemeProject;component/Theme.xaml";
var rd = new ResourceDictionary
                         {
                             Source = new Uri(MAINTHEME, UriKind.Relative)
                         };
Application.Current.Resources.MergedDictionaries.Add(rd);

That uses the same pack notation but programmatically loads the theme into the application-level resources.

Get by with a Little Help from my Blend

Finally, there is one more hurdle to jump. When you create your dynamic modules, you can reference the theme to your heart's content, but the designer isn't going to know about it. Both Blend and Cider (the built-in designer with Visual Studio 2010) rely on hints such as the App.xaml to find themes. Modules don't have their own App.xaml, so the theme isn't found.

If you have a solution for making this work in Cider, let me know - I haven't found anything satisfactory, but I do know what works in Blend.

First, the latest Blend will often come to your rescue the first time you edit the item in Blend. When it finds a missing style, it will prompt you with the following dialog:

Blend Dialog

From there, you can pick which resource dictionaries from which assemblies to use in the designer.

Behind the scenes, Blend creates a file called DesignTimeResources.xaml under the Properties folder. You can create this file yourself if you wish to be proactive about integrating your themes in the designer.

The contents simply contain the now-familiar merged dictionaries that you wish to have available:

<ResourceDictionary
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/MyApp.MyThemeProject;component/Theme.xaml"/>
    </ResourceDictionary.MergedDictionaries>
    <!-- Resource dictionary entries should be defined here. -->
</ResourceDictionary>

At this point, you should be fine in both runtime and Blend. If anyone nows how to make themes available at design-time in Cider, please let us know through the comments below.

Jeremy Likness