Monday, 30 September 2013

What I Learnt About .Net Custom Formatters

I recently needed to format a DateTime object to a custom format, this post is a dump of what I learnt. It shows the steps necessary to implement a formatter that returns the name of the date's weekday. (This wasn't the custom format I needed to implement, it's just an example).

There are 2 components to formatting, a FormatProvider and a CustomFormatter - the FormatProvider's task is to serve up an instance of a CustomFormatter.

First, the FormatProvider:
    public class WeekdayFormatProvider : IFormatProvider
    {
        public object GetFormat(Type formatType)
        {
            throw new NotImplementedException();
        }
    }
Next, the CustomFormatter:
    public class WeekdayFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            throw new NotImplementedException();
        }
    }
IFormatProvider.GetFormat() is called from within the string.Format method. The formatType argument is the type of CustomFormatter required; the convention is, if you can provide a formatter of that type, then return one, otherwise return null.
    public class WeekdayFormatProvider : IFormatProvider
    {
        public object GetFormat(Type formatType)
        {
            if (formatType == typeof(ICustomFormatter))
            {
                return new WeekdayFormatter();
            }
            return null;
        }
    }
The type of formatter required will change depending on the method used to invoke the FormatProvider. For example, the DateTime.ToString(IFormatProvider) method will request a formatter of type DateTimeFormatInfo. As this class is sealed, it's not possible to derive from it. So in order to use a custom formatter with a DateTime, you need to use a string.Format(IFormatProvider...) overload.

ICustomFormatter.Format() is also called from the string.Format method. It should return the formatted string for the required object (passed to the method via the arg parameter). For this example it just needs to calculate the day name and reverse it.
    public class WeekdayFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            var dayName = ((DateTime)arg).DayOfWeek.ToString();
            return new string(dayName.Reverse().ToArray());
        }
    }
The format parameter is used to pass the formatString component of a format item.
So, for the usage
    string.Format(new WeekdayFormatProvider(), "{0:R}", DateTime.MinValue);
the arguments would be
    format : "R"
    arg : [DateTime.MinValue]
    formatProvider : WeekdayFormatProvider
The following shows how this format string could be used. The formatter is going to inspect the value passed; if it's "R" then reverse the weekday, otherwise return the weekday in the more usual format.
    public class WeekdayFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            var dayName = ((DateTime)arg).DayOfWeek.ToString();
            if (format == "R")
            {
                return new string(dayName.Reverse().ToArray());
            }
            return dayName;
        }
    }
Another convention, in the Format() method, is to handle arguments that are not specifically of the type expected. The pattern is shown in this, the final version of the formatter:
    public class WeekdayFormatter : ICustomFormatter
    {
        public string Format(string format, object arg, IFormatProvider formatProvider)
        {
            try
            {
                var dayName = ((DateTime)arg).DayOfWeek.ToString();
                if (format == "R")
                {
                    return new string(dayName.Reverse().ToArray());
                }
                return dayName;
            }
            catch (InvalidCastException)
            {
                if (arg is IFormattable)
                {
                    return ((IFormattable)arg).ToString(format, CultureInfo.CurrentCulture);
                }
                else if (arg != null)
                {
                    return arg.ToString();
                }
                else
                {
                    return String.Empty;
                }
            }
        }
    }
Putting it all together, this usage:
        static void Main(string[] args)
        {
            var formatProvider = new WeekdayFormatProvider();
            
            var formattedMinDate = string.Format(formatProvider, "{0}", DateTime.MinValue);
            Console.WriteLine(formattedMinDate);
            
            var formattedMinDateReversed = string.Format(formatProvider, "{0:R}", DateTime.MinValue);
            Console.WriteLine(formattedMinDateReversed);

            var formattedInteger = string.Format(formatProvider, "{0:R} {1}", DateTime.MinValue, int.MaxValue);
            Console.WriteLine(formattedInteger);
            
            Console.ReadLine();
        }
gives this output:
Monday
yadnoM
yadnoM 2147483647
The final code used is available on GitHub at https://github.com/orangutanboy/CustomFormatter/

No comments:

Post a Comment