WPF Themes – Theme Management
This is the second post in a series about my experience developing a WPF Theme system. In the first post, I talked about my frustration in locating an fully-functional system, and my decision to build my own.
I’m going to start by addressing the problem of Theme Management – how to change the Theme dynamically at runtime without disrupting the rest of the application space. In so doing, I’m also going to solve the issue of extensibility.
As I said before, the way that everyone else seemed to handle Theme Management was to assume that Application.Current.Resources.MergedDictionaries did not contain anything of interest, and simply replaced the contents of this with the Theme Resource Dictionary. Clearly, in an application of any level of complexity, this just wouldn’t do.
I concluded, however, that the essence of the idea – that having the first Resource Dictionary in this collection be the Theme was a pretty good idea. There just needed to be a better approach to getting it there.
A little experimentation determined that the first Resource Dictionary in the MergedDictionaries of the first Resource Dictionary in Application.Current.Resources.MergedDictionaries (that is, an extra level down) was still effectively the first Resource Dictionary in the stack. So, if I could get a Resource Dictionary that I owned to be the first in A.C.R.MD (I’m tired of typing it out), then I could do whatever I wanted to the contents of it.
I created a ThemeManager class that will be used to manage the list of possible Themes, and to set the current Theme. The first time a Theme is set, this class will examine the contents of A.C.R.MD and make a list of the Uris for any Resource Dictionaries in that collection. It would then empty that collection, add a special Resource Dictionary to contain the Themes, and then add all of the previous Resource Dictionaries back into the collection.
private static void AddThemesContainer()
{
lock (_useLock)
{
if (Instance.ThemesContainer == null)
{
List<Uri> preserveList = new List<Uri>();
// Make a list of the URIs of things we want to keep
foreach (ResourceDictionary resource in
Application.Current.Resources.MergedDictionaries)
{
preserveList.Add(resource.Source);
}
// Clear the list
Application.Current.Resources.MergedDictionaries.Clear();
// Add the Global Includes dictionary
ResourceDictionary globalIncludeDictionary = new ResourceDictionary();
globalIncludeDictionary.Source =
new Uri("/SavoyTek.Windows.Themes;component/Themes/GlobalIncludes.xaml",
UriKind.RelativeOrAbsolute);
Application.Current.Resources.MergedDictionaries.Add(globalIncludeDictionary);
DiscoverThemeRegistrars(globalIncludeDictionary);
// Add everything else back in
foreach (Uri uri in preserveList)
{
if (uri != null)
{
ResourceDictionary resource = new ResourceDictionary();
resource.Source = uri;
Application.Current.Resources.MergedDictionaries.Add(resource);
}
}
Instance.ThemesContainer = Application.Current.Resources
.MergedDictionaries[0]
.MergedDictionaries[0];
}
}
}
The GlobalIncludes Resource Dictionary looks like this:
<ResourceDictionary> <s:String x:Key="GlobalIncludes">GlobalIncludes</s:String> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="\SavoyTek.Windows.Themes;component\Themes\ThemesContainer.xaml" /> <ResourceDictionary Source="\SavoyTek.Windows.Themes;component\Themes\GlobalBrushes.xaml" /> <ResourceDictionary Source="\SavoyTek.Windows.Themes;component\Themes\GlobalImages.xaml" /> <ResourceDictionary Source="\SavoyTek.Windows.Themes;component\Themes\CommonStyles.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary>
As you can see, the ThemeManager saves a reference to the ThemesContainer Resource Dictionary. It is this Resource Dictionary that ends up containing the Resource Dictionary for the active Theme. Now that I have a location completely under my control to house the active Theme, changing the Theme is done in essentially the same way as the other Theme Management systems I found.
public static void ChangeTheme(Theme theme)
{
if (theme != GetCurrentTheme())
{
AddThemesContainer();
lock (_useLock)
{
Instance.ThemesContainer.MergedDictionaries.Clear();
Instance.ThemesContainer.MergedDictionaries.Add(theme.ResourceDictionary);
Instance.CurrentTheme = theme;
}
// Signal that the theme changed.
OnCurrentThemeChanged();
}
}
In AddThemesContainer, you may have noticed a call to DiscoverThemeRegistrars. This method uses reflection to find any classes adorned with a custom attribute. It is expected that these classes will derive from ThemeResourceRegistrar. An instance of this class will be created, and it will be given the opportunity to add things (global Styles, Converters, etc.) to the GlobalIncludes Resource Dictionary.
private static void DiscoverThemeRegistrars(ResourceDictionary dictionary)
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
{
try
{
Type[] types = assembly.GetTypes();
foreach (Type type in types)
{
object[] attribs = type.GetCustomAttributes(typeof(ThemeRegistrarAttribute),
false);
if (attribts.Length > 0)
{
ThemeResourceRegistrar registrar =
(ThemeResourceRegistrar)type.GetConstructor(new Type[] { })
.Invoke(new object[] { });
registrar.AddResources(dictionary);
}
}
}
catch (Exception) { }
}
}
The idea is that another Assembly in the application space will have a Registrar class, which will be responsible for adding resources local to that Assembly to the Theme space. It would also be able to register additional Themes with the system.
In the next installment, I’ll talk about custom Markup Extensions that I used in my Themes.