// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StyleHelper.cs" company="Catel development team">
// Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
// Sets the available pixel shader modes of Catel.
// </summary>
// --------------------------------------------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Resources;
using System.Xml;
using Catel.Windows.Properties;
using log4net;
#if !SILVERLIGHT
using System.Windows.Markup;
using Ricciolo.StylesExplorer.MarkupReflection;
#endif
namespace Catel.Windows
{
#region Enums
/// <summary>
/// Sets the available pixel shader modes of Catel.
/// </summary>
public enum PixelShaderMode
{
/// <summary>
/// Disable all pixel shaders.
/// </summary>
Off,
/// <summary>
/// Automatically determine the best option.
/// </summary>
Auto,
/// <summary>
/// Use hardware for the pixel shaders.
/// </summary>
Hardware,
/// <summary>
/// Use software for the pixel shaders.
/// </summary>
Software
}
#endregion
/// <summary>
/// Helper class for WPF styles and themes.
/// </summary>
public static class StyleHelper
{
#region Constants
/// <summary>
/// Prefix of a default style key.
/// </summary>
private const string DefaultKeyPrefix = "Default";
/// <summary>
/// Postfix of a default style key.
/// </summary>
private const string DefaultKeyPostfix = "Style";
#endregion
#region Variables
/// <summary>
/// The <see cref="ILog">log</see> object.
/// </summary>
private static readonly ILog Log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#if !SILVERLIGHT
/// <summary>
/// Cached decompiled XAML resource dictionaries.
/// </summary>
private static readonly Dictionary<Uri, XmlDocument> _resourceDictionaryCache = new Dictionary<Uri, XmlDocument>();
/// <summary>
/// Cached types of <see cref="FrameworkElement"/> belonging to the string representation of the type.
/// </summary>
private static readonly Dictionary<string, Type> _styleToFrameworkElementTypeCache = new Dictionary<string, Type>();
#endif
#endregion
#region Properties
/// <summary>
/// This property allows you to disable all pixel shaders in Catel.
/// <para />
/// By default, all pixel shaders are enabled.
/// </summary>
public static PixelShaderMode PixelShaderMode = PixelShaderMode.Auto;
#endregion
#if !SILVERLIGHT
/// <summary>
/// Ensures that an application instance exists and the styles are applied to the application. This method is extremely useful
/// to apply when WPF is hosted (for example, when loaded as plugin of a non-WPF application).
/// </summary>
/// <exception cref="ArgumentNullException">when <paramref name="applicationResourceDictionary"/> is <c>null</c>.</exception>
public static void EnsureApplicationResourcesAndCreateStyleForwarders(Uri applicationResourceDictionary)
{
if (applicationResourceDictionary == null)
{
throw new ArgumentNullException("applicationResourceDictionary");
}
if (Application.Current == null)
{
try
{
new Application();
Application.Current.Resources.MergedDictionaries.Add(Application.LoadComponent(applicationResourceDictionary) as ResourceDictionary);
CreateStyleForwardersForDefaultStyles(Application.Current.Resources);
// Create an invisible dummy window to make sure that this is the main window
Window dummyMainWindow = new Window();
dummyMainWindow.Visibility = Visibility.Hidden;
}
catch (Exception ex)
{
Log.Error(ex, TraceMessages.EnsureApplicationResourcesFailed);
}
}
}
#endif
/// <summary>
/// Creates style forwarders for default styles. This means that all styles found in the theme that are
/// name like Default[CONTROLNAME]Style (i.e. "DefaultButtonStyle") will be used as default style for the
/// control.
/// This method will use the current application (<see cref="System.Windows.Application.Current"/> to retrieve
/// the resources. The forwarders will be written to the same dictionary.
/// </summary>
public static void CreateStyleForwardersForDefaultStyles()
{
CreateStyleForwardersForDefaultStyles(Application.Current.Resources);
}
/// <summary>
/// Creates style forwarders for default styles. This means that all styles found in the theme that are
/// name like Default[CONTROLNAME]Style (i.e. "DefaultButtonStyle") will be used as default style for the
/// control.
/// This method will use the passed resources, but the forwarders will be written to the same dictionary as
/// the source dictionary.
/// </summary>
/// <param name="sourceResources">Resource dictionary to read the keys from (thus that contains the default styles).</param>
public static void CreateStyleForwardersForDefaultStyles(ResourceDictionary sourceResources)
{
CreateStyleForwardersForDefaultStyles(sourceResources, sourceResources);
}
/// <summary>
/// Creates style forwarders for default styles. This means that all styles found in the theme that are
/// name like Default[CONTROLNAME]Style (i.e. "DefaultButtonStyle") will be used as default style for the
/// control.
/// <para />
/// This method will use the passed resources.
/// </summary>
/// <param name="sourceResources">Resource dictionary to read the keys from (thus that contains the default styles).</param>
/// <param name="targetResources">Resource dictionary where the forwarders will be written to.</param>
public static void CreateStyleForwardersForDefaultStyles(ResourceDictionary sourceResources, ResourceDictionary targetResources)
{
CreateStyleForwardersForDefaultStyles(sourceResources, sourceResources, targetResources, false);
}
/// <summary>
/// Creates style forwarders for default styles. This means that all styles found in the theme that are
/// name like Default[CONTROLNAME]Style (i.e. "DefaultButtonStyle") will be used as default style for the
/// control.
/// This method will use the passed resources.
/// </summary>
/// <param name="rootResourceDictionary">The root resource dictionary.</param>
/// <param name="sourceResources">Resource dictionary to read the keys from (thus that contains the default styles).</param>
/// <param name="targetResources">Resource dictionary where the forwarders will be written to.</param>
/// <param name="forceForwarders">if set to <c>true</c>, styles will not be completed but only forwarders are created.</param>
/// <exception cref="ArgumentNullException">when <paramref name="rootResourceDictionary"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="sourceResources"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="targetResources"/> is <c>null</c>.</exception>
public static void CreateStyleForwardersForDefaultStyles(ResourceDictionary rootResourceDictionary, ResourceDictionary sourceResources,
ResourceDictionary targetResources, bool forceForwarders)
{
if (rootResourceDictionary == null)
{
throw new ArgumentNullException("rootResourceDictionary");
}
if (sourceResources == null)
{
throw new ArgumentNullException("sourceResources");
}
if (targetResources == null)
{
throw new ArgumentNullException("targetResources");
}
#region If forced, use old mechanism
if (forceForwarders)
{
// Get all keys from this resource dictionary
var keys = (from key in sourceResources.Keys as ICollection<object>
where key is string &&
((string)key).StartsWith(DefaultKeyPrefix, StringComparison.InvariantCulture) &&
((string)key).EndsWith(DefaultKeyPostfix, StringComparison.InvariantCulture)
select key).ToList();
foreach (string key in keys)
{
Style style = sourceResources[key] as Style;
if (style != null)
{
Type targetType = style.TargetType;
if (targetType != null)
{
try
{
#if SILVERLIGHT
Style styleForwarder = new Style(targetType);
styleForwarder.BasedOn = style;
#else
Style styleForwarder = new Style(targetType, style);
#endif
targetResources.Add(targetType, styleForwarder);
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToCreateStyleForwarder, key);
}
}
}
}
foreach (ResourceDictionary resourceDictionary in sourceResources.MergedDictionaries)
{
CreateStyleForwardersForDefaultStyles(rootResourceDictionary, resourceDictionary, targetResources, forceForwarders);
}
return;
}
#endregion
List<Style> defaultStyles = FindDefaultStyles(sourceResources);
foreach (Style defaultStyle in defaultStyles)
{
try
{
Type targetType = defaultStyle.TargetType;
if (targetType != null)
{
ResourceDictionary resourceDictionaryDefiningStyle = FindResourceDictionaryDeclaringType(targetResources, targetType);
if (resourceDictionaryDefiningStyle != null)
{
Log.Debug(TraceMessages.CompletingStyleInfo, targetType);
resourceDictionaryDefiningStyle[targetType] = CompleteStyleWithAdditionalInfo(resourceDictionaryDefiningStyle[targetType] as Style, defaultStyle);
}
else
{
Log.Debug(TraceMessages.CannotFindStyleDefinitionCreatingForwarder, targetType);
#if SILVERLIGHT
var targetStyle = new Style(targetType);
targetStyle.BasedOn = defaultStyle;
targetResources.Add(targetType, targetStyle);
#else
targetResources.Add(targetType, new Style(targetType, defaultStyle));
#endif
}
}
}
catch (Exception)
{
Log.Warn(TraceMessages.FailedToCompleteStyle, defaultStyle);
}
}
#if !SILVERLIGHT
RecreateDefaultStylesBasedOnTheme(rootResourceDictionary, targetResources);
#endif
}
/// <summary>
/// Finds the <see cref="ResourceDictionary"/> declaring the real style for the target type.
/// </summary>
/// <param name="rootResourceDictionary">The root resource dictionary.</param>
/// <param name="targetType">Type of the target.</param>
/// <returns><see cref="ResourceDictionary"/> in which the style is defined, or <c>null</c> if not found.</returns>
/// <exception cref="ArgumentNullException">when <paramref name="rootResourceDictionary"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="targetType"/> is <c>null</c>.</exception>
private static ResourceDictionary FindResourceDictionaryDeclaringType(ResourceDictionary rootResourceDictionary, Type targetType)
{
if (rootResourceDictionary == null)
{
throw new ArgumentNullException("rootResourceDictionary");
}
if (targetType == null)
{
throw new ArgumentNullException("targetType");
}
var styleKey = (from key in rootResourceDictionary.Keys as ICollection<object>
where key == targetType
select key).FirstOrDefault();
if (styleKey != null)
{
return rootResourceDictionary;
}
foreach (ResourceDictionary mergedResourceDictionary in rootResourceDictionary.MergedDictionaries)
{
var foundResourceDictionary = FindResourceDictionaryDeclaringType(mergedResourceDictionary, targetType);
if (foundResourceDictionary != null)
{
return foundResourceDictionary;
}
}
return null;
}
/// <summary>
/// Finds all the the default styles definitions
/// </summary>
/// <param name="sourceResources">The source resources.</param>
/// <returns></returns>
private static List<Style> FindDefaultStyles(ResourceDictionary sourceResources)
{
List<Style> styles = new List<Style>();
var keys = from key in sourceResources.Keys as ICollection<object>
where key is string &&
((string)key).StartsWith(DefaultKeyPrefix, StringComparison.InvariantCulture) &&
((string)key).EndsWith(DefaultKeyPostfix, StringComparison.InvariantCulture)
select key;
foreach (string key in keys)
{
try
{
Style style = sourceResources[key] as Style;
if (style != null)
{
styles.Add(style);
}
}
catch (Exception ex)
{
Log.Warn(ex, TraceMessages.FailedToAddDefaultStyleToDefaultStylesList, key);
}
}
foreach (ResourceDictionary resourceDictionary in sourceResources.MergedDictionaries)
{
styles.AddRange(FindDefaultStyles(resourceDictionary));
}
return styles;
}
/// <summary>
/// Completes a style with additional info.
/// </summary>
/// <param name="style">The style.</param>
/// <param name="styleWithAdditionalInfo">The style with additional info.</param>
/// <returns></returns>
/// <exception cref="ArgumentNullException">when <paramref name="style"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="styleWithAdditionalInfo"/> is <c>null</c>.</exception>
private static Style CompleteStyleWithAdditionalInfo(Style style, Style styleWithAdditionalInfo)
{
if (style == null)
{
throw new ArgumentNullException("style");
}
if (styleWithAdditionalInfo == null)
{
throw new ArgumentNullException("styleWithAdditionalInfo");
}
Style newStyle = new Style(style.TargetType);
#region Copy style with additional info
foreach (Setter setter in styleWithAdditionalInfo.Setters)
{
newStyle.Setters.Add(setter);
}
#if !SILVERLIGHT
foreach (Trigger trigger in styleWithAdditionalInfo.Triggers)
{
newStyle.Triggers.Add(trigger);
}
#endif
#endregion
#region Copy original style
foreach (Setter setter in style.Setters)
{
bool exists = (from styleSetter in newStyle.Setters
where ((Setter)styleSetter).Property == setter.Property
select styleSetter).Any();
if (!exists)
{
newStyle.Setters.Add(setter);
}
}
#if !SILVERLIGHT
foreach (Trigger trigger in style.Triggers)
{
newStyle.Triggers.Add(trigger);
}
#endif
#endregion
return newStyle;
}
#if !SILVERLIGHT
/// <summary>
/// Recreates the default styles based on theme.
/// </summary>
/// <param name="rootResourceDictionary">The root resource dictionary.</param>
/// <param name="resources">The resources to fix.</param>
/// <exception cref="ArgumentNullException">when <paramref name="rootResourceDictionary"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="resources"/> is <c>null</c>.</exception>
/// <remarks>
/// This method is introduced due to the lack of the ability to use DynamicResource for the BasedOn property when
/// defining styles inside a derived theme.
/// </remarks>
private static void RecreateDefaultStylesBasedOnTheme(ResourceDictionary rootResourceDictionary, ResourceDictionary resources)
{
if (rootResourceDictionary == null)
{
throw new ArgumentNullException("rootResourceDictionary");
}
if (resources == null)
{
throw new ArgumentNullException("resources");
}
var keys = (from key in resources.Keys as ICollection<object>
where key is string &&
((string)key).StartsWith(DefaultKeyPrefix, StringComparison.InvariantCulture) &&
((string)key).EndsWith(DefaultKeyPostfix, StringComparison.InvariantCulture)
select key).ToList();
foreach (string key in keys)
{
Style style = resources[key] as Style;
if (style == null)
{
continue;
}
Type basedOnType = FindFrameworkElementStyleIsBasedOn(resources.Source, key);
if (basedOnType == null)
{
continue;
}
resources[key] = CloneStyleIfBasedOnControl(rootResourceDictionary, style, basedOnType);
}
foreach (ResourceDictionary resourceDictionary in resources.MergedDictionaries)
{
RecreateDefaultStylesBasedOnTheme(rootResourceDictionary, resourceDictionary);
}
}
#endif
/// <summary>
/// Clones a style when the style is based on a control.
/// </summary>
/// <param name="rootResourceDictionary">The root resource dictionary.</param>
/// <param name="style">The style.</param>
/// <param name="basedOnType">Type which the style is based on.</param>
/// <returns><see cref="Style"/>.</returns>
/// <exception cref="ArgumentNullException">when <paramref name="rootResourceDictionary"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="style"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="basedOnType"/> is <c>null</c>.</exception>
/// <remarks>
/// This method is introduced due to the lack of the ability to use DynamicResource for the BasedOn property when
/// defining styles inside a derived theme.
/// Should be used in combination with the <see cref="RecreateDefaultStylesBasedOnTheme"/> method.
/// </remarks>
private static Style CloneStyleIfBasedOnControl(ResourceDictionary rootResourceDictionary, Style style, Type basedOnType)
{
if (rootResourceDictionary == null)
{
throw new ArgumentNullException("rootResourceDictionary");
}
if (style == null)
{
throw new ArgumentNullException("style");
}
if (basedOnType == null)
{
throw new ArgumentNullException("basedOnType");
}
#if SILVERLIGHT
Style newStyle = new Style(style.TargetType);
newStyle.BasedOn = rootResourceDictionary[basedOnType] as Style;
#else
Style newStyle = new Style(style.TargetType, rootResourceDictionary[basedOnType] as Style);
#endif
foreach (SetterBase setter in style.Setters)
{
newStyle.Setters.Add(setter);
}
#if !SILVERLIGHT
foreach (TriggerBase trigger in style.Triggers)
{
newStyle.Triggers.Add(trigger);
}
#endif
return newStyle;
}
#if !SILVERLIGHT
/// <summary>
/// Finds the <see cref="FrameworkElement"/> a specific style is based on.
/// </summary>
/// <param name="resourceDictionaryUri">The resource dictionary URI.</param>
/// <param name="styleKey">The style key.</param>
/// <returns>
/// <see cref="Type"/> or <c>null</c> if the style is not based on a <see cref="FrameworkElement"/>.
/// </returns>
/// <exception cref="ArgumentNullException">when <paramref name="resourceDictionaryUri"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">when <paramref name="styleKey"/> is <c>null</c>.</exception>
/// <remarks>
/// This method is introduced due to the lack of the ability to use DynamicResource for the BasedOn property when
/// defining styles inside a derived theme.
/// Should be used in combination with the <see cref="RecreateDefaultStylesBasedOnTheme"/> method.
/// </remarks>
private static Type FindFrameworkElementStyleIsBasedOn(Uri resourceDictionaryUri, string styleKey)
{
if (resourceDictionaryUri == null)
{
throw new ArgumentNullException("resourceDictionaryUri");
}
if (styleKey == null)
{
throw new ArgumentNullException("styleKey");
}
if (_styleToFrameworkElementTypeCache.ContainsKey(styleKey))
{
return _styleToFrameworkElementTypeCache[styleKey];
}
try
{
XmlDocument doc;
if (_resourceDictionaryCache.ContainsKey(resourceDictionaryUri))
{
doc = _resourceDictionaryCache[resourceDictionaryUri];
}
else
{
StreamResourceInfo streamResourceInfo = Application.GetResourceStream(resourceDictionaryUri);
XmlBamlReader reader = new XmlBamlReader(streamResourceInfo.Stream);
doc = new XmlDocument();
doc.Load(reader);
_resourceDictionaryCache.Add(resourceDictionaryUri, doc);
}
#region Create xml namespace manager
// Create namespace manager (all namespaces are required)
XmlNamespaceManager xmlNamespaceManager = new XmlNamespaceManager(doc.NameTable);
foreach (XmlAttribute namespaceAttribute in doc.DocumentElement.Attributes)
{
// Clean up namespace (remove xmlns prefix)
string xmlNamespace = namespaceAttribute.Name.Replace("xmlns", "").TrimStart(new char[] { ':' });
xmlNamespaceManager.AddNamespace(xmlNamespace, namespaceAttribute.Value);
}
// Add a dummy node
xmlNamespaceManager.AddNamespace("x", "http://schemas.microsoft.com/winfx/2006/xaml");
xmlNamespaceManager.AddNamespace("ctl", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
#endregion
string xpath = string.Format("/ctl:ResourceDictionary/ctl:Style[@x:Key='{0}']/@BasedOn", styleKey);
XmlAttribute xmlAttribute = doc.SelectSingleNode(xpath, xmlNamespaceManager) as XmlAttribute;
if (xmlAttribute == null)
{
Log.Warn(TraceMessages.StyleDoesNotHaveBasedOnAttributeDefined, styleKey);
_styleToFrameworkElementTypeCache.Add(styleKey, null);
return null;
}
string basedOnValue = xmlAttribute.Value;
basedOnValue = basedOnValue.Replace("StaticResource", "");
basedOnValue = basedOnValue.Replace("x:Type", "").Trim(new[] { ' ', '{', '}' });
#region Create xml type mapper
XamlTypeMapper xamlTypeMapper = new XamlTypeMapper(new[] { "PresentationFramework" });
foreach (XmlAttribute namespaceAttribute in doc.DocumentElement.Attributes)
{
string xmlNamespace = namespaceAttribute.Name.Replace("xmlns", "").TrimStart(new char[] { ':' });
string value = namespaceAttribute.Value;
string clrNamespace = value;
string assemblyName = string.Empty;
if (clrNamespace.StartsWith("clr-namespace:"))
{
// We have a hit (formatting is normally one of the 2 below):
// * clr-namespace:[NAMESPACE]
// * clr-namespace:[NAMESPACE];assembly=[ASSEMBLY]
if (clrNamespace.Contains(";"))
{
clrNamespace = clrNamespace.Split(new char[] { ';' })[0];
}
clrNamespace = clrNamespace.Replace("clr-namespace:", "");
if (value.Contains(";"))
{
assemblyName = value.Split(new char[] { ';' })[1].Replace("assembly:", "");
}
xamlTypeMapper.AddMappingProcessingInstruction(xmlNamespace, clrNamespace, assemblyName);
}
}
#endregion
string[] splittedType = basedOnValue.Split(new[] { ':' });
string typeNamespace = (splittedType.Length == 2) ? splittedType[0] : "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
string typeName = (splittedType.Length == 2) ? splittedType[1] : splittedType[0];
Type type = xamlTypeMapper.GetType(typeNamespace, typeName);
if (type == null)
{
_styleToFrameworkElementTypeCache.Add(styleKey, null);
return null;
}
Log.Debug(TraceMessages.StyleIsBasedOnType, styleKey, type);
if ((type == typeof(FrameworkElement)) || type.IsSubclassOf(typeof(FrameworkElement)))
{
_styleToFrameworkElementTypeCache.Add(styleKey, type);
return type;
}
Log.Warn(TraceMessages.TypeIsNotAFrameworkElementType, type);
_styleToFrameworkElementTypeCache.Add(styleKey, null);
return null;
}
catch (Exception ex)
{
Log.Error(ex, TraceMessages.FailedToFindFrameworkElementWhereStyleIsBasedOn, styleKey);
return null;
}
}
#endif
}
}