Easy Composable Theming in WPF (.NET 4)

In .NET 4 there is a new namespace to be aware of and that is System.ComponentModel.Composition. Also known as MEF (Managed Extensibility Framework). This post outlines a very simple way to take advantage of the new Composition tools to add simple themeing to your WPF applications.

http://cid-dfcd2d88d3fe101c.skydrive.live.com/embedrowdetail.aspx/blog/justnbusiness/ComposableThemes.zip

 

 image image

 

Step 1: Create a shared project

The shared project should define interfaces and classes that are needed by both the application and each of the composable parts. In this sample application I have a project called Core which defines a single interface. All other projects reference this core project.

IThemeService.cs

namespace Core
{
    public interface IThemeService
    {
        ResourceDictionary Theme { get; }
    }
}

 

Step 2: Create Theme Projects

A theme project will reference the core project and implement the IThemeService interface.

ThemeXService.cs

using System.ComponentModel.Composition;
using Core;

namespace ThemeX
{
    [Export("ThemeX", typeof(IThemeService))]
    internal class ThemeXService : IThemeService
    {
        public System.Windows.ResourceDictionary Theme
        {
            get { return new Themes.Generic(); }
        }
    }
}

Additionally this project will contain a single resource dictionary class with all of our named resources defined.

Themes\Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:system="clr-namespace:System;assembly=mscorlib"
    xmlns:local="clr-namespace:ThemeX"
    x:Class="ThemeX.Themes.Generic">

    <system:String x:Key="ThemeTitle">Theme X!</system:String>
    <SolidColorBrush x:Key="ThemeBrush" Color="PowderBlue" />
</ResourceDictionary>

Themes\Generic.cs

using System.Windows;

namespace ThemeX.Themes
{
    public partial class Generic : ResourceDictionary
    {
        public Generic()
        {
            this.InitializeComponent();
        }
    }
}

 

Step 3: Create Your Application and Load a Theme

In this application I am loading the first theme at startup then changing the theme based on a selection of a ComboBox. In a real application it might make more sense to store the name of the theme in user settings somehow and load the theme using that value.

Here is the very simple window that is to be themed.

MainWindow.cs

<Window 
    x:Class="ComposableThemes.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:app="clr-namespace:ComposableThemes"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <ComboBox ItemsSource="{Binding ThemeNames, Source={x:Static app:App.Current}}"
                SelectionChanged="ComboBox_SelectionChanged" />
        <Border 
            Grid.Row="1"
            Margin="25"
            BorderThickness="2"
            CornerRadius="5"
            Background="{DynamicResource ThemeBrush}">
            <TextBlock 
                HorizontalAlignment="Center"
                VerticalAlignment="Center"
                Text="{DynamicResource ThemeTitle}" />
        </Border>
    </Grid>
</Window>

Notice the {DynamicResource …} references. These will give warnings in your design view because they cannot be resolved. They will be resolved at runtime because we will be loading the resource dictionary object created in step 2 at startup.

Here is how you can load an exported object and load a resource dictionary at runtime.

App.xaml.cs

public void SetTheme(string themeName)
{
    string location = Path.GetDirectoryName(typeof(App).Assembly.Location);
    using (var addinCatalog = new DirectoryCatalog(location))
    {
        using (CompositionContainer container = new CompositionContainer(addinCatalog))
        {
            if (!string.IsNullOrEmpty(themeName))
            {
                IThemeService theme = container.GetExport<IThemeService>(themeName).Value;
                if (theme != null)
                {
                    var themeDictionary = theme.Theme;
                    if (themeDictionary != null)
                    {
                        this.Resources.MergedDictionaries.Add(themeDictionary);
                    }
                }
            }
        }
    }
}

In this method we are using the applications directory to look for composable parts (plug-ins). A catalog is basically a container that loads assemblies and finds Exports / Imports for you. There is a very handy AggregateCatalog class that will allow you to have multiple directories or multiple assemblies or whatever type of catalog you want.

It’s useful to know that you can also use an Import attribute on a class to have the container automatically bind together the imported and exported parts. I find that to be a little more magical than I prefer so I did it this way instead.

Also, you can get a list of all theme names by inspecting the metadata of exported parts. Here is a snippet I used to populate my ComboBox with theme names:

App.xaml.cs

public IEnumerable<string> ThemeNames
{
    get
    {
        string location = Path.GetDirectoryName(typeof(App).Assembly.Location);
        using (var addinCatalog = new DirectoryCatalog(location))
        {
            foreach (var part in addinCatalog.Parts)
            {
                foreach (var definition in part.ExportDefinitions)
                {
                    var value = (string)definition.Metadata["ExportTypeIdentity"];
                    if (value == typeof(IThemeService).FullName)
                    {
                        yield return definition.ContractName;
                    }
                }
            }
        }
    }
}

 

Conclusion

All in all, I am very happy with the new Composition framework. It’s very easy to use and because of that, very powerful. One thing you should keep in mind however is that there is no magic here when it comes to loading and unloading assemblies, if you use a directory catalog it will load all assemblies in that directory into your current AppDomain and you cannot unload them (as is normal).

For another example of MEF in action check out my MetaTask in the MetaSharp source code. This is how all pipelines are loaded in MetaSharp.

Author: justinmchase

I'm a Software Developer from Minnesota.

Leave a Reply

%d bloggers like this: