Hot-swappable Themes in Silverlight

September 14, 2009

I’ve been playing around with hot-swapping themes in Silverlight recently. This turns out to be trickier than you might imagine due to a couple of facts:

  • Silverlight doesn’t support DynamicResources
  • Silverlight doesn’t support implicit styles

And no, I don’t count the ImplicitStyleManager in the SilverlightToolkit as it’s buggy and performs like a dog. At any rate, implicit styles soon become completely unmanageable in an application of any considerable size.
The solution I came up with was to have a ThemingService that automatically applies Styles to FrameworkElements. The FrameworkElements register themselves with the ThemingService by setting an attached StyleKey dependency property. The ThemingService then maintains a WeakReference to the FrameworkElement and simply walks down the list of registered elements whenever a new theme is applied.


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Markup;
using System.Windows.Threading;
using Microsoft.Practices.Composite.Events;
using Microsoft.Practices.Composite.Logging;
using Microsoft.Practices.Composite.Presentation.Events;
using ThemingPlay.Core.Configuration;

namespace ThemingPlay.Core.Theming
{
    public class ThemingService : IThemingService
    {
        #region Private Fields
        private readonly IConfigurationService _configurationService;
        private readonly ConfigurationLoadedEvent _configurationLoadedEvent;
        private readonly ThemesLoadedEvent _themesLoadedEvent;
        private readonly ThemeChangedEvent _themeChangedEvent;
        private readonly ILoggerFacade _logger;
        private readonly Dispatcher _dispatcher;

        private readonly IList<WeakReference> _elementReferences;
        private readonly IList<Theme> _themes;
        private Theme _currentTheme;
        #endregion

        #region Ctor
        public ThemingService(IConfigurationService configurationService,
                              IEventAggregator eventAggregator, Dispatcher dispatcher, ILoggerFacade logger)
        {
            _configurationService = configurationService;
            _configurationLoadedEvent = eventAggregator.GetEvent<ConfigurationLoadedEvent>();
            _configurationLoadedEvent.Subscribe(OnConfigurationLoaded, ThreadOption.UIThread);
            _themesLoadedEvent = eventAggregator.GetEvent<ThemesLoadedEvent>();
            _themeChangedEvent = eventAggregator.GetEvent<ThemeChangedEvent>();
            _dispatcher = dispatcher;
            _logger = logger;
            
            _elementReferences = new List<WeakReference>();
            _themes = new List<Theme>();
        }
        #endregion

        #region Dependency Properties
        public static readonly DependencyProperty StyleKeyProperty = DependencyProperty.RegisterAttached(
            "StyleKey", typeof (string), typeof (ThemingService),
            new PropertyMetadata(OnStyleKeyChanged));

        public static void SetStyleKey(FrameworkElement element, string styleKey)
        {
            element.SetValue(StyleKeyProperty, styleKey);
        }

        public static string GetStyleKey(FrameworkElement element)
        {
            return element.GetValue(StyleKeyProperty) as string;
        }

        private static void OnStyleKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var styledElement = (FrameworkElement) d;
            var themingService = SingletonContainer.Instance.Resolve<IThemingService>();
            themingService.RegisterThemedElement(styledElement);
        }
        #endregion

        #region Implementation of IThemingService
        public IList<Theme> Themes
        {
            get { return _themes; }
        }

        public Theme CurrentTheme
        {
            get { return _currentTheme; }
            set
            {
                _currentTheme = value;
                SwitchToTheme(_currentTheme);
                _themeChangedEvent.Publish(_currentTheme);
            }
        }

        public void RegisterThemedElement(FrameworkElement element)
        {
            _elementReferences.Add(new WeakReference(element));
            if (_currentTheme != null && _currentTheme.ResourceDictionary != null)
            {
                ApplyStyleToElement(_currentTheme.ResourceDictionary, element);
            }
        }
        #endregion

        #region Composite Event Handlers
        public void OnConfigurationLoaded(object nothing)
        {
            LoadThemesFromConfiguration();
        }
        #endregion

        #region Private Methods
        private void LoadThemesFromConfiguration()
        {
            var themesConfig = _configurationService[THEMES_CONFIGURATION_SECTION] as ThemesConfigurationSection;
            if (themesConfig != null)
            {
                foreach (var themeConfig in themesConfig.Themes)
                {
                    var theme = CreateTheme(themeConfig);
                    _themes.Add(theme);
                }
                var defaultTheme = (from t in _themes where t.Name == themesConfig.DefaultTheme select t).SingleOrDefault();
                if (defaultTheme != null)
                {
                    this.CurrentTheme = defaultTheme;
                }
                _themesLoadedEvent.Publish(_themes);
            }
        }

        private static Theme CreateTheme(ThemeConfigurationElement themeConfig)
        {
            var theme = new Theme
                        {
                            Name = themeConfig.Name,
                            Description = themeConfig.Description,
                            DictionaryUri = new Uri(themeConfig.DictionaryUri, UriKind.Relative),
                            PackageUri = UriHelper.AbsoluteUriFromRelativePath(themeConfig.PackageUri)
                        };
            return theme;
        }

        private void LoadCurrentThemeAsync(Theme theme)
        {
            var request = WebRequest.Create(theme.PackageUri);
            request.BeginGetResponse(result => _dispatcher.BeginInvoke(()=>
               {
                   try
                   {
                       var response = request.EndGetResponse(result);
                       using (var stream = response.GetResponseStream())
                       {
                           var assemblyParts = XapHelper.GetAssemblyParts(stream);
                           if (assemblyParts.Count == 1)
                           {
                               var assemblyPart = assemblyParts[0];
                               if (XapHelper.LoadAssemblyFromStream(stream, assemblyPart) != null)
                               {
                                   var sri = Application.GetResourceStream(theme.DictionaryUri);
                                   using (var xamlStream = sri.Stream)
                                   {
                                       using (var reader = new StreamReader(xamlStream))
                                       {
                                           var xaml = reader.ReadToEnd();
                                           var resourceDictionary = XamlReader.Load(xaml) as ResourceDictionary;
                                           if (resourceDictionary != null)
                                           {
                                               theme.ResourceDictionary = resourceDictionary;
                                               ApplyTheme(theme);
                                           }
                                       }
                                   }
                               }
                           }
                       }
                   }
                   catch (Exception ex)
                   {
                       _logger.Log(ex.Message, Category.Exception, Priority.High);
                   }
               }), null);
        }

        private void SwitchToTheme(Theme theme)
        {
            if (theme.ResourceDictionary != null)
            {
                ApplyTheme(theme);
            }
            else
            {
                LoadCurrentThemeAsync(theme);
            }
        }

        private void ApplyTheme(Theme theme)
        {
            ICollection<WeakReference> deadElements = null;
            foreach (var elementReference in _elementReferences)
            {
                if (elementReference.IsAlive)
                {
                    var styledElement = elementReference.Target as FrameworkElement;
                    if (styledElement != null)
                    {
                        ApplyStyleToElement(theme.ResourceDictionary, styledElement);
                    }
                }
                else
                {
                    if (deadElements == null)
                    {
                        deadElements = new List<WeakReference>();
                    }
                    deadElements.Add(elementReference);
                }
            }
            if (deadElements != null)
            {
                foreach (var deadElement in deadElements)
                {
                    _elementReferences.Remove(deadElement);
                }
            }
        }

        private void ApplyStyleToElement(ResourceDictionary resourceDictionary, FrameworkElement element)
        {
            var styleKey = GetStyleKey(element);
            if (!string.IsNullOrEmpty(styleKey))
            {
                if (resourceDictionary.Contains(styleKey))
                {
                    var style = resourceDictionary[styleKey] as Style;
                    if (style != null)
                    {
                        try
                        {
                            element.Style = style;
                        }
                        catch (Exception ex)
                        {
                            _logger.Log(ex.Message, Category.Exception, Priority.High);
                        }
                    }
                }
            }
        }
        #endregion

        #region Constants
        private static readonly string THEMES_CONFIGURATION_SECTION = "themes";
        #endregion
    }
}

There’s loads of other code in here to do with Prism and downloading the Themes dynamically, but you get the point…

Advertisements

One Response to “Hot-swappable Themes in Silverlight”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: