General Formatter for .NET 3/4: Implementation


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

  1. Introduction - Part in which proposed goals and desired features of the solution are defined.
  2. Design - Part in which solution design is outlined, explaining critical details that the solution will have to implement.
  3. Implementation - Third part which explains actual implementation of the formatter classes.
  4. 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.

Implementation

In this section we will explain the implementation of the solution described so far. General formatter is a class implementing IFormatProvider and ICustomFormatter interfaces. This is proposed method to implement formatters in .NET.

IFormatProvider exposes one callback function GetFormat, which is called by the Framework whenever string should be formatted to represent given object. This method receives type of the formatter requested. .NET Framework suggests that custom implementations should only implement GetFormat method to return a formatter of type implementing ICustomFormatter and to ignore all other calls, so to let .NET Framework make further decisions upon them. So in our case, whenever GetFormat is invoked with type set to ICustomFormatter, this implementation will return itself, i.e. this reference, which should be sufficient for .NET Framework to request formatting of the string that represents any particular object. Formatting itself is performed by the ICustomFormatter implementation, which is again a single method named Format.

Our implementation of the general formatter will start from the Format method as defined in the ICustomFormatter interface:

string Format(string format, object arg, IFormatProvider formatProvider)

Class from which everything begins is named VerboseFormatInfoBase. That is the base class for all formatting classes involved in this solution. It exposes default and copy constructors as well as cloning facility:

public abstract class VerboseFormatInfoBase: IFormatProvider, ICustomFormatter, ICloneable
{
    public VerboseFormatInfoBase();
    public VerboseFormatInfoBase(VerboseFormatInfoBase other;
    public abstract object Clone();
}


Further on it implements GetFormat and Format methods, as required by IFormatProvider and ICustomFormatter interfaces:

public virtual object GetFormat(Type formatType);
public virtual string Format(string format, object arg, IFormatProvider formatProvider);

After this formal part of the class, remaining members are dedicated to controlling and organizing formatting so that derived classes have to deal only with their specific tasks rather than with general tasks expected from the formatter. VerboseFormatInfoBase class defines virtual function IsFormatApplicable, which receives type of the object to which formatter would be applied:

internal virtual bool IsFormatApplicable(Type dataType)

Every derived implementation is required to return true from this method if it is applicable to given type. Otherwise it should return false. For example, EnumerableFormatInfo class, which handles objects implementing IEnumerable or generic IEnumerable looks like this:

        internal override bool IsFormatApplicable(Type dataType)
        {

            Type[] interfaces = null;
            if (dataType != null)
                interfaces = dataType.GetInterfaces();
            else
                interfaces = new Type[0];

            bool applicable = false;

            for (int i = 0; i < interfaces.Length; i++)
                if (interfaces[i].FullName == "System.Collections.IEnumerable" ||
                                      interfaces[i].FullName.StartsWith("System.Collections.Generic.IEnumerable`"))
                {
                    applicable = true;
                    break;
                }

            return applicable;
 
        }

This implementation iterates through interfaces implemented by the provided type and searches for IEnumerable and generic IEnumerable. If one of these is found, method returns success status. Otherwise, return value is false, indicating that EnumerableFormatInfo class cannot be applied to instances of given data type.

Next method provided by VerboseFormatInfoBase class is overloaded Format method with the following signature:

internal abstract bool Format(StringBuilder sb, string format, object arg, IFormatProvider formatProvider, ref int maxLength);

This method is crucial in this formatting implementation because all operations will, in one way or another, run through it. The first argument is the StringBuilder to which formatted content will be appended. Since Format method will be recursively invoked for contained objects, recursive invocations will append their output to the same StringBuilder, rather than create separate strings and then concatenate them, which would be quite inefficient.

The second argument to the Format method is the string representing the format pattern, and that is the string which was originally sent to the ICustomFormatter.Format method. In our implementation format is ignored because it would be quite difficult to implement it in full detail, having on mind large number of parameters that define the formatting.

Third argument of the Format method is the object which should be represented by the resulting formatted string. This is the object which was originally sent to the ICustomFormatter.Format method. Note that this argument may be null, which should be handled by the formatter appropriately.

Last argument of the Format method is integer maxLength. In single-lined formats this value specifies the largest number of characters allowed to the Format method to perform its task. Should Format method require more characters to complete, it would fail and append nothing to StringBuilder supplied. Otherwise, if it succeeds, resulting contents would be appended to the StringBuilder and maxLength would be reduced by the length of the appended string. For example, if maxLength had value 30 on input, and Format method completed successfully by appending total of 19 characters to the StringBuilder, then maxLength would have value 11 when Format method returns.

Format method returns Boolean value which indicates whether it has successfully appended formatted string to the StringBuilder or not. The only cause for failure is that maxLength value was insufficient and Format method could not perform the operation within given space.

At this point we will step a little bit in advance just to present a short example of how VerboseFormatInfo class, which is derived from VerboseFormatInfoBase, is used in practice. We will initialize a Point structure with specific coordinates and let VerboseFormatInfo class format a string which represents that point.

Point point = new Point(17, 42);
Console.WriteLine(string.Format(VerboseFormatInfo.SingleLinedFormat, "{0}", point));

This piece of code produces output the following output:

Point { bool IsEmpty=false, int X=17, int Y=42 }

VerboseFormatInfoBase class provides one more public overload of the Format method, which allows quick and simple conversion of objects to string:

public virtual string Format(object arg);

This implementation allows the caller to convert any object to string by only using the object of VerboseFormatInfoBase class, without need to access String.Format method. The example above would then be written in a bit shorter form, which produces the same output as before:

Point point = new Point(17, 42);
Console.WriteLine(VerboseFormatInfo.SingleLinedFormat.Format(point));

Beyond the Format method, VerboseFormatInfoBase class defines numerous methods and properties provided for convenience. For example, it exposes public property InstanceName which allows the caller to set the name of the object for which the string is being formatted. Similarly, caller can set InstanceDataType property to specify type of the object so that it is known in advance and formatter can correctly append the type name even when the object passed to the Format method is null, making it impossible to determine the type from the instance. Further on, a set of properties is exposed which define the format itself:

  • FirstContainedValuePrefix, LastContainedValueSuffix - These two properties are used to outline contained values when complex objects are formatted.

  • FieldDelimiter - Applied to delimit successive objects contained in the complex object.

  • IsMultiLinedFormat - Indicates whether FieldDelimiter contains new line characters or not. This is important for the formatter to know, because indentation is applied only in multi-lined formats.

  • IndentationString, RightMostIndentationString, LastIndentationString, LastRightMostIndentationString - Different strings appended to the string builder to indent content. These properties can be used to mimic various visual styles like simple indentation with tab characters, or tree-like indentation using pipe, dash and other special characters to represent lines and branches.

  • MaximumDepth - Specifies maximum number of references that will be walked into depth when formatting contained objects. This property can be used to indirectly limit total output produced by the formatter.

  • IndentationLevel - At every step specifies current depth of the object being formatted.

Other members of the VerboseFormatInfoBase class will not be listed in this text. For details please refer to the source code which is fully commented.

Main class derived from this VerboseFormatInfoBase class is VerboseFormatInfo, and that is actually the only class used by the callers, outside of this library. Its sole purpose is to invoke other classes derived from VerboseFormatInfoBase and to test results returned by their IsFormatApplicable members. For any given object, the first formatter which returns true is the one that will be used to effectively format the string. VerboseFormatInfo class tests specialized formatters in this particular order: 

  • ScalarFormatInfo - This class formats primitive types (integer, double, Boolean, etc.), DateTime, enumerations and strings.

  • DicionaryFormatInfo - Formats string which represents objects implementing IDictionary or generic IDictionary interface. These objects are specifically formatted by extracting their keys collection and then showing their contents as series of key-value pairs.

  • CompactMatrixFormatInfo - This formatter covers matrices (two-dimensional arrays) and two-dimensional jagged arrays of objects supported by ScalarFormatInfo. It formats the matrices in a familiar grid-like view.

  • CompactArrayFormatInfo - Covers one-dimensional arrays of objects to which ScalarFormatInfo is applicable. This formatter produces output with nicely aligned values contained in the array.

  • ArrayFormatInfo - Covers all other array types including multi-dimensional arrays.

  • EnumerableFormatInfo - Applied to objects that implement IEnumerable or generic IEnumerable interface. Strings are formatted to represent these objects as a series of values similar to one-dimensional array produced by ArrayFormatInfo or CompactArrayFormatInfo.

  • GeneralFormatInfo - Applied to all other objects. This formatter iterates through contained public and internal properties and through public fields exposed by any given object and recursively applies VerboseFormatInfo formatter to each of them.

VerboseFormatInfo.Format method not only that tries each of these specialized formatters against the given object, but it also tries to inline objects if possible. This is done by first invoking the single-lined implementation of the chosen formatter, with limited maximum length of the output, and only if that formatting fails the original chosen formatter is applied to the object.

All additional classes derived from VerboseFormatInfoBase are declared internal, hence not visible to the caller. The only class that should be used by the callers is VerboseFormatInfo.

This class also provides several static properties which can be used to quickly instantiate formatter with desired format-related property settings. These properties are:

  • SingleLinedFormat - As name implies, provides single-lined output for any object. This formatter has no limitation in length of the formatted string, or otherwise it could be put at risk of failing when formatting some specific object.

  • MultiLinedFormat - Creates indented representation of the object's content, where indentation is performed by multiple white space characters. This format is convenient for logging and for similar purposes where monospaced fonts are used to render strings.

  • TabbedMultiLinedFormat - Similar to MultiLinedFormat, only indents content using horizontal tab characters. This format is convenient when it is not certain that monospaced fonts will be in use. However, using tab characters carries specific risk because tab stops distribution might not be even in all cases and positions of particular tab stops might not be known in advance. All these circumstances might distort the output and affect it in a negative way.

  • TreeMultiLinedFormat - This format mimics the tree-like visual structure. It is convenient to format complex objects in a setting where monospaced fonts are used.

  • SimpleFormat - This is version of SingleLinedFormat which has MaximumDepth property set to one. This property can be used to show limited contents of the object by presenting only properties and public fields directly exposed by the object itself, rather than walking recursively into its contained objects to list their internal values too.

In the following text we will demonstrate the power of VerboseFormatInfo class on numerous examples. We will start with primitive objects and then progressively advance to very complex objects.