DekGenius.com
[ Team LiB ] Previous Section Next Section

Recipe 3.2 Allowing a Type to Represent Itself as a String

Problem

Your class or structure needs to control how its information is displayed when its ToString method is called. For example, when creating a new data type, such as a Line class, you might want to allow objects of this type to be able to display themselves in a textual format. In the case of a Line object, it would display itself as "(x1, y1)(x2, y2)".

Solution

Override and/or implement the IFormattable.ToString method to display numeric information, such as for a Line structure:

using System;
using System.Text;
using System.Text.RegularExpressions;

public struct Line : IFormattable
{
    public Line(int startX, int startY, int endX, int endY)
    {
        x1 = startX;
        x2 = endX;
        y1 = startY;
        y2 = endY;
    }

    public int x1;
    public int y1;
    public int x2;
    public int y2;

    public double GetDirectionInRadians( )
    {
        int xSide = x2 - x1;
        int ySide = y2 - y1;

        if (xSide == 0)    // Prevent divide-by-zero
            return (0);
        else
            return (Math.Atan (ySide / xSide));
    }

    public double GetMagnitude( )
    {
        int xSide = x2 - x1;
        int ySide = y2 - y1;
        return (Math.Sqrt( Math.Sqrt((xSide * xSide) + (ySide * ySide)) ));
    }

    // This overrides the Object.ToString method
    //   This override is not required for this recipe 
    //   and is included for completeness
    public override string ToString( ) 
    {
        return (String.Format("({0},{1}) ({2},{3})", x1, y1, x2, y2));
    }

    public string ToString(string format) 
    {
        return (this.ToString(format, null));
    }

    public string ToString(IFormatProvider formatProvider) 
    {
        return (this.ToString(null, formatProvider));
    }

    public string ToString(string format, IFormatProvider formatProvider) 
    {
        StringBuilder compositeStr = new StringBuilder("");

        if ((format != null) && (format.ToUpper( ).Equals("V")))
        {
            double direction = this.GetDirectionInRadians( );
            double magnitude = this.GetMagnitude( );

            string retStringD = direction.ToString("G5", formatProvider);
            string retStringM = magnitude.ToString("G5", formatProvider);

            compositeStr.Append("magnitude = ").Append(retStringM).Append
                               ("\tDirection = ").Append(retStringD);
        }
        else
        {
            string retStringX1 = this.x1.ToString(format, formatProvider);
            string retStringY1 = this.y1.ToString(format, formatProvider);
            string retStringX2 = this.x2.ToString(format, formatProvider);
            string retStringY2 = this.y2.ToString(format, formatProvider);

            compositeStr.Append("(").Append(retStringX1).Append(",").Append
                               (retStringY1).Append(")(").Append(retStringX2).Append
                               (",").Append(retStringY2).Append(")");
        }

        return (compositeStr.ToString( ));
    }
}

Discussion

The ToString method provides a convenient way to display the current contents, or state, of a structure (this recipe works equally well for reference types). The solution section of this recipe shows the various implementations of ToString for both numeric and textual data. The Line class contains two points in space that form the endpoints of a line. This line data is then fed into the ToString methods for that class to produce formatted output.

The following code exercises the ToString methods of the Line class:

using System.Globalization;

public void TestLineToString( )
{
    Line V1 = new Line(0, 0, 40, 123);
    Line V2 = new Line(0, -2, 1, 11);
    Line V3 = new Line(0, 1, 0, 1);

    Console.WriteLine("\r\nTest Default ToString method");
    Console.WriteLine("V1 = " + V1);
    Console.WriteLine("V2 = " + V2);
    Console.WriteLine("V1.ToString( ) = {0:V}", V1.ToString( ));
    Console.WriteLine("V2.ToString( ) = {0:V}", V2.ToString( ));

    Console.WriteLine("\r\nTest overloaded ToString(format) method");
    Console.WriteLine("V1.ToString(\"D\") = {0:D}", V1);
    Console.WriteLine("V1.ToString(\"D5\") = {0:D5}", V1);
    Console.WriteLine("V2.ToString(\"F\") = {0:F}", V2);
    Console.WriteLine("V1.ToString(\"N\") = {0:N}", V1);
    Console.WriteLine("V2.ToString(\"n\") = {0:n}", V2);
    Console.WriteLine("V1.ToString(\"E\") = {0:E}", V1);
    Console.WriteLine("V2.ToString(\"X\") = {0:X}", V2);

    Console.WriteLine("\r\nTest overloaded ToString(formatProvider) method");
    NumberFormatInfo NullFormatter = null;
    NumberFormatInfo Formatter = new NumberFormatInfo( );
    Formatter.NegativeSign = "!";
    Formatter.PositiveSign = "+";
    Console.WriteLine("V2.ToString(Formatter) = " + V2.ToString(Formatter));
    Console.WriteLine("V2.ToString(Formatter) = " + V2.ToString(Formatter));
    Console.WriteLine("V2.ToString(null) = " + V2.ToString(NullFormatter));
    Console.WriteLine("V2.ToString(null) = " + V2.ToString(NullFormatter));
    Console.WriteLine("V2.ToString(new CultureInfo(\"fr-BE\")) = " 
            + V2.ToString(new CultureInfo("fr-BE")));  //French - Belgium
    Console.WriteLine("V2.ToString(new CultureInfo(\"fr-BE\")) = " 
            + V2.ToString(new CultureInfo("fr-BE")));  //French - Belgium

    Console.WriteLine
            ("\r\nTest overloaded ToString(format, formatProvider) method");
    Console.WriteLine("V2.ToString(\"D\", Formatter) = " + V2.ToString("D",
                       Formatter));
    Console.WriteLine("V2.ToString(\"F\", Formatter) = " + V2.ToString("F", 
                       Formatter));
    Console.WriteLine("V2.ToString(\"D\", null) = " + V2.ToString("D", null));
    Console.WriteLine("V2.ToString(\"F\", null) = " + V2.ToString("F", null));
    Console.WriteLine("V2.ToString(\"D\", new CultureInfo(\"fr-BE\")) = " 
            + V2.ToString("D", new CultureInfo("fr-BE")));  //French - Belgium
    Console.WriteLine("V2.ToString(\"F\", new CultureInfo(\"fr-BE\")) = " 
            + V2.ToString("F", new CultureInfo("fr-BE")));  //French - Belgium

    Console.WriteLine("\r\nTest overloaded ToString(\"V\", formatProvider) method");
    Console.WriteLine("V2.ToString(\"V\", Formatter) = " + V2.ToString("V", 
                       Formatter));
    Console.WriteLine("V2.ToString(\"V\", null) = " + V2.ToString("V", null));
}

This code displays the following results:

Test Default ToString method
V1 = (0,0) (40,123)
V2 = (0,-2) (1,11)
V1.ToString( ) = (0,0) (40,123)
V2.ToString( ) = (0,-2) (1,11)

Test overloaded ToString(format) method
V1.ToString("D") = (0,0)(40,123)
V1.ToString("D5") = (00000,00000)(00040,00123)
V2.ToString("F") = (0.00,-2.00)(1.00,11.00)
V1.ToString("N") = (0.00,0.00)(40.00,123.00)
V2.ToString("n") = (0.00,-2.00)(1.00,11.00)
V1.ToString("E") = (0.000000E+000,0.000000E+000)(4.000000E+001,1.230000E+002)
V2.ToString("X") = (0,FFFFFFFE)(1,B)

Test overloaded ToString(formatProvider) method
V2.ToString(Formatter) = (0,!2)(1,11)
V2.ToString(Formatter) = (0,!2)(1,11)
V2.ToString(null) = (0,-2)(1,11)
V2.ToString(null) = (0,-2)(1,11)
V2.ToString(new CultureInfo("fr-BE")) = (0,-2)(1,11)
V2.ToString(new CultureInfo("fr-BE")) = (0,-2)(1,11)

Test overloaded ToString(format, formatProvider)        method
V2.ToString("D", Formatter) = (0,!2)(1,11)
V2.ToString("F", Formatter) = (0.00,!2.00)(1.00,11.00)
V2.ToString("D", null) = (0,-2)(1,11)
V2.ToString("F", null) = (0.00,-2.00)(1.00,11.00)
V2.ToString("D", new CultureInfo("fr-BE")) = (0,-2)(1,11)
V2.ToString("F", new CultureInfo("fr-BE")) = (0,00,-2,00)(1,00,11,00)

Test overloaded ToString("V", formatProvider) method
V2.ToString("V", Formatter) = magnitude = 3.6109        direction = 1.494
V2.ToString("V", null) = magnitude = 3.6109     direction = 1.494

This method prints out the two x and y coordinates that make up the start and end points of a line for the Line class. An example output of the Line.ToString( ) method is:

(0,0) (40,123)

This output could also be displayed as a vector that starts at the origin of the Cartesian plane and points straight up along the positive y-axis. Another choice for this would be to print out the magnitude and direction of this line. This result is demonstrated in the overloaded ToString method that accepts both a format string and an IFormatProvider.

The next overloaded ToString method takes a single argument, format, which is a String containing the formatting information of the type. This method calls the last overloaded ToString method and passes the format information as the first parameter and a null as the second parameter. The following ToString method operates similarly to the previous ToString method, except that it accepts an IFormatProvider data type as its only parameter. The format parameter of the last ToString method is set to null when called by this method.

The final ToString method is where all the real work takes place. This method accepts two parameters, a String (format) containing formatting information and an IFormatProvider (formatProvider) containing even more specific formatting information. The format string makes use of predefined formats such as "D", "d", "F", "f", "G", "g", "X", and "x", to name a few. (See Recipe 2.16 for more information on the formatting character codes.) These formats specify whether the information will be displayed as decimal ("D" or "d"), general ("G" or "g"), hexadecimal ("X" or "x"), or one of the other types. As a note, calling ToString with no parameters always sets the format type to general. In addition, this method also takes a special format character "V" or "v". This character formatting code is not one of the predefined formatting codes; instead, it is one that we added to provide special handling of a Line object's output in vector format. This code allows the Line type to be displayed as a magnitude and a direction:

magnitude = 13.038        direction = 1.494

The second parameter accepts any data type that implements the IFormatProvider interface. There are three types in the FCL that implement this interface: CultureInfo, DateTimeFormatInfo, or NumberFormatInfo. The CultureInfo class contains formatting information specific to the various supported cultures that exist around the world. The DateTimeFormatInfo class contains formatting information specific to date and time values; similarly, the NumberFormatInfo class contains formatting information specific to numbers.

This ToString method sets up a variable, compositeStr, which will contain the final formatted value of the Line type. Next, the format parameter is checked for null. Remember, the previous ToString method that accepts the IFormatProvider parameter will call this form of the ToString method and pass in a format value of null. So we must be able to handle a null value gracefully at this point. If the format parameter passed in to the Line type is not null and is equal to the character "V", we are able to provide a string to display this line as a magnitude and a direction. The direction and magnitude values are obtained for this object and are displayed in a General format with five significant digits of precision. If, on the other hand, any other type of formatting character code was passed in—including null—each of the individual coordinates are formatted using the ToString method of the Int32 structure. These coordinates are concatenated into a string and returned to the caller to be displayed.

The method:

public string ToString(string format, IFormatProvider formatProvider)

must be implemented, since the structure implements the IFormattable interface. The IFormattable interface provides a consistent interface for this ToString method:

public interface IFormattable
{
    string ToString(string format, IFormatProvider formatProvider);
}

For the Line structure, the IFormattable.ToString method passes its parameters to the Int32 structure's ToString method with the same method signature, which provides a more uniform formatting capability for Line values.

Using the IFormattable interface forces you to implement the IFormattable.ToString method to more effectively display your type's value(s). However, you do not have to implement it, as you can see for yourself by removing this interface from the Line structure's declaration. In fact, for performance's sake, it is best to not implement this interface on structures, due to the cost of boxing the structure. Implementing this interface on a class does not incur a performance penalty.


See Also

See Recipe 2.16; see the "IFormatProvider Interface" topic in the MSDN documentation.

    [ Team LiB ] Previous Section Next Section