1.01.2010

String templates revisited

Sometimes I encounter a task - create some little template, that user can easily configure. Yeah, there are many powerful template engines such as
  1. NVelocity(Caslte project)
  2. Brail(Caslte project)
  3. NHaml
  4. Spark
  5. StringTemplate .NET
  6. etc...
But, what if you don't want reference additional assemblies, and you need only simple basic functionality? For this time I saw 2 solutions:
  1. Use string format placeholders {0}, {1}, etc - it is good solution if you have only 2-3 placeholders, do not need descriptiveness of them, and (may be) need to use specific formatters such as {0:d}, {0:X} etc:
    string result = string.Format("{0} + {1} = {2}", 4, 5, 6);
    
  2. Use replacement, handwritten placeholders: #ID#, #NAME#, etc - they are more descriptive, but without formatters:
    string result = "#Arg1# + #Arg2# = #Res#"
                    .Replace("#Arg1#", 4)
                    .Replace("#Arg2#", 5)
                    .Replace("#Res#", 9);
    
But what if we will use templates similar to ASP.NET Ajax 4.0 - {Id}, {UserName}? They are more descriptive, reflects names of properties of the some object. Using reflection we can easily use names of the properties as placeholders. I wrote extension methods for StringBuilder and String for this purpose. Here is one for a StringBuilder:

public static StringBuilder AppendFormatEx(this StringBuilder self, string format, object args, MemberKind memberKind)
{
    MemberInfo[] members;

    switch (memberKind)
    {
        case MemberKind.Property:
            members = args.GetType().GetProperties();
            break;
        case MemberKind.Field:
            members = args.GetType().GetFields();
            break;
        default:
            throw new ArgumentOutOfRangeException("memberKind");
    }

    foreach (MemberInfo member in members)
    {
        string placeholder = string.Format("{{{0}}}", member.Name);

        object value;
        if (member is FieldInfo)
            value = (member as FieldInfo).GetValue(args);
        else if (member is PropertyInfo)
            value = (member as PropertyInfo).GetValue(args, null);
        else
            throw new InvalidOperationException("Only fields and properties are supported. But you use " + member.GetType().Name);

        format = format.Replace(placeholder, Convert.ToString(value));
    }

    self.Append(format);

    return self;
}

It is a simplified for readability main method that contains all the functionality. First of all, we take an array of public members from args, kind of members (property or field) you can specify by argument memberKind. Then for each extracted member we create name of the placeholder, get the value of the member and replace placeholder by value. Thats all.. as simple as possible.

An overloaded version of this method working only with public properties:
public static StringBuilder AppendFormatEx(this StringBuilder self, string format, object args)
{
    return AppendFormatEx(self, format, args, MemberKind.Property);
}

For String I have an overloaded extension method too:
public static string Format(this string format, object args, MemberKind memberKind)
{
    StringBuilder builder = new StringBuilder();
    builder.AppendFormatEx(format, args, memberKind);
    return builder.ToString();
}
It is uses a StringBuilder's method to format a string. An instance of the string represents a format. And overloaded version working only with public properties:
public static string Format(this string format, object args)
{
    return Format(format, args, MemberKind.Property);
}
Now I show you simple test console application, that gives you an idea how this works in code:
using System;
using System.Text;

namespace HennadiyKurabko.StringFormattersEx
{
    class Program
    {
        static void Main(string[] args)
        {
            // Arrange
            string format = "{Name}:\t\t{Age} year(s)\t\t{Login}\r\n";

            object[] users = new object[] {
                new { Name = "Jane Siemens", Age = 25, Login = "janes" },
                new { Name = "Tom Clancy", Age = 25, Login = "tomc" },
                new { Name = "Sofi Payne", Age = 25, Login = "sofip" }
            };

            // Act [String]
            Console.WriteLine("String extensions test");
            Console.WriteLine();

            foreach (var user in users)
            {
                Console.Write(format.Format(user));
            }

            Console.WriteLine("-----------------------------");

            // Act [StringBuilder]
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.AppendLine("StringBuilder extensions test");
            stringBuilder.AppendLine();

            foreach (var user in users)
            {
                stringBuilder.AppendFormatEx(format, user);
            }

            stringBuilder.AppendLine("-----------------------------");

            Console.WriteLine(stringBuilder.ToString());
        }
    }
}

You see, that format string contains placeholders with property names: {Name}, {Age}, {Login}. It is far more descriptive as {0}, {1}. But in that case you cannot use specific formatters. So, it is your choice what of the methods to use. Hope this helps you.

An example for this article you can download here: StringFormattersEx.zip

Good luck and happy coding!


Shout it

kick it on DotNetKicks.com