Sunday, July 26, 2009

Switching WPF interface themes at runtime, Part 2

In my previous post, Switching WPF interface themes at runtime
,I explained how we can easily switch between interface themes at runtime using a simple property and even databinding. That was OK for a single element, or a single window. But what about if we have an application with a lot of windows? Not that hard, just set the property on every window. Yes, that would work, but it has some disadvantages. For example, we change the theme on one window, then we must implement some logic to change it to all other windows that are open, otherwise you are left with windows on the same application with different themes, and that's just ugly. It also can introduce a lot of bugs. And, it is also very boring.

Using a global theme for the application

The idea is that we want to have a theme that should be applied on all of the windows of the application and that can be switched easily at a central location. So I continued working on my ThemeSelector class. I introcuded a property called Global Dictionary, which would be used for all elemenst registered with the theme selector. I also added another attachable property, which is boolean and tells the theme selector whether the element should use the global theme or not.
Let's take a look at the new code:


#region Global Theme

private static List elementsWithGlobalTheme = new List();

private static Uri globalThemeDictionary = null;

public static Uri GlobalThemeDictionary
{
get { return globalThemeDictionary; }
set
{
globalThemeDictionary = value;

// apply to all elements registered to use the global theme
foreach (FrameworkElement element in elementsWithGlobalTheme)
{
if (GetApplyGlobalTheme(element))
ApplyTheme(element, globalThemeDictionary);
}
}
}

public static readonly DependencyProperty ApplyGlobalThemeProperty =
DependencyProperty.RegisterAttached("ApplyGlobalTheme", typeof(bool),
typeof(MkThemeSelector),
new UIPropertyMetadata(false, ApplyGlobalThemeChanged));

public static bool GetApplyGlobalTheme(DependencyObject obj)
{
return (bool)obj.GetValue(ApplyGlobalThemeProperty);
}

public static void SetApplyGlobalTheme(DependencyObject obj, bool value)
{
obj.SetValue(ApplyGlobalThemeProperty, value);
}


private static void ApplyGlobalThemeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
if (obj is FrameworkElement)
{
FrameworkElement element = obj as FrameworkElement;
if ((bool)e.NewValue) // if property is changed to 'true', then add to the list of elements and apply theme
{
if (!elementsWithGlobalTheme.Contains(element))
elementsWithGlobalTheme.Add(element);

// apply the theme
ApplyTheme(element, GlobalThemeDictionary);
}
else
{
if (elementsWithGlobalTheme.Contains(element))
elementsWithGlobalTheme.Remove(element);

// apply the local theme instead of the global
ApplyTheme(element, GetCurrentThemeDictionary(element));
}
}
}

#endregion
That's all we need. When the ApplyGlobalTheme is set to true on an element, it starts using the global theme. If this is set to false, the element is switched back to the local theme set with the ThemeSelector, or to no theme at all. When the global theme is changed, the code searches all elements that use it and apply the new one. That's not something very complicated but it makes our lives a lot easier. The ApplyTheme(..) method is the same method I used on my previous post, you can look it up there to understand it better. But just in case, I will post it here:


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 { }
}

Using the Theme Selector with the global theme


Changing the global theme is easy. Just one line:



MkThemeSelector.GlobalThemeDictionary = new Uri("/ThemeSelector;component/Themes/ShinyRed.xaml",
UriKind.Relative);

And that's it. All of the elements that use the global theme will be switched. But how to tell the element that it uses the global theme? Just set the ApplyGlobalTheme attached property to true.




Let's see some screenshots.

This is a two-windows application with the no theme applied.



Let's see what happens when we change the global application theme using the Theme Selector.



Great. Both windows switched their themes. With just only one line of code.

Download Sample

The sample application can be downloaded here.

9 comments:

  1. You need to make your site wider. Code samples are very difficult to read.

    ReplyDelete
  2. You can download the sample code, it's much easier to read and also it is a working app, not just a code segment, so you'll be able to understand it better.

    ReplyDelete
  3. I desperately want to download the sample but cant because of the dreadful Filefactory site. Its plastered with adverts and no visible way of downloading the file. Any help?

    ReplyDelete
  4. Give me your e-mail address, I'll send you the sample

    ReplyDelete
  5. Nice solution .
    But i must ask, why You don't change Application.Resource but Window.resource twice?

    ReplyDelete
  6. Yes, a good question. I probably forgot to mention that, but this solution is guided by the idea that you don't always have a WPF application to work on, especially when you work on a big project, started a long time ago in Windows Forms. If you don't have the resources to transfer everything to WPF but you still want to use it for some new stuff, this will be an ideal solution. Imagine, for example that you have winforms app calling wpf windows - then no matter how many times you change application.resources you will not get the desired behavior. So I like to think that this is more robust. Of course, that's my opinion.

    ReplyDelete
  7. Hi, it is a very nice solution and a good work.

    But I have a question, is it posible to change the "theme" without to loose the checkbox value.
    Even in your example if before changing the "theme" we checking the checkbox and after taht we change to a "shinyblue" o "shinyred" "themes2, the checkbox loose his value.
    I think it would be a bug in the own file's styles ("shinyblue" o "shinyred"). I mean is not a problem in your code. But does you know how to resolve these issue.

    thanks in advance

    ReplyDelete
  8. It is solved with the last actualization of the toolkit.

    ReplyDelete
  9. download link is broken
    can you send me the sample to bogdanbrizhaty@gmail.com?

    ReplyDelete