More Extensions - The Generic ToString() Method for classes and structs
NOTE: The following solution has been improved a lot in this blog.
Don't you hate repetitive tasks?
I sure do.
Writing ToString() methods for classes is one of them.
We will implement the same kind of ToString() methods if we need them to give us information about the object state as is very usefull when running UnitTests or logging exceptions.
Here is a typical example:
class ControlData
{
public Point location;
public Size size;
public ControlData(Point location, Size size)
{
this.location = location;
this.size = size;
}
public override string ToString()
{
return string.Format("[ControlData: {0} location={1} size={2}]", this.location, this.size);
}
}
Granted, codegenerators will do this job for you.
But what if you want to rename some fields or the class itself.
In that case you have to generate the ToString() method all over again, since the rename refactoring will not change anything inside of strings.
In our case, were we to change 'location' to 'firstLocation' its name in here:
"[ControlData: {0} location={1} size={2}]"
will not change with it.
Turns out, that there is a way to automate a ToString() execution alltogether. So without any further ado:
public static string FieldsToString<T>(this T fieldContainer) where T : IFieldsEnumerable
{
var sb = new StringBuilder();
int fieldCount = 0;
sb.AppendFormat("[{0}:", fieldContainer.GetType().Name);
foreach (var field in fieldContainer.GetFieldEnumerator())
{
sb.AppendFormat("\n {0}\t{1}\t{2} ",
fieldContainer.GetType().GetFields()[fieldCount].FieldType.Name,
fieldContainer.GetType().GetFields()[fieldCount].Name,
field.ToString());
fieldCount++;
}
sb.Append("]");
return sb.ToString();
}
Now lets look at the previous class again, except that it now implements the
IFieldsEnumerable
interface:
class ControlData : IFieldsEnumerable
{
public Point location;
public Size size;
public ControlData(Point location, Size size)
{
this.location = location;
this.size = size;
}
public override string ToString()
{
return this.FieldsToString();
}
public IEnumerable<object>GetFieldEnumerator()
{
yield return location;
yield return size;
}
}
Here is that interface:
Calling:
public interface IFieldsEnumerable
{
IEnumerable<object>GetFieldEnumerator();
}
new ControlData(new Point(12, 13), new Size(21, 30)).ToString();
produces this neat output:
[ControlData:Of course we could also have called
Point location {X=12,Y=13}
Size size {Width=21, Height=30} ]
FieldsToString()
directly as in:new ControlData(new Point(12, 13), new Size(21, 30)).FieldsToString();with the same result.
Important is that the order of the yields in the GetFieldEnumerator() method exactly matches the order in which the fields are declared int the class. Otherwise the values of the fields will not line up with their names.
This requirement is caused by the fact, that the fieldnames are obtained from the class via reflection.
Now, if we want to change any field names, we can go ahead and be sure that all necessary changes in order to produce the correct ToString() output will be done automatically as we are not dealing with hard coded strings in the this method anymore.
If we add another field xxx , we just add a yield return xxx to the GetFieldEnumrator() method and are done.
Just to be clear, add the FieldsToString() method to your extension class and include the IFieldsEnumerable interface in the same project.
Now you just need to implement this interface for the classes to which you want this extra functionality.
Labels: extension methods, tdd, testing