Scott Hanselman

A Smarter (or Pure Evil) ToString with Extension Methods

April 27, 2008 Comment on this post [24] Posted in Programming
Sponsored By

Three years ago I postulated about a ToString implementation for C# that seemed useful to me and a few days later I threw it out on the blog. We used in at my old company for a number of things.

Just now I realized that it'd be even more useful (or more evil) with Extension Methods, so I opened up the old project, threw in some MbUnit tests and changed my implementation to use extension methods.

So, like bad take-out food, here it is again, updated as ToString() overloads:

[Test]
public void MakeSimplePersonFormattedStringWithDoubleFormatted()
{
Person p = new Person();
string foo = p.ToString("{Money:C} {LastName}, {ScottName} {BirthDate}");
Assert.AreEqual("$3.43 Hanselman, {ScottName} 1/22/1974 12:00:00 AM", foo);
}

[Test]
public void MakeSimplePersonFormattedStringWithDoubleFormattedInHongKong()
{
Person p = new Person();
string foo = p.ToString("{Money:C} {LastName}, {ScottName} {BirthDate}",new System.Globalization.CultureInfo("zh-hk"));
Assert.AreEqual("HK$3.43 Hanselman, {ScottName} 1/22/1974 12:00:00 AM", foo);
}

It's moderately well covered for all of the hour it took to write it, and I find it useful.

image

Here's all it is.

public static class FormattableObject 
{
public static string ToString(this object anObject, string aFormat)
{
return FormattableObject.ToString(anObject, aFormat, null);
}

public static string ToString(this object anObject, string aFormat, IFormatProvider formatProvider)
{
StringBuilder sb = new StringBuilder();
Type type = anObject.GetType();
Regex reg = new Regex(@"({)([^}]+)(})",RegexOptions.IgnoreCase);
MatchCollection mc = reg.Matches(aFormat);
int startIndex = 0;
foreach(Match m in mc)
{
Group g = m.Groups[2]; //it's second in the match between { and }
int length = g.Index - startIndex -1;
sb.Append(aFormat.Substring(startIndex,length));

string toGet = String.Empty;
string toFormat = String.Empty;
int formatIndex = g.Value.IndexOf(":"); //formatting would be to the right of a :
if (formatIndex == -1) //no formatting, no worries
{
toGet = g.Value;
}
else //pickup the formatting
{
toGet = g.Value.Substring(0,formatIndex);
toFormat = g.Value.Substring(formatIndex+1);
}

//first try properties
PropertyInfo retrievedProperty = type.GetProperty(toGet);
Type retrievedType = null;
object retrievedObject = null;
if(retrievedProperty != null)
{
retrievedType = retrievedProperty.PropertyType;
retrievedObject = retrievedProperty.GetValue(anObject,null);
}
else //try fields
{
FieldInfo retrievedField = type.GetField(toGet);
if (retrievedField != null)
{
retrievedType = retrievedField.FieldType;
retrievedObject = retrievedField.GetValue(anObject);
}
}

if (retrievedType != null ) //Cool, we found something
{
string result = String.Empty;
if(toFormat == String.Empty) //no format info
{
result = retrievedType.InvokeMember("ToString",
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase
,null,retrievedObject,null) as string;
}
else //format info
{
result = retrievedType.InvokeMember("ToString",
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase
,null,retrievedObject,new object[]{toFormat,formatProvider}) as string;
}
sb.Append(result);
}
else //didn't find a property with that name, so be gracious and put it back
{
sb.Append("{");
sb.Append(g.Value);
sb.Append("}");
}
startIndex = g.Index + g.Length +1 ;
}
if (startIndex < aFormat.Length) //include the rest (end) of the string
{
sb.Append(aFormat.Substring(startIndex));
}
return sb.ToString();
}
}

Enjoy.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Hosting By
Hosted in an Azure App Service
April 27, 2008 13:27
That's pretty sweet! Extending it to support ToHtml() or ToXml() or ToJSON() or ToYAML() could be useful, although that's taking it too far perhaps.
April 27, 2008 13:45
You have just created a very slow method that you can not check at compile time. Refactoring the propertynames will probably not find these strings, so it will fail to produce the expected string at runtime if you refactor.

Scott, I have great respect for you, but this feels like a very dangerous anti-pattern to me!! ;-)
April 27, 2008 15:54
I like this version of ToString.

I'd probably call TypeDescriptor.GetProperties though instead of doing reflection manually. That's more in-line with how Data Binding resolves properties and gives the runtime a chance to cache the descriptors. The descriptors still use MemberInfo.GetValue/SetValue, but the runtime may speed this up someday by creating delegates and invoking them directly.

@Ruurd: true, but plenty of other things won't be found when you refactor either. References to objects from XAML, XAML {Binding } elements, Windows Forms data bindings, the list goes on. That's why we test :)
April 27, 2008 18:14
There has always been a strong urge to have some basic form of serialization baked into System.Object, hasn't there? I don't think ToString is the way, though. Thanks for dusting this one off and updating though, Scott. Interesting pattern or anti-pattern as Ruurd said.
April 27, 2008 18:45
It's nice, but I don't like the fact, that you have to use reflection for that, and that your friend compiler, won't alert you when you make a typo. That being said, I'd LOVE see in C# what you can do with BOO. I blogged about it some time ago: here.
April 27, 2008 20:15
It looks nice except the fact that has been mentioned by Krzysztof and Ruurd.
It'd be better if you could cache them, but this won't clear the issue related with the compiler.
April 27, 2008 21:17
Ruurd - That's why I gave several caveats that it's possibly evil. However, it's no more even than XPath or RegEx, examples of two other "tunnelled languages" that can't live up to refactoring. Any idea how we could "LINQify" it? I'm not sure we could and still keep the formatting syntax. Being able to go {Birthday:yyyyMMdd} is pretty neat, IMHO.

Paul - I'll take a look at TypeDescriptor. There's another interesting pattern where you can code-gen via CodeDom a property getter, then stick that newly generated code into a hashtable and call it MUCH faster than Reflection. I might do a post on that just because it's obscure. Of course, I'll update this when C# has a "dynamic" keyword. ;)
April 27, 2008 21:56
Scott: you're right. You did mention it might be evil, but I can see people reading your blog and taking it's ideas without thinking about these things. ;-)
I'm real hot for WPF, but Xaml is evil in this respect as well (think databinding.. ouch).

Anyway, I always create const strings that define the names inside of my class, so that I can use them all over the code base and only having to change them in one place (right next to the definition of the property itself!). I'll use those in String.Formats and whatnot.

I'll have to think about LINQifying. Don't see an obvious route to take!
April 28, 2008 1:33
It's "threw" not "through". But on the topic at hand this is a failry anti-pattern cool idea. LINQifying it would however make it just plain cool!
April 28, 2008 2:44
PingBack...
http://www.acceptedeclectic.com/2008/04/when-should-i-employ-extension-methods.html

^^In the above linked post, I cite your work and I mention how much I love this formatting trick, but I'm struggling to understand what is the benefit of the extension method part.
April 28, 2008 12:59
I like it.

As far as people who are concerned about refactoring and compile-time checking...

Compiling != Testing... Testing == Testing !!!

As Scott H says, there are plenty of other examples of tunnelled languages that can contain syntactic errors that won't be picked up by the compiler (SQL as well, in addition to the examples Scott H provides).

If you wanted to you could implement some kind of static checking process that would check this for errors.
April 29, 2008 1:20
James, this is the mantra I use to justify Xaml. But really, when you think about it, you should not mix programming styles. Let dynamic languages be dynamic, and let static languages be static. It's their greatest strenght, and you shouldn't take away from it.
April 29, 2008 14:44
test
April 29, 2008 14:58
Testing OpenId. Any one who knows CSS have any idea why my theme doesn't like the openid watermark? Also, I'd like the OpenId graphic to float next to the person's name...
April 29, 2008 16:02
Is there a limit on how long the OpenId Url can be in the comment form?
April 29, 2008 21:07
I think I made it 96. That's arbitrary, as I need to check the spec. It's probably 255, but I think 96 is not unreasonable.

Any ideas on how I can make the text box the same length as the others?
April 29, 2008 23:57
I think I like it. Have you seen James Newton-King's FormatWith? It's very similar, except that he extends string instead of object.

May 01, 2008 3:28
Why did you use reflector to call ToString() method ?

result = retrievedType.InvokeMember("ToString",
BindingFlags.Public | BindingFlags.NonPublic |
BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.IgnoreCase
, null, retrievedObject, new object[] { toFormat, formatProvider }) as string;
why not just:
result = retrievedObject.ToString(toFormat, formatProvider);

??

Then, you regex (@"({)([^}]+)(})") is not fully correct. You use recursion for field/properties wich formatting.
So if write:
myObject.ToString("{MyProp:format}")
here 'format' is format-string which will be transmitted into next call ot ToString() - for the MyProp's value.
If so it's logical to expect that format-string can be really format-string wich own formatting:
myObject.ToString("{MyProp:{MyPropOfValueOfMyProp}}")
but it won't work due to your regex.
May 01, 2008 4:41
Shrike - Good points. For the first call to ToString(), you're right, that can be called without reflection. I guess I got a little "reflection-happy."

For the *second* call, if I change that to call directly it actually calls the extension method again and recurses until it dies. That makes me realize that perhaps I'd need another parameter, or even better, to name ToString something else like ToStringWithFormatting.
May 01, 2008 19:07
I created this exact same extension method a while ago, though I called it FormatObject() instead of ToString(). I don't agree with the objection about refactoring. Interfaces, especially public ones, should NOT change that frequently even during development. I can name a LOT of places where refactoring is difficult today (property change notification, anybody?), and we live with it.
May 02, 2008 0:25
OpenID Test (sorry, I couldn't resist)
May 02, 2008 13:12
I didn't even realize that I already had an account at MyOpenID
May 03, 2008 18:51
I wrote something like this a while back although it is an extension of string.Format rather than ToString (it would be easy enough to change the arguments around)

I used the DataBinder.Eval method to do most of the heavy lifting.

FormatWith 2.0 - String formatting with named variables

May 03, 2008 18:55
Haha, just tested the link to my blog and I noticed you had already found and commented my post :)

Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.