Saturday, July 25, 2009

Switching WPF interface themes at runtime

WPF introduced the use of dynamic resource references. They're very helpful in rich application development. The dynamic resources are loaded at runtime, and can be changed at any time. Dynamic resources could be styles, brushes, etc. WPF however does not provide a convenient way of changing the resource references. That's why I've created a simple class that allows the developers to switch different resources very easy.

Using dynamic resources

Creating and applying a dynamic resource is easy in WPF. First, we need to define the resource:


Then, we have to reference the resource like this:



And that's it. WPF searches for a resource with the given key and applies it when it's found.


Changing the dynamic resources at runtime


As I said, dynamic resources are applied at runtime, that means they can be changed. Let's assume that we have defined several resources like brushes and styles, and we want them to be in two variants - for example a red theme and a blue theme for user interfaces. We put all of the resources for each theme in a different resource dictionary file. For this example, I use the WPF Themes which can be found here: http://wpf.codeplex.com/Wiki/View.aspx?title=WPF%20Themes. So, assume that we have two resource dictionaries:

ShinyBlue.xaml
ShinyRed.xaml

Without these themes, a WPF window should look something like this:



Using the themes is pretty easy, all we need to do is merge one of the resource dictionary to the resources of root control or window:



And now, the window looks like that:



Pretty neat, huh? OK, that's good, but what if we want to change the theme to the red one? We have to go in the XAML code, change the source of the merged resource dictionary and recompile. No way! We should be able to come up with something nicer.

Well, there's a solution. See, everytime when the resources of a framework element are changed (added or removed resources), WPF goes through all elements with dynamic resource references and updates them accordingly. So the solution should be obvious - when we need to change the theme, we can simply remove the old merged dictionary, and add the new one. I searched a bit in the internet, and the most common solution is to clear all merged dictionaries from the collection and then add the desired one. Yes, allright, that would work. But what if the developer has added more than one resource dictionary, not only the one with the theme resources? Everything goes away, and bang, the software is not working. So there should be a proper way of detecting which resource dictionary contains the theme resources, and leave the other dictionaries alone.

It sounds a bit complicated, right? First, search for the right resource dictionary, then remove it from the list of merged dictionaries, then load the new one, and apply it. Yes but what if it could be done jyst by setting one single value to one signle property, and all is OK?

The ThemeSelector class

So there is it. The solution. I created a class which has an attachable property - the URI path to the desired theme dictionary. Now, let's think about finding the right dictionary to be removed when themes are being switched. Kinda obvious solution is a new class that inherits ResourceDictionary. Then, we search all merged dictionaries and remove those which are of this new type. Pretty simple, right? Here's the class:


public class ThemeResourceDictionary : ResourceDictionary
{
}

So it's time to see the real deal.

public class MkThemeSelector : DependencyObject
{

public static readonly DependencyProperty CurrentThemeDictionaryProperty =
DependencyProperty.RegisterAttached("CurrentThemeDictionary", typeof(Uri),
typeof(MkThemeSelector),
new UIPropertyMetadata(null, CurrentThemeDictionaryChanged));

public static Uri GetCurrentThemeDictionary(DependencyObject obj)
{
return (Uri)obj.GetValue(CurrentThemeDictionaryProperty);
}

public static void SetCurrentThemeDictionary(DependencyObject obj, Uri value)
{
obj.SetValue(CurrentThemeDictionaryProperty, value);
}

private static void CurrentThemeDictionaryChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is FrameworkElement) // works only on FrameworkElement objects
{
ApplyTheme(obj as FrameworkElement, GetCurrentThemeDictionary(obj));
}
}

private static void ApplyTheme(FrameworkElement targetElement, Uri dictionaryUri)
{
if (targetElement == null) return;

try
{
ThemeResourceDictionary themeDictionary = null;
if (dictionaryUri != null)
{
themeDictionary = new ThemeResourceDictionary();
themeDictionary.Source = dictionaryUri;

// add the new dictionary to the collection of merged dictionaries of the target object
targetElement.Resources.MergedDictionaries.Insert(0, themeDictionary);
}

// find if the target element already has a theme applied
List existingDictionaries =
(from dictionary in targetElement.Resources.MergedDictionaries.OfType()
select dictionary).ToList();

// remove the existing dictionaries
foreach (ThemeResourceDictionary thDictionary in existingDictionaries)
{
if (themeDictionary == thDictionary) continue; // don't remove the newly added dictionary
targetElement.Resources.MergedDictionaries.Remove(thDictionary);
}
}
finally { }
}
}

As I said, the class as one dependency property and an callback method to handle the event of changing the value of this property. There everything is straight-forward. First, the new theme dictionary is loaded, and then the old one is removed. That's it.

Using the ThemeSelector class

Here comes the nice part. The usage of the class is as simple as changing the value of one single property.
        private void ChangeToRedTheme()
{
MkThemeSelector.SetCurrentThemeDictionary(this, new Uri("/ThemeSelector;component/Themes/ShinyRed.xaml", UriKind.Relative));
}
When this method is called, the theme changes:


The ThemeSelector class and WPF data binding

We can even use databinding to change the themes dynamically. Let's assume that we have a combo box which have two items - the red theme, and the blue theme:


Now, on the element to which we want to apply the theme, for example the root grid in the window, we set the following binding expression:
local:MkThemeSelector.CurrentThemeDictionary="{Binding ElementName=cmbThemes, Path=SelectedItem.Tag}"

So it looks like this:


And that's all. When we run the application, we have this combo box, allowing us to select the theme in runtime.



Download sample application

The sample can be downloaded here: Download sample

9 comments:

  1. Great article, but what is in your code list (2) on line 58 ?
    I have a namespace error with it on line 44

    ReplyDelete
  2. This's just a bug of the syntax highlighter when using generics. Best way to see it now is to download the sample app. I'll try to fix the code samples later.

    ReplyDelete
  3. Hi, looks like when i apply the new one
    the old theme still applies to some of the controls on the page, why?

    and the download sample link seems not working

    ReplyDelete
  4. you probably forgot to remove the old resource dictionary from the merged dictionaries of the element's resources.
    oh, and the download link is working now.

    ReplyDelete
  5. Thank you for a fine post. Your code works like a charm (with VS 2008 Team Edition).

    ReplyDelete
  6. Great post!
    I tried the FileFactory Download and gave up after the 15th redirect. SkyDrive is free and not cary like FileFactory.

    ReplyDelete
  7. Is there any chance you could help me convert this from using a combobox to change style to a radiobutton? The is giving me problems. I need to use input from radiobutton instead

    ReplyDelete
  8. thank you for ur post.I added a button in the window,on the click event of the window i loaded another window say window2 but the theme is not affect the second window window2 my code is

    private void button1_Click(object sender, RoutedEventArgs e)
    {
    Window2 obj = new Window2();
    obj.Show();
    }

    ReplyDelete
  9. i'm not able to download the file from file factory. Please help me out.

    ReplyDelete