Click here to Skip to main content
13,256,260 members (47,140 online)
Click here to Skip to main content

Stats

29.9K views
537 downloads
11 bookmarked
Posted 11 Jan 2011

Catel - Part 4 of n: Unit testing with Catel

, 28 Jan 2011
This article explains how to write unit tests for MVVM using Catel.
src
Catel.Articles.04 - Unit testing
Models
Properties
Resources
Images
add.png
delete.png
edit.png
group.png
UI
Data
Converters
ViewModels
Windows
Catel.Articles.04 - Unit testing.Test
Models
Properties
UI
ViewModels
Catel.Articles.Base
Data
Attributes
Properties
Resources
Images
CatenaLogic.png
Preview.png
Run.png
ShowCode.png
UI
Controls
Helpers
ViewModels
Windows
Catel.Core
Attributes
ClassDiagrams
DataObjectBase.cd
SavableDataObjectBase.cd
Collections
Helpers
ComponentModel
Data
Attributes
Exceptions
Interfaces
Diagnostics
Extensions
Helpers
Exceptions
Helpers
IO
Exceptions
IoC
LLBLGen
Log4net
Appender
Extensions
Helpers
MVVM
Commands
Interfaces
Exceptions
Services
EventArgs
Exceptions
Interfaces
ViewModels
Attributes
Interfaces
Properties
Reflection
Exceptions
Extensions
Helpers
Runtime
Serialization
Attributes
Helpers
Security
Cryptography
Helpers
Catel.Examples.Models
Properties
Catel.Examples.PersonApplication
Properties
Resources
Images
add.png
delete.png
edit.png
group.png
UI
Data
Converters
ViewModels
Windows
Catel.Examples.Silverlight
Properties
Resources
Images
add.png
delete.png
edit.png
group.png
UI
Data
Converters
Pages
ViewModels
Windows
Catel.Examples.Silverlight.Web
Catel.Examples.Silverlight.Web.csproj.user
ClientBin
Properties
Catel.FxCop
Catel.Silverlight
Diagnostics
log4net
Core
MVVM
Commands
Services
ViewModels
Properties
Catel.Core
Catel.Windows
Reflection
Themes
Generic
Assets
Old
Windows
Controls
Data
Converters
Helpers
Helpers
Catel.Silverlight.Test
Properties
Catel.Silverlight.Test.Web
Catel.Silverlight.Test.Web.csproj.user
ClientBin
Properties
Catel.snk
Catel.Templates.WpfApplication
Properties
UI
Controls
ViewModels
Windows
Catel.Templates.WpfItemTemplates
Properties
UI
Controls
ViewModels
Windows
Catel.Test
Collections
Convert
Data
Helpers
IO
MVVM
UI
ViewModels
Properties
Reflection
Runtime
Serialization
Security
Cryptography
Test References
Catel.Windows.accessor
Windows
Data
Converters
Catel.vsmdi
Catel.Windows
ClassDiagrams
ViewModelBase.cd
Collections
Extensions
Helpers
MVVM
Commands
Services
Test
UI
ViewModels
Properties
Resources
Images
Add.png
ClearOutput.png
Edit.png
Error.png
Loading.gif
Preview.png
Remove.png
Save.png
TipOfTheDay.png
Warning.png
Themes
Aero
ExpressionDark
Assets
Generic
Assets
Controls
Jetpack
Assets
background.png
Old
SunnyOrange
Assets
Windows
Controls
Extensions
LinkLabel
StackGrid
Data
Converters
Helpers
Documents
Extensions
Extensions
Helpers
Input
Markup
Media
Effects
EmptyEffect
EmptyEffect.fx
EmptyEffect.ps
EmptyEffect.fx
GrayscaleEffect
GrayscaleEffect.fx
GrayscaleEffect.ps
Extensions
Imaging
Extensions
Windows
DataWindow
TipOfTheDay
Local.testsettings
Settings.StyleCop
TraceAndTestImpact.testsettings
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="RedirectDeserializationBinder.cs" company="Catel development team">
//   Copyright (c) 2008 - 2011 Catel development team. All rights reserved.
// </copyright>
// <summary>
//   <see cref="SerializationBinder" /> class that supports backwards compatible serialization.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using Catel.Properties;
using Catel.Reflection;
using log4net;

namespace Catel.Runtime.Serialization
{
    /// <summary>
    /// <see cref="SerializationBinder"/> class that supports backwards compatible serialization.
    /// </summary>
    internal sealed class RedirectDeserializationBinder : SerializationBinder
    {
        #region Variables
        /// <summary>
        /// The <see cref="ILog">log</see> object.
        /// </summary>
        private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

        /// <summary>
        /// A dictionary of all <see cref="RedirectTypeAttribute"/> found.
        /// </summary>
        private readonly Dictionary<string, RedirectTypeAttribute> _redirectAttributes = new Dictionary<string, RedirectTypeAttribute>();

        /// <summary>
        /// A list of microsoft public key tokens.
        /// </summary>
        private static List<string> _microsoftPublicKeyTokens;
        #endregion

        #region Constructor & destructor
        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <remarks>
        /// This constructor searches for attributes of the current application domain.
        /// </remarks>
        public RedirectDeserializationBinder()
            : this(AppDomain.CurrentDomain) { }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="appDomain"><see cref="AppDomain"/> to search in.</param>
        /// <remarks>
        /// This constructor searches for attributes in a specific application domain.
        /// </remarks>
        public RedirectDeserializationBinder(AppDomain appDomain)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

            List<Assembly> assemblies = new List<Assembly>();

            foreach (Assembly assembly in AssemblyHelper.GetLoadedAssemblies(appDomain))
            {
                // Skip dynamic assemblies
                if (string.Compare(assembly.FullName, "Anonymously Hosted DynamicMethods Assembly, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null") != 0)
                {
                    // Skip Microsoft assemblies
                    string company = assembly.Company();
                    if (!company.Contains("Microsoft"))
                    {
                        assemblies.Add(assembly);
                    }
                }
            }

            Initialize(assemblies);
            
            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// This method automatically loads all the assemblies into the current AppDomain and then uses the
        /// <see cref="RedirectDeserializationBinder(IEnumerable{Assembly})"/> constructor.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        /// <remarks>
        /// This constructor searches for attributes in specific assemblies.
        /// <para />
        /// No exceptions are caught during the loading of the assemblies. The caller is responsible for handling
        /// the exceptions correctly.
        /// </remarks>
        public RedirectDeserializationBinder(IEnumerable<string> assemblies)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

            List<Assembly> loadedAssemblies = new List<Assembly>();
            foreach (string assembly in assemblies)
            {
                loadedAssemblies.Add(Assembly.Load(assembly));
            }

            Initialize(loadedAssemblies);

            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RedirectDeserializationBinder"/> class.
        /// <para />
        /// Creates a custom binder that redirects all the types to new types if required. All properties
        /// decorated with the <see cref="RedirectTypeAttribute"/> will be redirected.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        /// <remarks>
        /// This constructor searches for attributes in specific assemblies.
        /// </remarks>
        public RedirectDeserializationBinder(IEnumerable<Assembly> assemblies)
        {
            Log.Debug(TraceMessages.CreatingRedirectDeserializationBindinger);

			Initialize(assemblies);

            Log.Debug(TraceMessages.CreatedRedirectDeserializationBindinger);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets the Microsoft public key tokens.
        /// </summary>
        /// <value>The Microsoft public key tokens.</value>
        private static IEnumerable<string> MicrosoftPublicKeyTokens
        {
            get
            {
                if (_microsoftPublicKeyTokens == null)
                {
                    _microsoftPublicKeyTokens = new List<string>();
                    _microsoftPublicKeyTokens.Add("b77a5c561934e089");
                    _microsoftPublicKeyTokens.Add("b03f5f7f11d50a3a");
                    _microsoftPublicKeyTokens.Add("31bf3856ad364e35");
                }

                return _microsoftPublicKeyTokens;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
        /// assemblies passed to this method.
        /// </summary>
        /// <param name="assemblies">Array of assembly locations that should be searched.</param>
        private void Initialize(IEnumerable<Assembly> assemblies)
        {
            Log.Debug(TraceMessages.InitializingRedirectDeserializationBinder);

            Type attributeType = typeof(RedirectTypeAttribute);

            foreach (Assembly assembly in assemblies)
            {
                try
                {
                    foreach (Type type in assembly.GetTypes())
                    {
                        InitializeAttributes(type, (RedirectTypeAttribute[])type.GetCustomAttributes(attributeType, true));

                        foreach (MemberInfo member in type.GetMembers())
                        {
                            InitializeAttributes(member, (RedirectTypeAttribute[])member.GetCustomAttributes(attributeType, true));
                        }
                    }
                }
                catch (Exception ex)
                {
                    Log.Warn(ex, TraceMessages.FailedToGetTypesOfAssembly, assembly.FullName);
                }
            }

            Log.Debug(TraceMessages.InitializedRedirectDeserializationBinder);
        }

        /// <summary>
        /// Initializes the binder by searching for all <see cref="RedirectTypeAttribute"/> in the
        /// attributes passed to this method.
        /// </summary>
        /// <param name="decoratedObject">object that was decorated with the attribute.</param>
        /// <param name="attributes">Array of attributes to search for.</param>
        private void InitializeAttributes(object decoratedObject, RedirectTypeAttribute[] attributes)
        {
            if (decoratedObject == null)
            {
                return;
            }

            if (attributes.Length == 0)
            {
            	return;
            }

            Type type = null;

            if (decoratedObject is Type)
            {
                type = decoratedObject as Type;
            }

            foreach (RedirectTypeAttribute attribute in attributes)
            {
                if (type != null)
                {
                    string typeName = TypeHelper.FormatType(type.Assembly.FullName, type.FullName);
                    typeName = ConvertTypeToVersionIndependentType(typeName);
                    string finalTypeName, finalAssemblyName;
                    SplitType(typeName, out finalAssemblyName, out finalTypeName);

                    attribute.NewTypeName = finalTypeName;
                    attribute.NewAssemblyName = finalAssemblyName;
                }

                if (_redirectAttributes.ContainsKey(attribute.OriginalType))
                {
                    Log.Warn(TraceMessages.RedirectAlreadyAdded, attribute.OriginalType, _redirectAttributes[attribute.OriginalType].TypeToLoad, attribute.TypeToLoad);
                }
                else
                {
                    Log.Debug(TraceMessages.AddingRedirectFromTypeToType, attribute.OriginalTypeName, attribute.NewTypeName);

                    _redirectAttributes.Add(attribute.OriginalType, attribute);
                }
            }
        }

        /// <summary>
        /// Binds an assembly and typename to a specific type.
        /// </summary>
        /// <param name="assemblyName">Original assembly name.</param>
        /// <param name="typeName">Original type name.</param>
        /// <returns><see cref="Type"/> that the serialization should actually use.</returns>
        public override Type BindToType(string assemblyName, string typeName)
        {
            string currentType = TypeHelper.FormatType(assemblyName, typeName);
            string currentTypeVersionIndependent = ConvertTypeToVersionIndependentType(currentType);
            string newType = ConvertTypeToNewType(currentTypeVersionIndependent);

            Type typeToDeserialize = LoadType(newType) ?? (LoadType(currentTypeVersionIndependent) ?? LoadType(currentType));

            if (typeToDeserialize == null)
            {
                Log.Error(Resources.CouldNotLoadNewType, currentType, newType);
            }

            return typeToDeserialize;
        }

        /// <summary>
        /// Tries to load a type on a safe way.
        /// </summary>
        /// <param name="type">The type to load.</param>
        /// <returns>The type or <c>null</c> if this method fails.</returns>
        /// <remarks>
        /// In case this method fails to load the type, a warning will be traced with additional information.
        /// </remarks>
        private static Type LoadType(string type)
        {
            Type loadedType = null;

            try
            {
                loadedType = Type.GetType(type);
            }
            catch (Exception ex)
            {
                Log.Warn(ex, TraceMessages.FailedToLoadType, type);
            }

            return loadedType;
        }

        /// <summary>
        /// Splits the type into a type name and assembly name.
        /// </summary>
        /// <param name="type">Type to split.</param>
        /// <param name="assemblyName">Assemby name.</param>
        /// <param name="typeName">Type name.</param>
        private static void SplitType(string type, out string assemblyName, out string typeName)
        {
            int splitterPos = type.IndexOf(", ");

            typeName = (splitterPos != -1) ? type.Substring(0, splitterPos).Trim() : type;
            assemblyName = (splitterPos != -1) ? type.Substring(splitterPos + 1).Trim() : type;
        }

        /// <summary>
        /// Converts a string representation of a type to a version independent type by removing the assembly version information.
        /// </summary>
        /// <param name="type">Type to convert.</param>
        /// <returns>String representing the type without version information.</returns>
        private static string ConvertTypeToVersionIndependentType(string type)
        {
            const string InnerTypesEnd = ",";

            string newType = type;
            string[] innerTypes = GetInnerTypes(newType);

            if (innerTypes.Length > 0)
            {
                // Remove inner types
                newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
                for (int i = 0; i < innerTypes.Length; i++)
                {
                    innerTypes[i] = ConvertTypeToVersionIndependentType(innerTypes[i]);
                }
            }

            string typeName, assemblyName;
            SplitType(newType, out assemblyName, out typeName);

            bool isMicrosoftAssembly = false;
            foreach (string t in MicrosoftPublicKeyTokens)
            {
                if (assemblyName.Contains(t))
                {
                    isMicrosoftAssembly = true;
                    break;
                }
            }

            // Remove version info from assembly (if not signed by Microsoft)
            if (!isMicrosoftAssembly)
            {
                int splitterPos = assemblyName.IndexOf(", ");
                if (splitterPos != -1)
                {
                	assemblyName = assemblyName.Substring(0, splitterPos);
                }
            }

            newType = TypeHelper.FormatType(assemblyName, typeName);

            if (innerTypes.Length > 0)
            {
                int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
                if (innerTypesIndex >= 0)
                {
                    newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
                }
            }

            return newType;
        }

        /// <summary>
        /// Converts a string representation of a type to a redirected type.
        /// </summary>
        /// <param name="type">Type to convert.</param>
        /// <returns>String representing the type that represents the redirected type.</returns>
        private string ConvertTypeToNewType(string type)
        {
            const string InnerTypesEnd = ",";

            string newType = type;
            string[] innerTypes = GetInnerTypes(newType);

            if (innerTypes.Length > 0)
            {
                newType = newType.Replace(string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)), string.Empty);
                for (int i = 0; i < innerTypes.Length; i++)
                {
                    innerTypes[i] = ConvertTypeToNewType(innerTypes[i]);
                }
            }

            if (_redirectAttributes.ContainsKey(newType))
            {
                newType = _redirectAttributes[newType].TypeToLoad;
            }

            if (innerTypes.Length > 0)
            {
                int innerTypesIndex = newType.IndexOf(InnerTypesEnd);
                if (innerTypesIndex >= 0)
                {
                    newType = newType.Insert(innerTypesIndex, string.Format(CultureInfo.InvariantCulture, "[{0}]", TypeHelper.FormatInnerTypes(innerTypes)));
                }
            }

            return newType;
        }

        /// <summary>
        /// Returns the inner type of a type, for example, a generic array type.
        /// </summary>
        /// <param name="type">Full type which might contain an inner type.</param>
        /// <returns>Array of inner types.</returns>
        private static string[] GetInnerTypes(string type)
        {
            const char InnerTypeCountStart = '`';
            char[] InnerTypeCountEnd = new[] { '[', '+' };
            const char InternalTypeStart = '+';
            const char InternalTypeEnd = '[';
            const string AllTypesStart = "[[";
            const char SingleTypeStart = '[';
            const char SingleTypeEnd = ']';

            List<string> innerTypes = new List<string>();

            try
            {
                int countIndex = type.IndexOf(InnerTypeCountStart);
                if (countIndex == -1)
                {
                	return innerTypes.ToArray();
                }

                // This is a generic, but does the type definition also contain the inner types?
                if (!type.Contains(AllTypesStart))
                {
                	return innerTypes.ToArray();
                }

                // Get the number of inner types
                int innerTypeCountEnd = -1;
                foreach (char t in InnerTypeCountEnd)
                {
                    int index = type.IndexOf(t);
                    if ((index != -1) && ((innerTypeCountEnd == -1) || (index < innerTypeCountEnd)))
                    {
                        // This value is more likely to be the one
                        innerTypeCountEnd = index;
                    }
                }

                int innerTypeCount = int.Parse(type.Substring(countIndex + 1, innerTypeCountEnd - countIndex - 1));

                // Remove all info until the first inner type
                if (!type.Contains(InternalTypeStart.ToString()))
                {
                    // Just remove the info
                    type = type.Substring(innerTypeCountEnd + 1);
                }
                else
                {
                    // Remove the index, but not the numbers
                    int internalTypeEnd = type.IndexOf(InternalTypeEnd);
                    type = type.Substring(internalTypeEnd + 1);
                }

                // Get all the inner types
                for (int i = 0; i < innerTypeCount; i++)
                {
                    // Get the start & end of this inner type
                    int innerTypeStart = type.IndexOf(SingleTypeStart);
                    int innerTypeEnd = innerTypeStart + 1;
                    int openings = 1;

                    // Loop until we find the end
                    while (openings > 0)
                    {
                        if (type[innerTypeEnd] == SingleTypeStart)
                        {
                            openings++;
                        }
                        else if (type[innerTypeEnd] == SingleTypeEnd)
                        {
                            openings--;
                        }

                        // Increase current pos if we still have openings left
                        if (openings > 0)
                        {
                        	innerTypeEnd++;
                        }
                    }

                    innerTypes.Add(type.Substring(innerTypeStart + 1, innerTypeEnd - innerTypeStart - 1));
                    type = type.Substring(innerTypeEnd + 1);
                }
            }
            catch (Exception ex)
            {
                Log.Warn(ex, TraceMessages.FailedToRetrieveInnerTypes);
            }

            return innerTypes.ToArray();
        }
        #endregion
    }
}

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)

Share

About the Author

Geert van Horrik
Software Developer CatenaLogic
Netherlands Netherlands
I am Geert van Horrik, and I have studied Computer Science in the Netherlands.

I love to write software using .NET (especially the combination of WPF and C#). I am also the lead developer of Catel, an open-source application development framework for WPF, Silverlight, WP7 and WinRT with the focus on MVVM.

I have my own company since January 1st 2007, called CatenaLogic. This company develops commercial and non-commercial software.

To download (or buy) applications I have written, visit my website: http://www.catenalogic.com

You may also be interested in...

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 28 Jan 2011
Article Copyright 2011 by Geert van Horrik
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid