Showing posts with label Skins. Show all posts
Showing posts with label Skins. Show all posts

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.

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