Click here to Skip to main content
15,885,990 members
Articles / Desktop Programming / WPF

Catel - Part 4 of n: Unit testing with Catel

Rate me:
Please Sign up or sign in to vote.
4.55/5 (10 votes)
28 Jan 2011CPOL11 min read 48.9K   572   11  
This article explains how to write unit tests for MVVM using Catel.
// --------------------------------------------------------------------------------------------------------------------
// <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
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Netherlands Netherlands
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions