65.9K
CodeProject is changing. Read more.
Home

Custom String FormatWith using Reflection

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2 votes)

Jul 7, 2012

CPOL

1 min read

viewsIcon

12163

This is an alternative for "Custom String FormatWith using Reflection"

Background

posted a Tip that was pretty cool, but forfeited the value-formatting ability of string.Format(). I thought there ought to be a way to accomplish both.

Using the code

This alternative is called in the same manner as the original Tip. I added an overload that takes an IFormatProvider to inform the formatting. This example is extended from the original Tip:

 UserInformation user = new UserInformation {
        FirstName = "Joe", 
        LastName = "Doe", 
        Address1 = "Joe's House", 
        City = "San    Jose", 
        Zipcode = "94101", 
        Email = "joe@doe.com", 
        PhoneNumber = 408000000
      };

      var userInfoXml = @"
<userinfo>
  <firstname>{FirstName,15}</firstname>
  <lastname>{LastName,-15}</lastname>
  <email>{{{Email}}}</email>
  <phone>{{PhoneNumber}}--{PhoneNumber:N0}</phone>
</userinfo>";

      Console.WriteLine(userInfoXml.FormatWithObject(user));
 

Which displays:

<userinfo>
  <firstname>            Joe</firstname>
  <lastname>Doe            </lastname>
  <email>{joe@doe.com}</email>
  <phone>{PhoneNumber}--408,000,000</phone>
</userinfo>

All of the normal Composite Formatting should work correctly.

Implementation

I changed this to rewrite the input string as a normal composite formatting string and then just called string.Format().

I simplified the construction of propertyNamesAndValues.

There is special case checking for the doubled curley braces ({{ or }}) appearing in the input string. They are subtituted out for characters that are "safe" (i.e. don't appear in the string) before rewriting the formatting string, and are restored before calling string.Format(). See the {{{Email}}} and {{PhoneNumber}} above. (This can be comparatively expensive!)

 
  public static class CustomStringFormattingExtensionMethods
  {
    public static string FormatWithObject(this string str, object o)
    {
      return FormatWithObject(str, o, CultureInfo.CurrentCulture);
    }
    public static string FormatWithObject(this string str, object o, IFormatProvider formatProvider)
    {
      if (o == null)
        return str;
      var propertyNamesAndValues = o.GetType()
        .GetProperties()
        .Where(pi => pi.CanRead)
        .Select(pi => new {
          pi.Name,
          Value = pi.GetValue(o, null)
        });

      char substLeftDouble = '\0';              // **very** unlikely
      char substRightDouble = substLeftDouble;  // initially equal
      if (str.Contains("{{") || str.Contains("}}"))
      {
        var strAndDigits = "0123456789" + str;
        while (strAndDigits.Contains(++substLeftDouble));
        substRightDouble = substLeftDouble;
        while (strAndDigits.Contains(++substRightDouble));
        str = Regex.Replace(str, "{{", new string(substLeftDouble, 1));
        str = Regex.Replace(str, "}}", new string(substRightDouble, 1), RegexOptions.RightToLeft);
      }

      var index = 0;
      foreach (var pnv in propertyNamesAndValues)
      {
        //str = str.Replace("{" + pnv.Name, "{" + index.ToString(CultureInfo.InvariantCulture));
        str = Regex.Replace(str, "{" + pnv.Name + @"\b", "{" + index.ToString(CultureInfo.InvariantCulture));
        index++;
      }
      if (substRightDouble != substLeftDouble)  // if they differ, then we need to handle this case
      {
        str = str.Replace(new string(substLeftDouble, 1), "{{").Replace(new string(substRightDouble, 1), "}}");
      }
      // this depends on the Select enumerating in the same order as foreach
      return string.Format(formatProvider, str, propertyNamesAndValues.Select(p => p.Value).ToArray());
    }
  }

Points of Interest

I started by using Regex heavily and kept refining it to simpler forms. Dealing with the doubled braces is kind of ugly, but I couldn't think of something simpler. strAndDigits is necessary because we're about to put numbers into the formatting string and must avoid using digits as the substitution characters.

This probably will not behave well if any property name is a proper prefix of another property name. This was fixed by changing the str.Replace() in the foreach to Regex.Replace() and adding the "\b" word boundary anchor to the match pattern.

History

  • July 7, 2012 Initial posting.
  • July 8, 2012 Updated to actually USE the IFormatProvider argument.
  • July 9, 2012 Fixed to work correctly if a property name is a proper prefix of another property name.