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…