.NET provides a whole slew of utilities for serializing objects into an XML form. But as I wrote in my previous post, .NET Compact Framework has serious problems with this serialization. The good news is that you can leverage all of the existing Attributes and tricks that you think should work (if it weren't so buggy) and use them in your own serialization scheme.
Get Started
For example I want to know if I should skip a given member? There are a number of different things I can check. Is a Reference type null? Is there and XmlIgnore attribute? Is there a PropertyNameSpecified value set to false? All of those questions can easily be answered using reflection.
/// <summary>
/// Should the current property be skipped based on rules
/// such as the existence of a propertySpecified value set to false?
/// </summary>
/// <param name="member">The MemberInfo to check</param>
/// <param name="o">The object that contained this member</param>
/// <returns>true if this member should be skipped</returns>
public bool SkipMember(MemberInfo member, object o)
{
object val = null;
if (member.MemberType == MemberTypes.Field)
{
val = ((FieldInfo)member).GetValue(o);
}
else if (member.MemberType == MemberTypes.Property)
{
val = ((PropertyInfo)member).GetValue(o, null);
}
if (null == val)
return true;
string propertyToTest = member.Name + "Specified";
PropertyInfo specifiedProperty = o.GetType().GetProperty(propertyToTest);
if ((null != specifiedProperty && !(bool)specifiedProperty.GetValue(o, null)))
return true;
FieldInfo specifiedField = o.GetType().GetField(propertyToTest, FIELD_BINDING_FLAGS);
if ((null != specifiedField && !(bool)specifiedField.GetValue(o)))
return true;
return member.IsDefined(typeof(XmlIgnoreAttribute), false);
}
I can use a similar "fall-through" strategy to determine the name of the element to write using the XmlElement attribute for example. Now that I know I can answer some basic questions about an Object using the built-in mechanisms that .NET uses for serialization I can get down to serious serialization.
We're all Object-Oriented programmers these days right? Right!? So to start I decided that the best way to handle this problem was to decompose it into a bunch of simpler problems.
ITagWriter
There are two things that we can write in XML. Either an XML Element or an XML Attribute. So, I created an interfaceITagWriter
with two concrete implementations to correspond to these two XML types: AttributeTagWriter
and ElementTagWriter
. These classes allow me to write the structure of the XML Document.
/// <summary>
/// Interface to implement to write different Xml tags
/// Either Elements or Attributes.
/// </summary>
internal interface ITagWriter
{
/// <summary>
/// Write the opening Xml tag with the given name
/// </summary>
/// <param name="doc">The XML Document to write the tage to.</param>
/// <param name="tagName">The name of the tag</param>
void WriteStart(XmlWriter doc, string tagName);
/// <summary>
/// Write the appropriate end tag
/// </summary>
/// <param name="doc">The XML Document to write the tage to.</param>
void WriteEnd(XmlWriter doc);
}
IValueWriter
With the ability to write the structure, I then need to be able to write out the values of the various objects and their properties. Just like with theITagWriter
interface, I decided to create an IValueWriter
for the various kinds of values that I would need to write. The types I came up with were ObjectWriter
, CollectionValueWriter
, EnumValueWriter
, SimpleValueWriter
, and XmlElementValueWriter
.
/// <summary>
/// Interface to implement to write different kinds of values.
/// </summary>
internal interface IValueWriter
{
/// <summary>
/// Write the Entry value to the XmlDocument
/// </summary>
/// <param name="doc">The XML Document to write the tage to.</param>
/// <param name="entry">The meta-information and value to write.</param>
void Write(XmlWriter doc, CustomSerializationEntry entry);
}
You'll notice the CustomSerializationEntry
class
is the parameter for the IValueWriter.Write()
method. This class contains all of the metadata and the
value about the various properties of an Object. This alows us an easy way
to ask questions about a given property. Is it a Collection? Is it an Enum?
Is there a sort order? Basically the idea is to encapsulate all of the
things that are interesting from a serialization point of view.
To help manage the interaction I also created a basic TypeLookup
class. The job of this class is to
determine what type of ITagWriter
and IValueWriter
to use for a
given CustomSerializationEntry
instance. This allows us to centralize that decision making in a
single class. The centralized knowledge keeps the individual writer
implementations much simpler. They just need to ask for the correct
writer and then call the methods defined in the interface. They don't
need to care what type they are writing. All hail power of
encapsulation and abstraction!
Start Serializing
I bootstrap the serialization by creating an ObjectWriter to handle the outermost object. From there, the ObjectWriter takes over, constructing CustomSerializationEntry objects for each of the serialized object's properties. The type of the property determines the type of IValueWriter that is used to write the property value.
/// <summary>
/// Serialize an object using the given
/// <paramref name="xmlRoot">xmlRoot</paramref> as the root element name.
/// </summary>
/// <param name="o"></param>
/// <param name="xmlRoot"></param>
/// <returns></returns>
public string Serialize(object o, string xmlRoot)
{
StringBuilder sb = new StringBuilder();
using (XmlTextWriter writer = new XmlTextWriter(new StringWriter(sb)))
{
writer.Formatting = Formatting.Indented;
XmlWriter xmlDoc = XmlWriter.Create(writer);
WriteRootElement(xmlDoc, o, xmlRoot);
}
return sb.ToString();
}
private static void WriteRootElement(XmlWriter doc, object o, string rootElement)
{
doc.WriteStartDocument();
ObjectWriter writer = new ObjectWriter(new TypeLookup());
writer.Write(doc, o, rootElement);
doc.WriteEndDocument();
}
The ObjectWriter itself creates a CustomSerializationEntry for all the
properties that should be written. It then loops over the properties. Notice
how it uses the TypeLookup (lookup) to ask for the proper value writer for
each of the properties.
// ...
public void Write(XmlWriter doc, object o, string elementName)
{
doc.WriteStartElement(elementName);
IEnumerable<CustomSerializationEntry> entries = GetMemberInfo(o);
foreach (CustomSerializationEntry currentEntry in entries)
{
lookup.GetValueWriter(currentEntry).Write(doc, currentEntry);
}
doc.WriteEndElement();
}
// ...