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

9 comments:

  1. Great Explanation!!!

    I like the approach of breaking large theme into smaller. As I think we can have default theme reside in the main Project Module. And if user is looking for different theme, then load it dynamically same as Silverlight Toolkit Theme projects.

    ReplyDelete
  2. The code that loads themes dynamically raise an exception
    Error HRESULT E_FAIL has been returned from a call to a COM component.

    My situation is not unique. See google's search:
    http://bit.ly/9ipwN6

    ReplyDelete
  3. Great point - this link in particular deals with the specific steps to deal with that:

    http://www.dotnetcurry.com/ShowArticle.aspx?ID=371

    ReplyDelete
  4. Ok, Jeremy, I found out what the problem was

    My resources was marked as Embedded resource.
    So, I would add some notice that Build Action for each *.xaml resource should be set to Page (not Embedded Resource or something else)

    ReplyDelete
  5. Hi Jeremy. I have tried this approach, but it does not work. The resources defined in the later dictionaries in the theme cannot reference colors and brushes defined in the earlier dictionaries. Trying to do so gives an error. So, I'm not sure what the point of this is?

    ReplyDelete
  6. Jon, I'd have to see your implementation because we don't have that problem - we use a lot of complex themes and dependency chains and they all work fine - so I'm not sure how your code differs.

    ReplyDelete
  7. I ran across the same issue.

    In ColorStyles.xaml:
    <Color x:Key="SampleColor">#FFFFFFFF</Color>

    In BrushStyles.xaml:
    <SolidColorBrush x:Key="SampleBrush" Color="{StaticResource SampleColor}" />

    You will get the error Cannot find a resource with Name/Key SampleColor.

    Is there a step missing?

    ReplyDelete
  8. sounds like a great approach, but what would be ultra cool would be a small sample app with oooh lets say one of the new themese released for silverlight embedded and demonstrating the entire concept :) maybe one of the basic examples from the prism 4 framework as a starting point...am i asking for too much???

    otherwise awsome stuff...

    ReplyDelete
  9. Hi, is there any way to share resources in a silverlight control library?. Just like if if have an app.xaml file for my class library

    ReplyDelete