General Formatter for .NET 4/4: Examples

This is the last of four parts this article consists of:

  • Introduction - Part in which proposed goals and desired features of the solution are defined.

  • Design - Part in which solution design is outlined, explaining critical details that the solution will have to implement.

  • Implementation - Third part which explains actual implementation of the formatter classes.

  • Example - Final part which lists numerous examples of formatters use.

The last part of the article has a compressed file attached with it, containing complete and commented source code of the formatter classes, source code of the demonstration project and compiled library.

How to Use

VerboseFormatInfoBase and VerboseFormatInfo classes implement IFormatProvider interface which implies that they can be used as any other .NET format provider. For example, this line of code will print the content of a Point structure:

Console.WriteLine(string.Format(new VerboseFormatInfo(), "{0}", new Point(3, 4)));

Output produced by this line of code is:

Point {bool IsEmpty=false, int X=3, int Y=4}

More options are available if one allocates the formatter before its use and then set its properties to appropriate values:

VerboseFormatInfo vfi = new
VerboseFormatInfo();
vfi.FieldDelimiter = "; ";
vfi.FirstContainedValuePrefix = "(";
vfi.LastContainedValueSuffix = ")";
vfi.InstanceName = "center";
 
Console.WriteLine(string.Format(vfi, "{0}", new Point(3, 4)));

This would produce somewhat different output:

Point center (bool IsEmpty=false; int X=3; int Y=4)

However, caller doesn't have to bother too much setting up formatter's properties. VerboseFormatInfo class exposes static properties that already have pre-set values of all relevant properties: SingleLinedFormat, SimpleFormat, MultiLinedFormat, TabbedMultiLinedFormat and TreeMultiLinedFormat. These properties return VerboseFormatInfoBase instances that are ready to be used to format strings:

Console.WriteLine(string.Format(VerboseFormatInfo.SimpleFormat, "{0}", new Point(3, 4)));

Even shorter notation is available, as VerboseFormatInfoBase class exposes Format method which receives a single object, which is the object being formatted, and returns formatted string. Hence, the same effect as with line above can be achieved with this code:

Console.WriteLine(VerboseFormatInfo.SimpleFormat.Format(new Point(3, 4)));

In the following section we will demonstrate full power of the VerboseFormatInfo class. Up to this point we have already seen one of its most powerful features - simplicity. Caller is almost never expected to do anything regarding string formatting, apart from simply instantiating appropriate form of the verbose formatter. The formatter can then be used by either providing it to the String.Format method or by calling its Format method directly.

For instance, ToString method of the VerboseFormatInfoBase class (and hence all classes derived from it) looks like this:

public override string ToString()
{
    return FormatInfoUtils.CreateSimpleFormatter().Format(this)
}
In this code FormatInfoUtils is the static class internally used by verbose formatters. In general case, ToString method of any class can be
implemented using the same pattern:
public override string ToString()
{
    return VerboseFormatInfo.SimpleFormat.Format(this);
}

Thanks to this single-liner, any VerboseFormatInfo class instance can be simply formatted into string without any additional effort:

Console.WriteLine(VerboseFormatInfo.TreeMultiLinedFormat);

This code provides quite verbose description of the TreeMultiLinedFormat instance:

VerboseFormatInfo {string FieldDelimiter="\r\n", string FirstContainedValuePrefix="{", int IndentationLevel=0, string IndentationString=" |   ", Type InstanceDataType=null, string InstanceName=null, bool IsMultiLinedFormat=true, string LastContainedValueSuffix="}", string LastIndentationString=" |   ", string LastRightMostIndentationString=" +-- ", string LinePrefix="", int MaximumDepth=5, int MaximumFormattedLength=-1, int RawMaximumFormattedLength=80, string RightMostIndentationString=" |-- ", bool ShowDataType=true, bool ShowInstanceName=true, VerboseFormatInfo ValueOnly={}}

Just try to imagine how much coding it would require to produce the same output manually, let alone errors that would probably arise here and there in this long string. If one is not satisfied with such long output, the same instance can be formatted in a bit different way:

VerboseFormatInfoBase vfi = VerboseFormatInfo.TreeMultiLinedFormat;

vfi.MaximumDepth = 1;

Console.WriteLine(vfi.Format(vfi));

And this is the output produced:

VerboseFormatInfo {
 |-- string FieldDelimiter="\r\n"
 |-- string FirstContainedValuePrefix="{"
 |-- int IndentationLevel=0
 |-- string IndentationString=" |   "
 |-- Type InstanceDataType=null
 |-- string InstanceName=null
 |-- bool IsMultiLinedFormat=true
 |-- string LastContainedValueSuffix="}"
 |-- string LastIndentationString=" |   "
 |-- string LastRightMostIndentationString=" +-- "
 |-- string LinePrefix=""
 |-- int MaximumDepth=1
 |-- int MaximumFormattedLength=-1
 |-- int RawMaximumFormattedLength=80
 |-- string RightMostIndentationString=" |-- "
 |-- bool ShowDataType=true
 |-- bool ShowInstanceName=true
 +-- VerboseFormatInfo ValueOnly={}}

Depending on visual settings, i.e. the viewer and fonts used, this form could be much easier to read.

Examples

In this section we will present several examples of VerboseFormatInfo class use. First we will start with very simple case of an array of characters:

char[] charArray = new char[] { 'A', '\n', '\t', (char)8, 's', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g' };

Console.WriteLine(VerboseFormatInfo.SimpleFormat.Format(charArray));

This code produces output:

char[13] {'A', '\n', '\t', 0x08, 's', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g'}

Output shows that type of the formatted object is array of characters of dimension 13. Further on, all characters are presented, conveniently showing special new line, tab and backspace character (ASCII code 8) differently from plain letters. It is worthy to notice that exactly the same output is produced when MultiLinedFormat is used:

Console.WriteLine(VerboseFormatInfo.MultiLinedFormat.Format(charArray));

Output is the same because VerboseFormatInfo class has first tried to inline the array, and that attempt ended in a success. So there was no reason to break lines and to write the output over multiple lines.

char[13] {'A', '\n', '\t', 0x08, 's', 'o', 'm', 'e', 't', 'h', 'i', 'n', 'g'}

However, next example is not that simple:

string[] text = new string[]

{

    "The", "following", "code", "example", "demonstrates", "the",

    "implementation", "of", "the", "IEnumerable", "and", "IEnumerator",

    "interfaces", "for", "a", "custom", "collection.", "In", "this",

    "example,", "members", "of", "these", "interfaces", "are", "not",

    "explicitly", "called,", "but", "they", "are", "implemented",

    "to", "support", "the", "use", "of", "foreach", "(For", "Each",

    "in", "Visual", "Basic)", "to", "iterate", "through", "the", "collection."

};

Console.WriteLine(VerboseFormatInfo.MultiLinedFormat.Format(text));

In this example we are printing out array of strings (which is actually a paragraph copied from MSDN online edition). Output would in this case look as follows:

string[48] {"The"            "following"      "code"           "example"
            "demonstrates"   "the"            "implementation" "of"
            "the"            "IEnumerable"    "and"            "IEnumerator"
            "interfaces"     "for"            "a"              "custom"
            "collection."    "In"             "this"           "example,"
            "members"        "of"             "these"          "interfaces"
            "are"            "not"            "explicitly"     "called,"
            "but"            "they"           "are"            "implemented"
            "to"             "support"        "the"            "use"
            "of"             "foreach"        "(For"           "Each"
            "in"             "Visual"         "Basic)"         "to"
            "iterate"        "through"        "the"            "collection."   }

Verbose formatter has determined that content is so long that it cannot be written in one line. Consequently, output has been broken over several lines, strictly taking care that none of the lines is longer than 80 characters. All values printed are aligned so that longest string fits into the layout. This example has shown that MultiLinedFormat is more flexible than SingleLinedFormat and it should be used in cases when output may freely span over multiple lines if it is too long.

Next example will be the jagged array:

int jaggedArrayRows = 12;

int[][] jaggedIntArray = new int[jaggedArrayRows][];

for (int i = 0; i < jaggedArrayRows; i++)

{

    int jaggedArrayCols = rnd.Next(40) + 1;

    jaggedIntArray[i] = new int[jaggedArrayCols];

    for (int j = 0; j < jaggedArrayCols; j++)

        jaggedIntArray[i][j] = rnd.Next(1000);

}

Console.WriteLine(VerboseFormatInfo.MultiLinedFormat.Format(jaggedIntArray));

Here we are creating a jagged array with 12 rows and variable number of columns. We are using MultiLinedFormat again to produce the following output:

int[12][3-33] {
    Row 0  {465 618 681 359 895 451 40  869 525 435 576 620 975 965 589 939 324}
    Row 1  {861 542 181 461 972 568 23  675 534 662 594 55  242 619 736 523 22  350 967 414
            338 571 559 303 687 948}
    Row 2  {261 570 438 525 262 756 167 101 906 218 3   645 113 336 219 916 377 274 470 192
            957 883 451 942 239 972 910 305 290 983 878 773 24 }
    Row 3  {761 105 96  137 332 299 75  517 830 304 18  704 853 78  118 654 181 29  217 755
            853 378 118 495 285 290 845 861 811 923 340}
    Row 4  {621 355 574 114 820 239 941 171 117 439}
    Row 5  {147 923 535}
    Row 6  {832 187 636 801 849}
    Row 7  {269 904 383 987 600 852 42  453 213 706 906 963 788 82  497 503 4   834 942 87
            45  637 413}
    Row 8  {971 572 750 171 12  674 175 121 491 673 352 451 191 127}
    Row 9  {387 899 718 904 733 146 183 134 452}
    Row 10 {328 182 802 859 762 511 631 490 57  15  28  102 870 440 539 531 784 472 115 730
            46  312 877 613 554}
    Row 11 {326 733 680 179 787 437 297 550}}

Columns are again aligned, although some lines are so long that they must be broken into multiple lines. At this point we will demonstrate the use of TreeMultiLinedFormat property:

Console.WriteLine(VerboseFormatInfo.TreeMultiLinedFormat.Format(jaggedIntArray));

Output is now different:

int[12][1-39] {
 |-- Row 0  {589 142 240 99  461 910 998 406 267 430 724 960 316 242 752 224 416 611 879 121
 |           195 889 118 378 934 776 187 939 229 739 869 401 748 588 144 502}
 |-- Row 1  {529 720 159 538 440 760 936 185 721 737 771 148 949 781 86  678 606 699 699 23
 |           862 165 685 722 59  176 528 561 323 212 727 98  250 255 886 890 720 582 754}
 |-- Row 2  {182 192 213 38  416 790}
 |-- Row 3  {957 782 723 141 888 444 479 107 364 35  436 479}
 |-- Row 4  {232 375 861 509 10  672 898 694 894 195 957}
 |-- Row 5  {945}
 |-- Row 6  {841 670 952 646 306 268 896 570 778 600 70  838 654 770 148 522 854 283 103 490
 |           896 806 330}
 |-- Row 7  {28  743 892 585 63  587 765 117 943 873 637 436 412 388 130 210 367 661 597 261
 |           438 171 18  127 546 372 340 674 921}
 |-- Row 8  {673 34  339 248 618 563 376 310 507 191 835 953 895 780 132 711 109 466 153}
 |-- Row 9  {122 235 209 69  265 856 724 765 39  690 246 91  196 487 199 603 97  164 770 566
 |           833 57  154 406 426 484 275 237 994 835 262 874 520 27  333 438}
 |-- Row 10 {270 983 761 838 611 271 816 944 743 757 407 581 529 613 945 696 586 59  64  81
 |           803 642 780 581 487 770 203 428 371 570 168 154 760 778}
 +-- Row 11 {181 8   728 222 785 337 461 682 727 867 656 464 221 316 178 933 968 268 357 21
             627 202 180 351 840 67  388 573 173 589 253 820 751 785}}

However, full power of the TreeMultiLinedFormat can be seen when applied to more complex objects. In the next example we will create a hash table and fill it with number of mixed Point and Rectangle structures:

Hashtable hash = new Hashtable();
int dictSize = 9;
for (int i = 0; i < dictSize; i++)
{
    int key = 0;
    do
    {
        key = rnd.Next(1000);
    }
    while (dict.ContainsKey(key));
 
    Point value = new Point(rnd.Next(100), rnd.Next(70));
    object objValue = value;
 
    if (key % 3 == 0)
        objValue = new Rectangle(value, new Size(rnd.Next(90), rnd.Next(30)));
 
    hash.Add(key, objValue);
 
}
Console.WriteLine(VerboseFormatInfo.TreeMultiLinedFormat.Format(hash));

Resulting object is relatively complex and we are printing it out using TreeMultiLinedFormat to produce the following output:

Hashtable = {
 |-- Item[0] = {
 |    |-- int Key=100
 |    +-- Point Value={bool IsEmpty=false, int X=39, int Y=49}}
 |-- Item[1] = {
 |    |-- int Key=470
 |    +-- Point Value={bool IsEmpty=false, int X=30, int Y=35}}
 |-- Item[2] = {
 |    |-- int Key=979
 |    +-- Point Value={bool IsEmpty=false, int X=5, int Y=43}}
 |-- Item[3] = {
 |    |-- int Key=893
 |    +-- Point Value={bool IsEmpty=false, int X=33, int Y=51}}
 |-- Item[4] = {
 |    |-- int Key=567
 |    +-- Rectangle Value = {
 |         |-- int Bottom=34
 |         |-- int Height=27
 |         |-- bool IsEmpty=false
 |         |-- int Left=81
 |         |-- Point Location={bool IsEmpty=false, int X=81, int Y=7}
 |         |-- int Right=164
 |         |-- Size Size={int Height=27, bool IsEmpty=false, int Width=83}
 |         |-- int Top=7
 |         |-- int Width=83
 |         |-- int X=81
 |         +-- int Y=7}}
 |-- Item[5] = {
 |    |-- int Key=69
 |    +-- Rectangle Value = {
 |         |-- int Bottom=26
 |         |-- int Height=20
 |         |-- bool IsEmpty=false
 |         |-- int Left=49
 |         |-- Point Location={bool IsEmpty=false, int X=49, int Y=6}
 |         |-- int Right=130
 |         |-- Size Size={int Height=20, bool IsEmpty=false, int Width=81}
 |         |-- int Top=6
 |         |-- int Width=81
 |         |-- int X=49
 |         +-- int Y=6}}
 |-- Item[6] = {
 |    |-- int Key=921
 |    +-- Rectangle Value = {
 |         |-- int Bottom=52
 |         |-- int Height=1
 |         |-- bool IsEmpty=false
 |         |-- int Left=6
 |         |-- Point Location={bool IsEmpty=false, int X=6, int Y=51}
 |         |-- int Right=77
 |         |-- Size Size={int Height=1, bool IsEmpty=false, int Width=71}
 |         |-- int Top=51
 |         |-- int Width=71
 |         |-- int X=6
 |         +-- int Y=51}}
 |-- Item[7] = {
 |    |-- int Key=801
 |    +-- Rectangle Value = {
 |         |-- int Bottom=42
 |         |-- int Height=3
 |         |-- bool IsEmpty=false
 |         |-- int Left=82
 |         |-- Point Location={bool IsEmpty=false, int X=82, int Y=39}
 |         |-- int Right=137
 |         |-- Size Size={int Height=3, bool IsEmpty=false, int Width=55}
 |         |-- int Top=39
 |         |-- int Width=55
 |         |-- int X=82
 |         +-- int Y=39}}
 +-- Item[8] = {
      |-- int Key=341
      +-- Point Value={bool IsEmpty=false, int X=78, int Y=52}}}

This example demonstrates power of TreeMultiLinedFormat. It can be used to present complex objects in such way that output becomes quite readable on the screen, even when spanning dozens of lines. Though only when monospaced fonts are used, so be careful with this format. Note in the output above that all Point structures have been inlined, even those which are part of the Rectangle instance. On the other hand, none of the Rectangle instances has been inlined. All decisions have been made by the formatter having on mind length of the formatted object - points are producing short strings and they are inlined; rectangles produce long strings and they are broken over several lines.

Conclusion

In this article we have presented set of classes that can be used to convert any object into user friendly string representation. Conversion results are often not ideal, sometimes even far from desired, but this method has one significant advantage on its side: All formatting operations are single-liners. Even with the best of efforts, custom formatting cannot be performed in such a simple and short way. However, if custom formatting is still required, it can be applied manually, leaving less demanding tasks to the verbose formatter. If used in that pattern, verbose formatter can significantly reduce time needed to address the formatting issues in code.

Please feel free to download the source code attached. It contains full definition of all classes from the verbose formatter solution, as well as test bench project which demonstrates output produced by verbose formatter when applied to different objects.