Tuesday, March 30, 2010

Private Accessor T4 Generation for NUnit

As mentioned in a previous post I have been experimenting for quite some time with generating private accessors for NUnit.  If you use MsTest, then you can use the Visual Studio private accessor generator which works a treat, but not for NUnit sadly. For me this is a nice draw card for MsTest over NUnit.  The MsTest generated accessors are cleaner than my generation tool here, but hey, I don't have Microsoft's development budget :-)

There are a few hacks in it, and I might refine it over time when the needs arise. The biggest limitation is not recursively generating accessors for all private and internal classes found inside a targeted class.  In my projects I get around this easily as I am an avid user of interfaces and abstract classes.  These can be substituted in most cases using my internal type mapping dictionary. For example if you want to generate an accessor for an internal class it must have an interface to use with this accessor generator. Not as bad as it sounds if you are interfacing classes for extensive unit testing.

Download the sample project to see an example of  T4 consuming the private accessor generator. But here's the generator code for a quick glance.


namespace ReesTestToolkit
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text;

    /// <summary>
    /// A Code generator designed to be used with T4 code generation. This generator creates private accessor wrappers for a give type.
    /// </summary>
    public class PrivateAccessorGenerator
    {
        private readonly Action<string> errorWriter;
        private readonly Type targetType;
        private readonly Action<string> warningWriter;
        private readonly Action<string> writer;
        private bool isStaticClass;

        /// <summary>
        /// Initializes a new instance of the <see cref="PrivateAccessorGenerator"/> class.
        /// </summary>
        /// <param name="targetType">Type of the target to generate a private accessor wrapper for.</param>
        /// <param name="writer">The code writer.</param>
        /// <param name="errorWriter">The error writer.</param>
        /// <param name="warningWriter">The warning writer.</param>
        public PrivateAccessorGenerator(Type targetType, Action<string> writer, Action<string> errorWriter, Action<string> warningWriter)
        {
            this.errorWriter = errorWriter;
            this.warningWriter = warningWriter;
            this.writer = writer;
            this.targetType = targetType;

            MemberExcludeList = new List<string>() { "MemberwiseClone", "Finalize", "Equals", "GetHashCode", "GetType" };
            InternalTypeMapping = new Dictionary<Type, Type>();
        }

        /// <summary>
        /// Gets the internal type mapping.  This is the types to use in place of inaccessable internal types.
        /// Specify the type as the FullName of the type.
        /// If no mapping is specified here the <see cref="System.Object"/> is used instead.
        /// </summary>
        /// <value>The internal type mapping.</value>
        public IDictionary<Type, Type> InternalTypeMapping { get; private set; }

        /// <summary>
        /// Gets the member exclude list. These members will be excluded in the resulting private accessor.
        /// </summary>
        /// <value>The member exclude list.</value>
        public IList<string> MemberExcludeList { get; private set; }

        /// <summary>
        /// Gets the number of properties generated. Used for testing of the generator.
        /// </summary>
        /// <value>The property count.</value>
        public int PropertyCount { get; private set; }

        /// <summary>
        /// Gets the number of Methods generated. Used for testing of the generator.
        /// </summary>
        /// <value>The Method count.</value>
        public int MethodCount { get; private set; }

        /// <summary>
        /// Gets the number of Constants generated. Used for testing of the generator.
        /// </summary>
        /// <value>The Constants count.</value>
        public int ConstantCount { get; private set; }

        /// <summary>
        /// Gets the number of Fields generated. Used for testing of the generator.
        /// </summary>
        /// <value>The Fields count.</value>
        public int FieldCount { get; private set; }

        /// <summary>
        /// Gets the constructor count.
        /// </summary>
        /// <value>The constructor count.</value>
        public int ConstructorCount { get; private set; }

        /// <summary>
        /// Generates this instance.
        /// </summary>
        public void Generate()
        {
            try
            {
                this.isStaticClass = this.targetType.GetConstructors().Length == 0 && this.targetType.IsAbstract && this.targetType.IsSealed;

                this.Preconditions();

                this.WriteHeader();

                this.writer("        // Fields...");
                this.WriteStaticFields();

                if (!this.isStaticClass)
                {
                    this.WriteFields();
                }

                if (!this.isStaticClass)
                {
                    this.writer("        // Constructors...");
                    this.WriteConstructors();
                }

                this.writer("        // Properties...");
                this.WriteProperties(true);

                if (!this.isStaticClass)
                {
                    this.WriteProperties(false);
                }

                this.writer("        // Methods...");
                this.WriteMethods(true);
                if (!this.isStaticClass)
                {
                    this.WriteMethods(false);
                }
            } catch (Exception ex)
            {
                this.errorWriter(ex.ToString());
            } finally
            {
                this.WriteFooter();
            }
        }

        /// <summary>
        /// Corrects the generic format. This assumes the correct type has already been selected using <see cref="PublicizableType"/>
        /// </summary>
        /// <param name="fullTypeName">Full name of the type.</param>
        /// <param name="genericArgs">The generic args.</param>
        /// <returns>A properly formatted generic type ie: IList[string] instead of IList`1</returns>
        private static string CorrectGenericFormat(string fullTypeName, IEnumerable<Type> genericArgs)
        {
            if (!genericArgs.Any())
            {
                return fullTypeName;
            }

            string typeName = fullTypeName.Substring(0, fullTypeName.IndexOf("`")) + "<";
            bool first = true;
            foreach (var genericArgType in genericArgs)
            {
                if (!first)
                {
                    typeName += ", ";
                }

                if (genericArgType.IsGenericType)
                {
                    typeName += CorrectGenericFormat(genericArgType.FullName, genericArgType.GetGenericArguments());
                } else
                {
                    typeName += genericArgType.Name;
                }

                first = false;
            }

            typeName.Remove(typeName.Length - 2, 2);
            return typeName + ">";
        }

        private static void WriteOneProperty(Action<string> codeWriter, string type, string property, bool staticOnly, bool canWrite, bool canRead)
        {
            if (!canRead && !canWrite)
            {
                return;
            }

            string staticString = staticOnly ? "static " : string.Empty;
            string target = staticOnly ? "null" : "this.target";

            codeWriter(
                string.Format(
                    @"
        public {0}{1} {2} {{",
                    staticString,
                    type,
                    property));

            if (canRead)
            {
                codeWriter(
                    string.Format(
                        @"
            get {{
                try {{
                    return ({0})TargetType.GetProperty(""{1}"", BindingFlags.NonPublic | {3} | BindingFlags.Public).GetValue({2}, new object[] {{ }});
                }} catch (NullReferenceException ex) {{
                    throw new Exception(""The private accessor may be out of date, try regenerating its code. An Object Reference Not Set was thrown."", ex);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}
",
                        type,
                        property,
                        target,
                        staticOnly ? "BindingFlags.Static" : "BindingFlags.Instance"));
            }

            if (canWrite)
            {
                codeWriter(
                    string.Format(
                        @"
            set {{
                try {{
                    TargetType.GetProperty(""{0}"", BindingFlags.NonPublic | {2} | BindingFlags.Public).SetValue({1}, value, new object[] {{ }});
                }} catch (NullReferenceException ex) {{
                    throw new Exception(""The private accessor may be out of date, try regenerating its code. An Object Reference Not Set was thrown."", ex);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}",
                        property,
                        target,
                        staticOnly ? "BindingFlags.Static" : "BindingFlags.Instance"));
            }

            codeWriter("        }");
        }

        private static void WriteOneMethod(
            Action<string> codeWriter,
            string returnType,
            string methodName,
            string methodGeneric,
            string[] argTypes,
            string[] argNames,
            bool isFunction,
            bool isStatic,
            string genericWhereSpec)
        {
            var arguments = new StringBuilder();
            var argumentNamesBuilder = new StringBuilder();
            for (int index = 0; index < argTypes.Length; index++)
            {
                if (index > 0)
                {
                    arguments.Append(", ");
                    argumentNamesBuilder.Append(", ");
                }

                arguments.AppendFormat("{0} {1}", argTypes[index], argNames[index]);
                argumentNamesBuilder.Append(argNames[index]);
            }

            string argumentNames = ", new object[] { }";
            if (argumentNamesBuilder.Length > 0)
            {
                argumentNames = ", new object[] {" + argumentNamesBuilder.ToString() + "}";
            }

            string methodOverload = string.Empty;
            if (methodName == "MemberwiseClone")
            {
                methodOverload = "new ";
            }

            string genericTypeParamName = methodGeneric.Replace("<", string.Empty).Replace(">", string.Empty);
            if (isFunction && returnType == null)
            {
                returnType = genericTypeParamName;
            }

            codeWriter(
                string.Format(
@"
        public {0}{1}{2} {3}{4}({5}) {6}{{",
                isStatic ? "static " : string.Empty,
                methodOverload,
                returnType ?? genericTypeParamName,
                methodName,
                methodGeneric,
                arguments,
                genericWhereSpec));
            codeWriter(
                string.Format(
@"            try {{
                {0} TargetType.GetMethod(""{1}"", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public).Invoke({2}{3});
            }} catch (NullReferenceException ex) {{
                throw new Exception(""The private accessor may be out of date, try regenerating its code. An Object Reference Not Set was thrown."", ex);
            }} catch (TargetInvocationException ex) {{
                throw ex.InnerException ?? ex;
            }}",
                    isFunction ? "return (" + returnType + ")" : string.Empty,
                    methodName,
                    isStatic ? "null" : "this.target",
                    argumentNames));
            codeWriter("        }");
        }

        private string GetFieldTypeName(FieldInfo field)
        {
            if (!field.FieldType.IsGenericType)
            {
                return PublicizableType(field.FieldType);
            }

            return new GenericTypeStringMaker(PublicizableTypeAsType(field.FieldType)).Digest();
        }

        private string PublicizableType(Type type)
        {
            if (type == null)
            {
                errorWriter("Null type passed to PublicizableType.");
                throw new ArgumentNullException("type", "Null Type passed to type");
            }

            return PublicizableTypeAsType(type).FullName;
        }

        private Type PublicizableTypeAsType(Type type)
        {
            if (type == null)
            {
                errorWriter("Null type passed to PublicizableType.");
                throw new ArgumentNullException("type", "Null Type passed to type");
            }

            if (InternalTypeMapping.ContainsKey(type))
            {
                return InternalTypeMapping[type];
            }

            if (type.IsPublic)
            {
                return type;
            }

            this.warningWriter(String.Format("The internal type {0} has been found and is not mapped, System.Object will be used.", type.Name));

            return typeof(object);
        }

        private void Preconditions()
        {
            if (this.errorWriter == null)
            {
                throw new InvalidOperationException("errorWriter is null");
            }

            if (this.targetType == null)
            {
                this.errorWriter("targetType cannot be null");
            }

            if (this.writer == null)
            {
                this.errorWriter("code writer delegate cannot be null");
            }

            if (!this.targetType.IsPublic && !InternalTypeMapping.ContainsKey(this.targetType))
            {
                errorWriter(
                    string.Format(
                        "Targetted type {0} is internal but no mapping has been supplied to use in place of the internal type.", this.targetType.Name));
            }

            foreach (var pair in InternalTypeMapping.ToArray())
            {
                if (pair.Key.IsPublic)
                {
                    warningWriter(
                        string.Format(
                            "Internal Type mapping warning: Given type {0} is public and usable as is, it does not need to be mapped to another type.",
                            pair.Key.Name));
                    InternalTypeMapping.Remove(pair);
                }

                if (!pair.Value.IsAssignableFrom(pair.Key))
                {
                    errorWriter(string.Format("Internal Type mapping error: Internal type {0} is not assignable into mapped type {1}", pair.Key.Name, pair.Value.Name));
                }
            }
        }

        private void WriteStaticFields()
        {
            foreach (var field in this.targetType.GetFields(BindingFlags.Static | BindingFlags.NonPublic)
                .Where(field => !field.Name.EndsWith("BackingField") && !field.Name.Contains("CS$")))
            {
                if (field.IsLiteral)
                {
                    ConstantCount++;
                } else
                {
                    FieldCount++;
                }

                var returnTypeType = PublicizableTypeAsType(field.FieldType);
                string returnType = returnTypeType.IsGenericType
                                        ? CorrectGenericFormat(returnTypeType.FullName, returnTypeType.GetGenericArguments())
                                        : returnTypeType.FullName;

                this.writer(
                    string.Format(
                        @"
        public static {0} {1} {{
            get {{
                try {{
                    return ({0})TargetType.GetField(""{1}"", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}",
                        returnType,
                        field.Name));
                if (!field.IsLiteral)
                {
                    this.writer(
                        string.Format(
                            @"
            set {{
                try {{
                    TargetType.GetField(""{0}"", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, value);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}",
                            field.Name));
                }

                this.writer(@"
        }");
            }
        }

        private void WriteConstructors()
        {
            this.writer(
                string.Format(
                    @"
        public {0}_Accessor({1} target) {{
            TargetType = target.GetType();
            this.target = target;
        }}",
                    this.targetType.Name,
                    PublicizableType(this.targetType)));

            foreach (var ctor in this.targetType.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public))
            {
            ConstructorCount++;
                if (ctor.GetParameters().Count() == 0)
                {
                    this.writer(
                        string.Format(
                            @"
        public {0}_Accessor() {{
            this.target = TargetType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, new Type[] {{ }}, null).Invoke(new object[] {{ }});
        }}",
           this.targetType.Name));
                } else
                {
                    var typedArgs = new StringBuilder();
                    var typesOnly = new StringBuilder();
                    var namesOnly = new StringBuilder();
                    int count = 0;
                    foreach (var param in ctor.GetParameters())
                    {
                        if (count > 0)
                        {
                            typedArgs.Append(", ");
                            typesOnly.Append(", ");
                            namesOnly.Append(", ");
                        }

                        typedArgs.Append(new GenericTypeStringMaker(PublicizableTypeAsType(param.ParameterType)).Digest());
                        typedArgs.Append(" ");
                        typedArgs.Append(param.Name);

                        typesOnly.Append("typeof(");
                        typesOnly.Append(new GenericTypeStringMaker(PublicizableTypeAsType(param.ParameterType)).Digest());
                        typesOnly.Append(")");

                        namesOnly.Append(param.Name);
                        count++;
                    }

                    this.writer(
                        string.Format(
                            @"
        public {1}_Accessor({0}) {{
            this.target = TargetType.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public, null, new[] {{ {2} }}, null).Invoke(new object[] {{ {3} }});
        }}",
                            typedArgs,
                            this.targetType.Name,
                            typesOnly,
                            namesOnly));
                }
            }
        }

        private void WriteMethods(bool staticOnly)
        {
            BindingFlags filter = staticOnly ? BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public : BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public;

            var methods = new List<MethodCache>();
            foreach (var method in this.targetType.GetMethods(filter)
                .Where(m => !m.Name.StartsWith("get_")
                    && !m.Name.StartsWith("<")
                    && !m.Name.StartsWith("set_")
                    && !m.Name.Contains("ctor")
                    && !m.Name.StartsWith("add_")
                    && !m.Name.StartsWith("remove_")
                    && !MemberExcludeList.Contains(m.Name)))
            {
                Type returnType = PublicizableTypeAsType(method.ReturnType);
                string methodReturnType;
                if (method.ReturnType.IsGenericType)
                {
                    methodReturnType = new GenericTypeStringMaker(returnType).Digest();
                } else if (returnType.FullName == "System.Void")
                {
                    methodReturnType = "void";
                } else
                {
                    methodReturnType = returnType.FullName;
                }

                string methodGeneric = string.Empty;
                string genericWhereSpec = string.Empty;
                if (method.IsGenericMethod)
                {
                    methodGeneric = "<" + String.Join(",", method.GetGenericArguments().Select(g => g.Name).ToArray()) + ">";
                    genericWhereSpec = String.Join(
                        " ",
                        method.GetGenericArguments().Select(g => string.Format("where {0} : {1} ", g.Name, g.BaseType.Name)).ToArray());

                    genericWhereSpec = genericWhereSpec.Replace("Object", "new()");
                }

                var methodParameters = method.GetParameters()
                    .Select(p => CorrectGenericFormat(PublicizableType(p.ParameterType), p.ParameterType.GetGenericArguments()))
                    .ToArray();

                var cachedMethods = new MethodCache(method.Name, method);
                Action<string> dummyWriter = cachedMethods.Add;
                WriteOneMethod(
                    dummyWriter,
                    methodReturnType,
                    method.Name,
                    methodGeneric,
                    methodParameters,
                    method.GetParameters().Select(p => p.Name).ToArray(),
                    methodReturnType != "void",
                    staticOnly,
                    genericWhereSpec);
                cachedMethods.Close();
                methods.Add(cachedMethods);
            }

            PostProcessMethods(methods);
        }

        private void PostProcessMethods(IEnumerable<MethodCache> methods)
        {
            var duplicates =
                methods.Where(method => methods.Count(m => m.MethodName == method.MethodName) > 1)
                    .OrderBy(method => method.MethodName);
            var others = methods.Where(method => methods.Count(m => m.MethodName == method.MethodName) == 1).ToList();

            // Select a duplicate method that is specifically declared in the target type, ie not a base class.
            others.AddRange(from duplicate in duplicates.Select(m => m.MethodName).Distinct()
                            select duplicates.Count(m => m.MethodMetadata.DeclaringType == this.targetType) into refinementCheck
                            where refinementCheck == 1
                            select duplicates.Single(m => m.MethodMetadata.DeclaringType == this.targetType));

            // Update the duplicate list
            duplicates = methods.Where(m => others.Count(m2 => m2.MethodName == m.MethodName) == 0).OrderBy(m => m);
            duplicates.ToList().ForEach(m => this.warningWriter(string.Format("Duplicate method found '{0}' - unable to choose a duplicate to include in Accessor", m.MethodName)));

            // Output the code
            others.ForEach(m => this.writer(m.Code));

            this.MethodCount += others.Count();
        }

        private void PostProcessProperties(IEnumerable<PropertyCache> properties)
        {
            var duplicates =
                properties.Where(method => properties.Count(m => m.MethodName == method.MethodName) > 1)
                    .OrderBy(method => method.MethodName);
            var others = properties.Where(method => properties.Count(m => m.MethodName == method.MethodName) == 1).ToList();

            // Select a duplicate method that is specifically declared in the target type, ie not a base class.
            others.AddRange(from duplicate in duplicates.Select(m => m.MethodName).Distinct()
                            select duplicates.Count(m => m.PropertyMetadata.DeclaringType == this.targetType) into refinementCheck
                            where refinementCheck == 1
                            select duplicates.Single(m => m.PropertyMetadata.DeclaringType == this.targetType));

            // Update the duplicate list
            duplicates = properties.Where(m => others.Count(m2 => m2.MethodName == m.MethodName) == 0).OrderBy(m => m);
            duplicates.ToList().ForEach(m => this.warningWriter(string.Format("Duplicate method found '{0}' - unable to choose a duplicate to include in Accessor", m.MethodName)));

            // Output the code
            others.ForEach(m => this.writer(m.Code));

            this.PropertyCount += others.Count();
        }

        private void WriteProperties(bool staticOnly)
        {
            var properties = new List<PropertyCache>();
            BindingFlags filter = BindingFlags.NonPublic | BindingFlags.Public;
            if (staticOnly)
            {
                filter |= BindingFlags.Static;
            } else
            {
                filter |= BindingFlags.Instance;

                // Write AccessingThisTarget property
                this.writer(string.Format(@"        public {0} AccessingThisTarget {{ get {{ return ({0})this.target; }} }}", PublicizableType(this.targetType)));
            }

            foreach (var property in this.targetType.GetProperties(filter)
                .Where(p => !p.Name.Contains("CachedAnonymous") && !MemberExcludeList.Contains(p.Name)))
            {
                string propertyReturnType;
                Type returnType = PublicizableTypeAsType(property.PropertyType);
                if (property.PropertyType.IsGenericType)
                {
                    propertyReturnType = new GenericTypeStringMaker(returnType).Digest();
                } else
                {
                    propertyReturnType = returnType.FullName;
                }

                var cacheProperty = new PropertyCache(property.Name, property);
                WriteOneProperty(cacheProperty.Add, propertyReturnType, property.Name, staticOnly, property.CanWrite, property.CanRead);
                cacheProperty.Close();
                properties.Add(cacheProperty);
            }

            PostProcessProperties(properties);
        }

        private void WriteFields()
        {
            foreach (var property in this.targetType.GetFields(BindingFlags.Instance | BindingFlags.NonPublic)
                .Where(field => !field.Name.EndsWith("BackingField") && !field.Name.Contains("CachedAnonymous")))
            {
                string typeName = GetFieldTypeName(property);
                FieldCount++;
                this.WriteOneField(typeName, property.Name);
            }
        }

        private void WriteOneField(string type, string property)
        {
            this.writer(
                string.Format(
@"
        public {0} {1} {{
            get {{
                try {{
                    return ({0})TargetType.GetField(""{1}"", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(this.target);
                }} catch (NullReferenceException ex) {{
                    throw new Exception(""The private accessor may be out of date, try regenerating its code. An Object Reference Not Set was thrown."", ex);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}

            set {{
                try {{
                    TargetType.GetField(""{1}"", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(this.target, value);
                }} catch (NullReferenceException ex) {{
                    throw new Exception(""The private accessor may be out of date, try regenerating its code. An Object Reference Not Set was thrown."", ex);
                }} catch (TargetInvocationException ex) {{
                    throw ex.InnerException ?? ex;
                }}
            }}
        }}",
                type,
                property));
        }

        private void WriteHeader()
        {
            this.writer("#pragma warning disable 465");

            if (this.isStaticClass)
            {
                this.writer(
                    string.Format(
@"
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [GeneratedCode(""ReesTestToolKit.PrivateAccessorGenerater"", ""1"")]
    public static class {0}_Accessor {{
        public static Type TargetType;",
                        this.targetType.Name));
            } else
            {
                this.writer(
                    string.Format(
@"
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [GeneratedCode(""ReesTestToolKit.PrivateAccessorGenerater"", ""1"")]
    public class {0}_Accessor {{
        private readonly object target;
        public static Type TargetType;
",
                          this.targetType.Name));
            }

            this.writer(
                string.Format(
                    @"        static {0}_Accessor() {{
            TargetType = Type.GetType(""{1}"");
        }}
",
                    this.targetType.Name,
                    this.targetType.AssemblyQualifiedName));
        }

        private void WriteFooter()
        {
            this.writer("    } // End Class " + this.targetType.Name);
            this.writer("// ============================================================================================================");
            this.writer("#pragma warning restore 465");
        }
    }
}

WPF Localization

Firstly, let me address a misnomer. A lot of people call the process of adding other languages, internationalisation.  This is inaccurate, firstly you are adding more than languages, your adding culture sensitive spelling for example. The process is called Localisation, as you're localising an application to a specific culture, not necessarily another language.

My key goals of internationalisation are:

  1. Replacement of strings served by the application code onto the UI.
  2. Correct formatting of numbers and dates.
  3. Best practices for UI layout to allow dynamic resizing (text could grow or shrink depending on spelling and language).
  4. Replacement of hard coded strings used in UI code (labels, titles etc).
  5. Possibly full replacement of a UI form if significantly layout changes need to be made to accommodate culture.

This is a good overview of available options and approaches: 

Setup
I have found making en-US the default culture to be the easiest approach.  Only the concrete Application executable is localised (in my opinion anyway), I also believe that exceptions are not translated and are logged only and not presented to the user in their raw form.
Set the default culture at the top of the csproj file as follows:

Set the default culture in AssemblyInfo.cs

   31 [assemblyNeutralResourcesLanguage("en-US"UltimateResourceFallbackLocation.Satellite)]

And finally for testing its best to be specific about the culture you want to use when running on a developer machine. For production remove code lines 9 - 14.  This code needs to be run as soon after the application starts as possible. Preferably in App.Xaml.cs constructor or in the void Main startup.

    9             var culture = new CultureInfo("en-US");
   10             // var culture = new CultureInfo("en-NZ");
   11 
   12             Thread.CurrentThread.CurrentUICulture = culture;
   13             Thread.CurrentThread.CurrentCulture = culture;
   14 
   15             // Ensure the current culture passed into bindings is the OS culture.
   16             // By default, WPF uses en-US as the culture, regardless of the system settings.
   17             FrameworkElement.LanguageProperty.OverrideMetadata(
   18                 typeof(FrameworkElement),
   19                 newFrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)));

Using the Vs Command-Line prompt run the following commands:
msbuild /t:updateuid ApplicationProject.csproj

This will generate x:uid attributes in all xaml files automatically.
Check the process was successful by running (or check to see if any new elements have been added without x:uid's)
msbuild /t:checkuid ApplicationProject.csproj

Note: x:uid is a completely different attribute to x:Name. They are used for different purposes. They are not used in code.

Build the application at this point to generate the default culture satellite assembly.  A folder called en-US should appear under bin containing a *.Resources.dll.


1) Replacement Strings Served by the Application Code.
Use a standard Resource String approach for this (.resx file). Best practice is to place them all in the same folder called Resources (not under Properties).  Name them as per diagram.  Also String.resx and Strings.en-US.resx are exactly the same (this probably isn't necessary, but found it to be clear and could allow changing the default culture easily).

Build the application again and a new en-NZ folder should appear under bin containing a new *.Resources.dll.
At this point the satellite assemblies only contain string resources from the resx files.


2) Correct Format of Dates and Numbers
This is simply a best practice step.  All formatting should be done using standard techniques. For example:

dateTime.ToString("d") for short date using current regional format.
dateTime.ToString("D") for long date format.
dateTime.ToString("f") for short date/time pattern.
dateTime.ToString("F") for long date/time patten.
See msdn for more information.

Don't do this:
dateTime.ToString("dd-MM-yy")


3) Xaml Layout Best Practice
Again this is simply a best practice step. Any element that contains text must be capable of growing and shrinking to accommodate more or less text after translation. Try to avoid fixed sizing and positioning.  This is exactly the same as developing in HTML to allow different screen sizes.  Do not put language resources in static images.  It is ok however to embed hard coded strings, field labels and titles into xaml as these will be translated using step 4.  Try to avoid excessive complexity by overusing resource (resx) files.  This can be avoided by hard coding text into the xaml files where it makes sense to do so (ie it doesnt change unless culture changes).  


4) Replacement of Hard Coded Strings in Xaml
Download and compile the LocBaml tool from Microsoft.
(I have a downloaded version of this tool and I have added extra runtime feedback text, see me for more info).

The LocBaml.exe tool must be located in the bin folder of the application with all the other dependencies of the application.  
Run the tool from the bin folder of the application with the following parameters to extract the xaml string resources for localisation:
LOCBAML /parse en-US\ApplicationProject.Resources.dll

This points the tool at the existing en-US resource dll and extracts the xaml strings into an external file.  This is so the file can be translated and then used to create a new resource dll 

This will create a new CSV file called: ApplicationProject.Resources.csv.
I prefer to rename this file to the intended culture and relocate it to the main project folder.  In this example I would simply name this file en-NZ.csv.


This will be a persistent file and should be version controlled.  It may need to be regenerated and merged with the new one in the future if xaml layout changes.

Edit this file with Excel (or similar) and filter the third column to hide anything classified as a "None". This leaves text contain that needs to be considered for translation.
Column headings are as follows:
Resource File, Resource Key, Localisation Category, Readable, Modifiable, Value.

Build the new satellite assembly as follows from the bin folder of the application (remember the csv file is in the main project folder):
LOCBAML /generate en-US\ApplicationProject.Resources.dll /trans:..\..\en-NZ.csv /cul:en-NZ /out:en-NZ

This will create the new dll containing the translated xaml into the satellite assembly in the en-NZ folder. There is a problem however.  It overwrites the dll currently in that folder meaning all the Resource strings (resx) are now not in the satellite assembly.
This is solved by using a post build task to run a batch file to create the translated xaml resources and link it with the resx resources.

Here's the batch file called CombineResxBaml.cmd
@ECHO OFF
REM                                                        %1            %2    %3              %4            %5
REM USAGE Post Build Task: $(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)
REM %1 Example C:\Development\ApplicationName\
REM %2 Example en-NZ
REM %3 Example x86
REM %4 Example ApplicationName
REM %5 Example Debug

REM en-US must be the default language in your csproj.

cls
IF %3 == x86 CALL "c:\Program Files\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat"
IF %3 == x64 CALL "c:\Program Files\Microsoft Visual Studio 10.0\VC\bin\vcvars64.bat"
cd %1

resgen .\Resources\Strings.%2.resx
copy .\Resources\Strings.%2.resources bin\%5\%2
cd .\bin\%5

IF NOT EXIST "%1\bin\%5\%2" MD "%1\bin\%5\%2"

locbaml /generate ..\..\obj\%3\%5\%4.g.en-US.resources /tran:..\..\%2.csv /cul:%2 /out:.\%2
ECHO Baml Generation Complete.
ping 127.0.0.1 -n 3 -w 1000 > nul

cd %2
del %4.Resources.Strings.%2.resources
ren Strings.%2.resources %4.Resources.Strings.%2.resources

del %4.Resources.dll
al /template:"..\%4.exe" /embed:%4.g.%2.resources /embed:%4.Resources.Strings.%2.resources /culture:%2 /out:%4.resources.dll

cd ..\..\..

And the post build task looks like this to create a en-NZ satellite:
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)

If more than one alternate culture is targeted then add more post build tasks:
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) en-NZ $(PlatformName) $(TargetName) $(ConfigurationName)
$(TargetDir)CombineResxBaml.cmd $(ProjectDir) fr-FR $(PlatformName) $(TargetName) $(ConfigurationName)

5) Full Replacement of Xaml Where Necessary
To do...


Maintenance
During the application life cycle is going to evolve.  When it does the culture.csv files will need to be regenerated.  I would suggest a merge based approach to preserve existing translations into newly generated csv files.  It is also advisable to try to reuse x:uid in xaml development to avoid having to merge the culture.csv files altogether.


All in all I think this is a highly flexible localisation approach and certainly better than previous attempts in winforms and asp.net.

Thursday, March 25, 2010

Unit Testing with Private Accessors Part 2

In the previous post, I demo'ed my static helper class method of accessing private members.  In this post I wanted to explore the possibility of creating a T4 code generator that given a type creates a strongly typed wrapper that tests can access simply and with very readable test code.
Lets start at the end and work back to the beginning. Here's the use case...

Here's a sample of how my static helper class would look in a use case:


   24         [Test]
   25         public void TestUsage() {
   26             var target = new PrivateAccessorGeneratorTestClass();
   27 
   28             PrivateAccessor.SetProperty(target"privateInt"1);
   29             PrivateAccessor.SetProperty(target"PrivateObject"new List<string>(new[] { "Ben","Rees" }));
   30             PrivateAccessor.SetProperty(target"privateString""something");
   31             PrivateAccessor.InvokeMethod(target"VoidMethod");
   32 
   33             var methodResult = PrivateAccessor.InvokeMethod<int>(target"IntMethod"newobject[] { 1"2" });
   34             var privateIntResult = (int)PrivateAccessor.GetField(target"privateInt");
   35             var privateObjectResult = (IList<string>)PrivateAccessor.GetProperty(target,"PrivateObject");
   36             var privateStringResult = (string)PrivateAccessor.GetProperty(target,"privateString");
   37 
   38             Assert.AreEqual(1privateIntResult);
   39             Assert.AreEqual(1methodResult);
   40             Assert.AreEqual(2privateObjectResult.Count);
   41             Assert.AreEqual("something"privateStringResult);
   42         }


And here's how the new use case would look using the generated strongly typed wrapper...

    8         [Test]
    9         public void TestUsage() {
   10             var target = new PrivateAccessorGeneratorTestClass();
   11             var accessor = new PrivateAccessorGeneratorTestClass_Accessor(target);
   12 
   13             accessor.privateInt = 1;
   14             accessor.PrivateObject = new List<string>(new[] { "Ben""Rees" });
   15             accessor.privateString = "something";
   16             accessor.VoidMethod();
   17             var result = accessor.IntMethod(1"2");
   18 
   19             Assert.AreEqual(1accessor.privateInt);
   20             Assert.AreEqual(1result);
   21             Assert.AreEqual(2accessor.PrivateObject.Count);
   22             Assert.AreEqual("something"accessor.privateString);
   23         }
The target is the class with non-public members to which we need to access and the accessor is the strongly typed generated wrapper class that allows easy access and takes care of the finding and invoking the members using reflection .

Here is the target class (for clarity):

   26     public class PrivateAccessorGeneratorTestClass {
   27         public PrivateAccessorGeneratorTestClass(string data) {
   28             // omitted for clarity - not important
   29         }
   30 
   31         internal int privateInt { getprivate set; }
   32 
   33         internal List<string> PrivateObject { getprivate set; }
   34 
   35         private string privateString { getset; }
   36 
   37         private void VoidMethod() {
   38             // omitted for clarity - not important
   39         }
   40 
   41         protected int IntMethod(int istring s) {
   42             // omitted for clarity - not important
   43         }
   44     }

The generator produces the PrivateAccessorGeneratorTestClass_Accessor class based on the type it is give, which in this example case is the PrivateAccessorGeneratorTestClass.
I won't go into detail on how the generator works just now, suffice to say it just enumerators through all non-public members and outputs a wrapping method to call the method on the target type using reflection. 

This all looks pretty good from a use case point of view.  However there are a number of down sides:

Pros / Cons:
+ Nice readably use case syntax.
+ Easy to use in testing.
- Requires maintenance of the Code Generator TT file. A list of types must be given to the generator to produce an accessor for each type required in testing.
- The code generator doesn't automatically generate during compile (could be automated with a batch file). [Edit 26-March] Fixed see below.
- Once the code generator has run, it file-locks the assemblies it references, which quite often stops the build process because it needs to rewrite the DLL file.
- Simply renaming or removing a member's name, will still successfully compile, because the code generator must be run manually. [Edit 26-March] fixed see below.

There are some ways around these downsides.  One idea I will pursue is to make a copy of the solution DLL's and the TT generator file can reference those. This copy an take place pre-build, and so could the execution of the code generator.  This might circumvent the locking issue.  (Although it could just defer it to the second build). Then renaming a member name would cause a compile error on the second compile after the change.

More research I think.

[Edit 26-March-2010]
I have explored automating template transform on building the solution (in Vs not CI).  I wondered if this might circumvent the problem with file locking references to class libraries.  This is how I did it:

  1. Add a batch file to your solution with the following text:
    "%CommonProgramFiles%\microsoft shared\TextTemplating\10.0\texttransform.exe" -out "%1.cs" -P "%ProgramFiles%\Reference Assemblies\Microsoft\Framework\v3.5" -P %2 "%1.tt"

    (Without the carriage returns)
  2. Add a Pre-Build task to the project which contains the TT file:
    $(SolutionDir)RunTemplate.bat $(ProjectDir)AccessorGeneratorTestClass.g $(OutDir)

    Replace "AccessGeneratorTest.g" with the name of your template to transform.  Note that the .TT extension is missing.  My actual file is named AccessGeneratorTest.g.tt in this example, and in the above commandline I have dropped the .TT.
  3. Build the solution, this will transform the template first.

However it doesn't prevent the T4 engine from file locking the referenced Dll's.  This really limits the ease of use of these templates.

I still need to explore copying the DLL's on a Pre-Build task. Although the chances for that to work are slim.

[Edit 1-April-2010]
A colleague of mine (thanks Simon) found an excellent toolkit for T4 called funnily enough T4 Toolkit.  Its written by Oleg Sych a very respectable developer and authority on all things T4.

Inside this toolkit there is a alternative preprocessor to reference an assembly and it copies the assembly instead of referencing it in place. And this gets around the reference locking issue.  Wow, thanks Oleg.  To use Oleg's toolkit you MUST install it with his MSI you cannot simply reference the dll.

So now the Pros vs Cons of using a generated accessor the Pros far out way the Cons.

Here's a use case template of the T4 using Oleg's preprocessor.

// <autogenerated/>
// Last generated <#= DateTime.Now #>
<#@ template language="C#" hostspecific="true"#>

<#@ assembly name="System" #>

<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\FrameworkWpf.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\FrameworkTestToolkit.dll" #>
<#@ VolatileAssembly processor="T4Toolbox.VolatileAssemblyProcessor" name="bin\debug\WpfAppTemplate.exe" #>

<#@ output extension=".cs" #>

<#@ import namespace="System" #>
<#@ import namespace="FrameworkTestToolkit" #>

namespace WpfAppTemplateTest {
using System;
using System.Reflection;
<# 
    // Add new types into the below array:
    Type[] types = new Type[] { 
typeof(FrameworkWpf.SafeEvent),
typeof(FrameworkWpf.Mvvm.ControllerBase),
typeof(FrameworkTestToolkit.PrivateAccessorGeneratorTestClass),
typeof(WpfAppTemplate.PostController),
typeof(WpfAppTemplate.ShellController),
};


// Do not modify this code
foreach (Type type in types) {
PrivateAccessorGenerator builder = new PrivateAccessorGenerator(type, WriteLine, Error, Warning);
builder.Generate();
}
#>
}

Happy generations.
[Edit] See this post for more information: http://blog.rees.biz/2010/03/private-accessor-t4-generation-for.html