
Smart Serializer Implementation
One of the requirements of the serializer design is that when a new serialization format is needed, all data types must be updated with a metadata attribute to support that format. However, through the use of serialization providers coupled with serializers that use generic object metadata, this requirement can be met. This section describes the preferred way to design a serializer for a particular format so that the need for many custom serializers is minimized.
The following schema defines a hypothetical XML format to which an object graph will be persisted.
<TypeName>
<PropertyName>
ValueString
</PropertyName>
</TypeName>
This format is serialized using an invented class called DemoXmlSerializer.
public abstract class DemoXmlSerializer
{
public abstract string Serialize(
IDesignerSerializationManager m,
object graph);
}
It is important to understand that DemoXmlSerializer is a modular class that builds up a string from pieces. For example, the DemoXmlSerializer for the Int32 data type would return the string “23” when passed an integer of value 23.
Serialization Providers
The previous schema example makes it clear that there are two fundamental types to be handled:
It would be difficult to adorn every class with a custom serializer that can convert that class to text or XML tags. Serialization providers solve this by providing a callback mechanism in which an object is given the opportunity to provide a serializer for a given type. For this example, the available set of types is restricted by the following conditions:
If the type can be converted to a string by using the IConvertible interface, the StringXmlSerializer will be used.
If the type cannot be converted to a string, but is public and has an empty constructor, the ObjectXmlSerializer will be used.
If neither of these is true, the serialization provider will return null, indicating that there is no serializer for the object.
The following code example shows how the calling serializer resolves what happens with this error occurs.
internal class XmlSerializationProvider : IDesignerSerializationProvider
{
object GetSerializer(
IDesignerSerializationManager manager,
object currentSerializer,
Type objectType,
Type serializerType)
{
// Null values will be given a null type by this serializer.
// This test handles this case.
if (objectType == null)
{
return StringXmlSerializer.Instance;
}
if (typeof(IConvertible).IsSubclassOf(objectType))
{
return StringXmlSerializer.Instance;
}
if (objectType.GetConstructor(new object[]) != null)
{
return ObjectXmlSerializer.Instance;
}
return null;
}
}
Once a serialization provider is defined, it must be put to use. A serialization manager can be given a serialization provider through the AddSerializationProvider method, but this requires that this call be made to the serialization manager. A serialization provider can be automatically added to a serialization manager by adding a DefaultSerializationProviderAttribute to the serializer. This attribute requires that the serialization provider have a public, empty constructor. The following code example shows the necessary change to DemoXmlSerializer.
[DefaultSerializationProvider(typeof(XmlSerializationProvider))]
public abstract class DemoXmlSerializer
{
}
Now, whenever a serialization manager is asked for any type of DemoXmlSerializer, the default serialization provider will be added to the serialization manager if it has not happened already.
Serializers
The sample DemoXmlSerializer class has two concrete serializer classes named StringXmlSerializer and ObjectXmlSerializer. The following code example shows the implementation of StringXmlSerializer.
internal class StringXmlSerializer : DemoXmlSerializer
{
internal StringXmlSerializer Instance = new StringXmlSerializer();
public override string Serialize(
IDesignerSerializationManager m,
object graph)
{
if (graph == null) return string.Empty;
IConvertible c = graph as IConvertible;
if (c == null)
{
// Rather than throwing exceptions, add a list of errors
// to the serialization manager.
m.ReportError("Object is not IConvertible");
return null;
}
return c.ToString(CultureInfo.InvariantCulture);
}
}
The ObjectXmlSerializer implementation is more involved, because it is necessary to enumerate the public properties of the object. The following code example shows the implementation of ObjectXmlSerializer.
internal class ObjectXmlSerializer : DemoXmlSerializer
{
internal ObjectXmlSerializer Instance = new ObjectXmlSerializer();
public override string Serialize(
IDesignerSerializationManager m,
object graph)
{
StringBuilder xml = new StringBuilder();
xml.Append(“<”);
xml.Append(graph.GetType().FullName);
xml.Append(“>”);
// Now, walk all the properties of the object.
PropertyDescriptorCollection properties;
Property p;
properties = TypeDescriptor.GetProperties(graph);
foreach(p in properties)
{
if (!p.ShouldSerializeValue(graph))
{
continue;
}
object value = p.GetValue(graph);
Type valueType = null;
if (value != null) valueType = value.GetType();
// Get the serializer for this property
DemoXmlSerializer s = m.GetSerializer(
valueType,
typeof(DemoXmlSerializer)) as DemoXmlSerializer;
if (s == null)
{
// Because there is no serializer,
// this property must be passed over.
// Tell the serialization manager
// of the error.
m.ReportError(string.Format(
“Property {0} does not support XML serialization”,
p.Name));
continue;
}
// You have a valid property to write.
xml.Append(“<”);
xml.Append(p.Name);
xml.Append(“>”);
xml.Append(s.Serialize(m, value);
xml.Append(“</”);
xml.Append(p.Name);
xml.Append(“>”);
}
xml.Append(“</”);
xml.Append(graph.GetType().FullName);
xml.Append(“>”);
return xml.ToString();
}
}
ObjectXmlSerializer invokes other serializers for each property value. This has two advantages. First, it allows ObjectXmlSerializer to be very simple. Second, it provides an extensibility point for third-party types. If ObjectXmlSerializer is presented with a type that cannot be written out by either of these serializers, a custom serializer can be provided for that type.
Usage
To use these new serializers, an instance of IDesignerSerializationManager must be created. From this instance, you ask for a serializer and then ask the serializer to serialize your objects. For the following code example, Rectangle will be used for an object to serialize, because this type has an empty constructor and has four properties that support IConvertible. Instead of implementing IDesignerSerializationManager yourself, you can use the implementation provided by DesignerSerializationManager.
Rectangle r = new Rectangle(5, 10, 15, 20);
DesignerSerializationManager m = new DesignerSerializationManager();
DemoXmlSerializer x = (DemoXmlSerializer)m.GetSerializer(
r.GetType(), typeof(DemoXmlSerializer);
string xml = x.Serialize(m, r);
This would create the following XML.
<System.Drawing.Rectangle>
<X>
5
</X>
<Y>
10
</Y>
<Width>
15
</Width>
<Height>
15
</Height>
</System.Drawing.Rectangle>
Note: |
|---|
The XML is not indented. This could be easily accomplished through the
Context property on IDesignerSerializationManager. Each level of serializer could add an object to the context stack that contains the current indent level, and each serializer could search for that object in the stack and use it to provide an indent.
|