This is the third 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.
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.