IFormatProvider, IFormattable, and ICustomFormatter – demystified!

This is a heavy school week, so I should not have dug into this tonight – but this was too interesting to put down! Ok, so you know how when you use String.Format, certain types allow special formatting? How is that done? Well – it’s a long story that involves a few pieces. Let me lay out a scenario…

Imagine you have a customer account number that is normally in the form of “HFD-12345678-11”, we’ll call this the “long format”. In many parts of your system, you just need the middle 8 numbers. So you call “12345678” the “short format”, informally.

If I store this account number in it’s own data structure, called CustomerAccount, I would like to be able to specify that format in the .ToString(), or if I use a String.Format(). For example, I would like to do this:

CustomerAccount customerAccount = new CustomerAccount();

customerAccount.AccountNumber = "HFD-01992039-13";

 

Debug.WriteLine(

    String.Format("Long format:  {0:LF}", customerAccount)); // I want: HFD-12345678-11

 

Debug.WriteLine(

    String.Format("Short format: {0:SF}", customerAccount)); // I want: 12345678

 

Debug.WriteLine("ToString() – Defaults to LF:  " +

    customerAccount.ToString()); // I want default to be: HFD-12345678-11

           

Debug.WriteLine("ToString("LF") – LongFormat:  " +

    customerAccount.ToString("LF")); // I want: HFD-12345678-11

           

Debug.WriteLine("ToString("SF") – ShortFormat: " +

    customerAccount.ToString("SF")); // I want: 12345678

Right now, this does work – I get something like this in my output window:

Long format:  HFD-01992039-13
Short format: 01992039
ToString() – Defaults to LF:  HFD-01992039-13
ToString("LF") – LongFormat:  HFD-01992039-13
ToString("SF") – ShortFormat: 01992039

First, I created a class called CustomerAccount which has a public property called AccountNumber, which is a string. Next, I have that class implement IFormatProvider and IFormattable. That would look something like this:

public class CustomerAccount : IFormatProvider, IFormattable

{

    public string AccountNumber { get; set; }

 

 

    /// <summary>

    /// Returns the string representation of this object, specifying the format

    /// and format provider.

    /// </summary>

    /// <param name="format">Format of the account number: LF=Long format; SF=Short format.</param>

    /// <param name="formatProvider">The format provider to use, if different than that the default.</param>

    public string ToString(string format, IFormatProvider formatProvider)

    {

        return customerAccountFormatter.Format(format, AccountNumber, this);

    }

 

    /// <summary>

    /// Gets the default ICustomerFormatter for this object.

    /// </summary>

    public object GetFormat(Type formatType)

    {

        if (formatType == typeof(ICustomFormatter))

            return customerAccountFormatter;

        else

            return null;

    } ICustomFormatter customerAccountFormatter = new CustomerAccountFormatter();

}

IFormattable includes the ToString() overload. Whenever you use String.Format(), it calls this overload automatically (if the type implements IFormattable) – otherwise, it seems to just call the regular .ToString() on the object.

Put another way, when you do String.Format(“{0:LF}”, myCustomerAccount) – the String.Format() passes this ToString overload the format and the format provider, if specified.

Now, with the code above, I suspect I’m not implementing that correctly. I would assume that you could see if formatProvider is null. If it is, we use our default formatProvider (the current instance), else we find the formatter in the provider that was passed in. I couldn’t get that to work because I wasn’t sure what to pass for Type to the GetFormat()… ANYhow, the code above works for this sample, although there may be a better way to implement it.

IFormatProvider includes the GetFormat() method, which returns the formatter we want to use.

Next, we have our custom formatter called CustomerAccountFormatter, which implements ICustomFormatter.

ICustomFormatter defines a Format() method that takes in the format that was passed, the actual object to format, and the format provider. It’s in this method where you do your logic for how you’d want to handle the formatting tokens of “LF” and “SF”, for example. Also note there is another method in there called HandleOtherFormats() which takes care of other formats (like DateTime, etc). When a developer uses the String.Format(IFormatProvider, string, params object[] args) overload, String.Format() will send ALL formatting to you to process. So, you need to go look up the format provider for each, if you aren’t going to handle it! Here’s what this class looks like:

public class CustomerAccountFormatter : ICustomFormatter

{

    public string Format(string format, object arg, IFormatProvider formatProvider)

    {

        if (arg == null)

            return string.Empty;

 

        if (format == null)

            return HandleOtherFormats(format, arg);

 

        if (format.Equals("LF", StringComparison.InvariantCultureIgnoreCase))

        {

            // Long format "HFD-01992039-13"

            return arg.ToString();

        }

        else if (format.Equals("SF", StringComparison.InvariantCultureIgnoreCase))

        {

            // Short format "01992039"

            return arg.ToString().Substring(4, 8);

        }

        else

        {

            return HandleOtherFormats(format, arg);

        }

    }

 

    /// <summary>

    /// Handles all other formats.

    /// </summary>

    private string HandleOtherFormats(string format, object arg)

    {

        if (arg is IFormattable)

            return ((IFormattable)arg).ToString(format, CultureInfo.CurrentCulture);

        else if (arg != null)

            return arg.ToString();

        else

            return String.Empty;

    }

}

So that was kind of messy, huh? I agree. But let’s try to break this down into the pieces. When I call:

String.Format("Long format:  {0:LF}", customerAccount)

it checks to see if customerAccount implements IFormattable. Since it does, it calls the .ToString(format, IFormatProvider) overload, which consequently calls the .Format() on our custom formatter. That’s how “LF” gets translated into the “long format” of our account number. When I call:

customerAccount.ToString("LF")

I actually have a couple of convenience-overloads for that. Here those are:

/// <summary>

/// Returns the string representation of this object.

/// </summary>

/// <remarks>Defaults to "LF" (long-format) of account number in the form

/// of XXX-NNNNNNNN-WWW.</remarks>

public override string ToString()

{

    return ToString("LF", this);

}

 

/// <summary>

/// Returns the string representation of this object, specifying the format.

/// LF=Long format; SF=Short format.

/// </summary>

public string ToString(string format)

{

    return ToString(format, this);

}

 

/// <summary>

/// Returns the string representation of this object, specifying the format

/// and format provider.

/// </summary>

/// <param name="format">Format of the account number: LF=Long format; SF=Short format.</param>

/// <param name="formatProvider">The format provider to use, if different than that the default.</param>

public string ToString(string format, IFormatProvider formatProvider)

{

    return customerAccountFormatter.Format(format, AccountNumber, this);

}

So when I just call .ToString(“LF”), you can see that calls the .ToString(string, IFormatProvider) overload passing in the right information. I would guess that this part is correct, because I could call formatProvider.GetFormat() to find out what the formatter is for that object. Again, I am not sure of the best-practice here – but this should get you (or future-me) in the ballpark if you ever need this type of functionality for ToString or String.Format. Enjoy!

Posted in .NET 4.0, Uncategorized

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Archives
Categories

Enter your email address to follow this blog and receive notifications of new posts by email.

Join 2 other followers

%d bloggers like this: