Fun With Extension Methods - Extending Object Part 1

Some time ago I was showing a colleague how to enhance an object's Design Time capabilities (or actually Debug Time) by adding a DebuggerDisplayAttribute. I blogged about this attribute earlier, so I won't go into it again now. But what I do want to talk about is the way the attribute gets its data.

Introduction

In my previous article, I showed you how to use property names in a format string, directly in the constructor of the DebuggerDisplayAttribute, like this:

VB.NET
<DebuggerDisplayAttribute("Bug: {Title} Priority: {Priority}")> _
Public Class Bug

End Class    

C#
[DebuggerDisplayAttribute("Bug: {Title} Priority: {Priority}")]
public class Bug
{

}      

What you may not know is that instead of providing property names in this constructor, it's also possible to provide a method name that returns a string, like this:

VB.NET
<DebuggerDisplay("{DebugInfo()}")> _
Public Class Bug

End Class    

C#
[DebuggerDisplay("{DebugInfo()}")]
public class Bug
{

}      

This is useful if you want to apply some logic. For example, if the class is a collection, you can display the number of items and if the collection is not empty, display information about the first item (just an example; you can basically do whatever you want in this method). Here's a short example:

VB.NET
Private Function DebugInfo() As String
  Dim result As String = String.Format("Count: {0}. ", Count)
  If this.Items.Count > 0 Then
    result += string.Format("First item: {0}.", this.Items(0).ToString())
  End If
  return result
End Function

C#
private string DebugInfo()
{
  string result = string.Format("Count: {0}. ", Count);
  if (this.Items.Count > 0)
  {
    result += string.Format("First item: {0}.", this.Items[0].ToString());
  }
  return result;
} 

This is all relatively easy and straightforward to implement. However, if you want to display a lot of properties, you quickly end up with a big string.Format mess with lots of {n} placeholders and property names. But do you really need to use string.Format? And why can't we just use the property names of an object directly in the format string, as the DebuggerDisplayAttribute allows us to do? Why does string.Format not support this:

VB.NET
Dim result As String = _
           String.Format("Bug: {Title} Priority: {Priority}", myBug)
C#
string result = 
              string.Format("Bug: {Title} Priority: {Priority}", myBug);

This code would take the Bug instance, get the values of the Title and Priority properties and then replace their respective placeholders in the format string.

Fortunately, there are existing extension methods available that allow you to do something very close to this. They allow you to add an additional ToString() overload to the object class that provides this very behavior. Some time ago, I found the following two methods; one by Scott Hanselman, and the other by James Newton-King

Both proposed solutions had their own advantages. The first implementation overloads ToString() on object and allows you to skip some elements that don't have associated properties on the object being formatted. So you can do something like this:

string result = myPerson.ToString("Person: {Name} {SomeLiteral}");

which could result in a text like "Imar Spaanjaars {SomeLiteral}".

The second implementation uses a FormatWith extension method on the string class and allows you to access sub properties, enabling you to write something like this:

string result = 
    "Person: {Id} First address: {Addresses[0].Street}".FormatWith(myPerson);

Since both methods had their own strong advantages, it made a lot of sense to combine them. It turned out that was pretty easy to do: steal some from Scott, mix it with some stuff from James, add a tiny bit of code myself and voilà: two nice extension methods for object. Because the code uses DataBinder.Eval, it's no longer easy to detect non-existing properties. Therefore, I decided to modify the code slightly, and use a double pair of curly braces to define a literal value that should be left alone. All property names wrapped in a single pair of braces are sent to DataBinder.Eval where they cause an error if the object being formatted doesn't have a property by that name.

public static class ObjectExtensions
{
  // The ToString overloads are inspired by:
  // A Smarter (or Pure Evil) ToString with Extension Methods
  // FormatWith 2.0 - String formatting with named variables
  // This implementation is a combination of both: the regular expressions 
  // from Scott Hanselman combined with the DataBinder.Eval idea 
  // from James Newton-King.
  
/// <summary> /// Enables you to get a string representation of the object using string /// formatting with property names, rather than index based values. /// </summary> /// <param name="anObject">The object being extended.</param> /// <param name="aFormat">The formatting string, like "Hi, my name /// is {FirstName} {LastName}".</param> /// <returns>A formatted string with the values from the object replaced /// in the format string.</returns> /// <remarks>To embed a pair of {} on the string, simply double them: /// "I am a {{Literal}}".</remarks> public static string ToString(this object anObject, string aFormat) { return ObjectExtensions.ToString(anObject, aFormat, null); } /// <summary> /// Enables you to get a string representation of the object using string /// formatting with property names, rather than index based values. /// </summary> /// <param name="anObject">The object being extended.</param> /// <param name="aFormat">The formatting string, like "Hi, my name is /// {FirstName} {LastName}".</param> /// <param name="formatProvider">An System.IFormatProvider that /// provides culture-specific formatting information.</param> /// <returns>A formatted string with the values from the object replaced in /// the format string.</returns> /// <remarks>To embed a pair of {} on the string, simply double them: /// "I am a {{Literal}}".</remarks> 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]; 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(":"); if (formatIndex == -1) { toGet = g.Value; } else { toGet = g.Value.Substring(0, formatIndex); toFormat = g.Value.Substring(formatIndex + 1); } if (!toGet.StartsWith("{")) // Make sure we're not dealing // with a string literal wrapped in {} { // Get the object's value using DataBinder.Eval. object resultAsObject = DataBinder.Eval(anObject, toGet); // Format the value based on the incoming formatProvider // and format string string result = string.Format(formatProvider, "{0:" + toFormat + "}", resultAsObject); sb.Append(result); } else // Property name started with a { which means we treat it // as a literal. { sb.Append(g.Value); } startIndex = g.Index + g.Length + 1; } if (startIndex < aFormat.Length) { sb.Append(aFormat.Substring(startIndex)); } return sb.ToString(); } }

With this extension method, you can now do stuff like this:

[TestMethod]
public void FormatString()
{
  Person myPerson = new Person("Imar", "Spaanjaars");
  string result = myPerson.ToString(
            "FirstName: {FirstName} with LastName: {LastName}");
  Assert.AreEqual("FirstName: Imar with LastName: Spaanjaars", result);
}

 

I took the set of tests that Scott supplied in his download, added a few of my own and ran them all. After some changes to accommodate for the different behavior regarding literals using {} and some culture specific issues, they all ran fine. However, it doesn't mean I have tested every possible scenario.

So, if you use this code: do so with caution. Take some time to read the original articles, understand the code and implementation differences, then turn to this one if you like it. If you find a bug, or a scenario where this doesn't work, let me know.

If you want a VB version of the extension methods: take a look here or send me an e-mail and ask me nicely. Of course clicking the Donate button speeds things up considerably.... ;-)


Where to Next?

Wonder where to go next? You can post a comment on this article. You can read existing comments below or you can post a comment yourself on this article .


Consider making a donation
Please consider making a donation using PayPal. Your donation helps me to pay the bills so I can keep running Imar.Spaanjaars.Com, providing fresh content as often as possible.



Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the QuickDocId of the document.

For more information about the Talk Back feature, check out this news item.