Monday, 24 August 2009

StringBuilder Extensions

I recently had a requirement to modify some Html-based reports.  These reports were built up in code using a StringBuilder, along with report content data.  The plus-point is that it worked and delivered the desired reports.  On the downside, the code was tedious and verbose.  For example:
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("<TABLE id=\"Table2\" width=\"100%\">");
stringBuilder.Append("<TR style=\"page-break-inside : avoid\"><TD class=\"reportHeaderTitle\" >" + ResourceManager.GetString("ReportTitle", culture) + "</TD></TR>");
stringBuilder.Append("<TR><TD style=\"PADDING-LEFT: 5px; PADDING-BOTTOM:10px\" width=\"100%\">");
stringBuilder.Append("<TABLE id=\"Table2\" width=\"100%\">");
stringBuilder.Append("<TR style=\"page-break-inside : avoid\"><TD class='reportHeader' align=\"center\" width=\"50%\">" + ResourceManager.GetString("ReportSubTitle", culture) + "</TD>");
stringBuilder.Append("<TD class='reportHeader' align=\"center\" width=\"50%\">" + ResourceManager.GetString("ReportResults", culture) + "</TD></TR>");
...

I was staring at 1000s of lines of code like this.  After a while it was all a blur.

My first impulse was to subclass the StringBuilder and build up some Html-specific methods.  Unfortunately, the StringBuilder class is sealed.   I also looked at the HtmlTextWriter class, a class primarily used when working with custom server controls.  But, this seemed overkill for the purpose.  I just needed a clean way to build-up an Html string in code.

I then turned to writing a set of Extensions Methods to provide this functionality.
#region Using Directives

using System.Text;

#endregion

namespace Common.Extensions
{
    public static class StringBuilderExtensions
    {
        public static void BeginTable(this StringBuilder stringBuilder)
        {
            stringBuilder.Append("<table>");
        }

        public static void EndTable(this StringBuilder stringBuilder)
        {
            stringBuilder.Append("</table>");
        }

        public static void BeginRow(this StringBuilder stringBuilder)
        {
            stringBuilder.Append("<tr>");
        }
        ...
    }
}

In refactoring the code I first pulled all of the inline styles out and into style classes.  Then I began migrating the existing code over to use the new extension methods.  The refactored code looks something like this:

var stringBuilder = new StringBuilder();
stringBuilder.HorizontalRule();
stringBuilder.BeginH2();
stringBuilder.Append(ResourceManager.GetString("ReportTitle", _Culture));
stringBuilder.EndH2();

stringBuilder.BeginTable();
stringBuilder.BeginTHead();
stringBuilder.BeginRow();
stringBuilder.BeginCell("reportHeader");
stringBuilder.Append(ResourceManager.GetString("ReportSubTitle", _Culture));
stringBuilder.EndCell();
stringBuilder.BeginCell();
stringBuilder.Append(ResourceManager.GetString("ReportResults", _Culture));
stringBuilder.EndCell();
stringBuilder.EndRow();
stringBuilder.EndTHead();
...
In it's current state, the extensions only supply the Html tags that were required, but could easily be expanded to support all of the commonly used tags.  I personally find this refactored code much easier to read and maintain.

You can download the StringBuilderExtensions.zip here.
I hope this may be of some use.

4 comments:

Avner said...

Nice. I would return an IDisposable object from BeginTable, BeginRow, etc. methods in order to leverage the using keyword and make the code a little more correlated to the hierarchy of the elements in the table. (The Dispose method should call EndTable, EndRow, etc.)

rgramann said...

Hmm...I suspect that may complicate the code, having to hold a reference to the returned object. As well, myObject.Dispose() seems less intuitive than stringBuilder.EndTable(). Further, and more importantly, the IDisposable interface is primarily used to release unmanaged resources. Using the Dispose method to append a string to a StringBuilder,IMHO seems inappropriate.

Chuck said...

I like it.

rgramann said...

Thanks, Chuck