Scott Hanselman

A Smarter (or Pure Evil) ToString with Extension Methods

April 27, '08 Comments [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. I am a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Sunday, April 27, 2008 9:27:39 AM UTC
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.
Sunday, April 27, 2008 9:45:45 AM UTC
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!! ;-)
Sunday, April 27, 2008 11:54:24 AM UTC
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 :)
Sunday, April 27, 2008 2:14:17 PM UTC
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.
Sunday, April 27, 2008 2:45:42 PM UTC
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.
Sunday, April 27, 2008 4:15:59 PM UTC
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.
Tuna Toksoz
Sunday, April 27, 2008 5:17:22 PM UTC
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. ;)
Sunday, April 27, 2008 5:56:27 PM UTC
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!
Sunday, April 27, 2008 9:33:56 PM UTC
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!
Sunday, April 27, 2008 10:44:13 PM UTC
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.
Monday, April 28, 2008 8:59:10 AM UTC
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.
Monday, April 28, 2008 9:20:39 PM UTC
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.
Tuesday, April 29, 2008 10:44:24 AM UTC
test
Tuesday, April 29, 2008 10:58:18 AM UTC
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...
Tuesday, April 29, 2008 12:02:52 PM UTC
Is there a limit on how long the OpenId Url can be in the comment form?
Tuesday, April 29, 2008 5:07:30 PM UTC
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?
Tuesday, April 29, 2008 7:57:03 PM UTC
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.

Wednesday, April 30, 2008 11:28:12 PM UTC
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.
Shrike
Thursday, May 01, 2008 12:41:15 AM UTC
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.
Thursday, May 01, 2008 3:07:26 PM UTC
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.
Thursday, May 01, 2008 8:25:14 PM UTC
OpenID Test (sorry, I couldn't resist)
Friday, May 02, 2008 9:12:42 AM UTC
I didn't even realize that I already had an account at MyOpenID
Saturday, May 03, 2008 2:51:10 PM UTC
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

Saturday, May 03, 2008 2:55:49 PM UTC
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.