Creating Desktop Skins in WPF for PRISM Desktop

Based on Creating a Skinned User Interface in WPF (excellent blog on the topic!), adapted for PRISM and dependency injection.

Creating SKINS in WPF Desktop for PRISM is relatively easy.   The first thing we want to do is create our styles.  Note in my WPF Project (Demo.WPF second arrow) that I use an Assets folder - this is to be consistent with the .NET RIA Services auto generated styles (top arrow). 

My Styles.xaml file will contain styles common to all skins - in this case I have a StylesDefault (Blue) and StylesGreen (Green).

With the Styles (skins) out of the way we can start wiring up PRISM.   We do this by Loading both of the Stylesxxxxx.xaml files and registering them in our Container - the type will be ResourceDictionary with the names set to "Blue" and Default".  The code for the Windows.cs (Shell) follows:

   22 /// <summary>
   23 /// Interaction logic for Window1.xaml
   24 /// </summary>
   25 public partial class Window1 : Window
   26 {
   27     /// <summary>
   28     /// Load resources delegate
   29     /// </summary>
   30     public delegate void LoadResourcesDelegate();
   31 
   32     [Dependency]
   33     public IUnityContainer Container { get; set; }
   34 
   35     [Dependency]
   36     public ILoggerFacade Logger { get; set; }
   37 
   38     public Window1()
   39     {
   40         InitializeComponent();
   41 
   42         this.Loaded += new RoutedEventHandler(Window1_Loaded);
   43     }
   44 
   45     void Window1_Loaded(object sender, RoutedEventArgs e)
   46     {
   47         // Load asynchronously
   48         Dispatcher.BeginInvoke(new LoadResourcesDelegate(LoadResources));
   49     }
   50 
   51     private void LoadResources()
   52     {
   53         // Load secondary styles when the XAML Build Action
   54         // is set to "Content"
   55         Container.RegisterInstance<ResourceDictionary>("Blue",
   56             (ResourceDictionary)Application.LoadComponent(
   57             new Uri("Assets/StylesGreen.xaml", UriKind.Relative)));
   58 
   59         // Load default styles (so we can restore).  The XAML
   60         // Build Action is "Page" - required because the App.XAML
   61         // shows this as the MergeDictionary
   62         Container.RegisterInstance<ResourceDictionary>("Default", 
   63             (ResourceDictionary) Application.LoadComponent(
   64             new Uri("Assets/StylesDefault.xaml", UriKind.Relative)));
   65     }
   66 }

Note how we are loading the skins on their own thread (line 48).  We don't have an immediate need for the skins since our App.xaml code is loading the StylesDefault.xaml resources for it's own use (line 8 below); we don't have to affect performance by loading skins on the UI thread.

    1 <Application x:Class="Demo.WPF.App"
    2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    4 
    5     <Application.Resources>
    6         <ResourceDictionary>
    7             <ResourceDictionary.MergedDictionaries>
    8                 <ResourceDictionary Source="Assets/StylesDefault.xaml"/>
    9             </ResourceDictionary.MergedDictionaries>
   10         </ResourceDictionary>
   11     </Application.Resources>
   12 
   13 </Application>

At this point we have successfully loaded our skins into our Unity Container and are ready to wire-up a view to utilize them.

We'll now go to our Modules folder and select the ModuleClient.WPF (WPF for Desktop) and wire-up our MenuRegion's view (MainMenu) to process the available skins.  Note below we create a context menu with two menu options "Blue" (default) and "Green".

Our code behind will handle the ContextMenu_Click by using the EventAggregator to raise a MenuItem click event (ClickEventArgs handles MenuItem, Button and CheckBox clicks).   We're done on the UI side - now we'll turn our attention to the Presenter which will handle the click event.

The PrismContrib.xxxx assemblies abstract much of the framework from us (KISS).   Within these libraries resides the PresenterBase<T> which wires everything up on lines 27-32.   Once wired-up we have virtual methods that we can override to simplify our lives, e.g., below we override the OnButtonClickEventHandler to handle our MenuItem click.

The magic happens on lines 45 and 50 as applicable.  They utilize a Extensions class which extends the IUnityContainer interface.  Note how we can use the same code (line 29 below) for both Silverlight and WPF Desktop by using the "using" statements on lines 5 and 7 (below).

Line 26 will resolve our ResourceDictionary by the name effectively giving us a reference to the applicable skin dictionary.

On line 29 we get a reference to the current resource and clear it on line 33.   All that remains is to add our updated MergedDictionary and walla!  We have successfully applied a new skin.

    1 using System.Windows;
    2 using Microsoft.Practices.Unity;
    3 
    4 #if SILVERLIGHT
    5 using wpfCollection = System.Windows.PresentationFrameworkCollection<System.Windows.ResourceDictionary>;
    6 #else
    7 using wpfCollection = System.Collections.ObjectModel.Collection<System.Windows.ResourceDictionary>;
    8 #endif
    9 
   10 
   11 using System.Collections.ObjectModel;namespace Infrastructure.Resource
   12 {
   13     /// <summary>
   14     /// Resource Utility
   15     /// </summary>
   16     public static class ResourceUtil
   17     {
   18         /// <summary>
   19         /// Sets the skin.
   20         /// </summary>
   21         /// <param name="rd">The rd.</param>
   22         /// <param name="skinName">Name of the skin.</param>
   23         public static void SetSkin(this IUnityContainer container, string skinName)
   24         {
   25 #if !WinForm
   26             ResourceDictionary skin =
   27                 container.Resolve<ResourceDictionary>(skinName);
   28 
   29             wpfCollection mergedDicts =
   30                 Application.Current.Resources.MergedDictionaries;
   31 
   32             if (mergedDicts.Count > 0)
   33                 mergedDicts.Clear();
   34 
   35             // Apply the selected skin so that all elements in the
   36             // application will honor the new look and feel.
   37             mergedDicts.Add(skin);
   38 #endif
   39 
   40         }
   41     }
   42 }

Right clicking on the Menu bar (top sky-blue region) reveals our context menu. 


Tags: , ,
Categories: WPF


Actions: E-mail | Permalink |  Grammar/Typo/Better way? Please let me know