Introduction
I needed a simple serialization / deserialization mechanism to save and load state information for objects whose type (the class) is constructed and compiled at runtime (see my article on Declaratively Populating A PropertyGrid). Neither XmlSerializer
nor BinaryFormatter
classes handle this scenario. So, I wrote a lightweight assembly to handle my specific needs.
Serialization
The serializer consists of three steps:
- Initialize using the
Start()
method
- Serialize one or more objects to XML
- Finish the process with the
Finish()
method
Initialization
This is very straight forward. A MemoryStream
and XmlTextWriter
are constructed and the initial XML document is setup:
public void Start()
{
ms=new MemoryStream();
xtw = new XmlTextWriter(ms, Encoding.UTF8);
xtw.Formatting=Formatting.Indented;
xtw.Namespaces=false;
xtw.WriteStartDocument();
xtw.WriteComment("Auto-Serialized");
xtw.WriteStartElement("Objects");
}
The Serializer
The serializer does a few important things and has a few limitations:
- it serializes only public properties and enumerations that are writeable
- it ignores arrays
- it looks for a
[DefaultValue]
attribute decorating the property, and if it exists and the property value equals the default value, the property is not serialized
- it uses a type converter to convert the property value to a string, rather than the
ToString()
method. This allows deserialization to work on Font
, Color
, and other classes that know how to serialize to a string
- it serializes only properties with the
IsSerializable
flag set in the property type
- it does not walk the object graph to serialize child classes
public void Serialize(object obj)
{
Trace.Assert(xtw != null, "Must call Serializer.Start() first.");
Trace.Assert(obj != null, "Cannot serialize a null object.");
Type t=obj.GetType();
xtw.WriteStartElement(t.Name);
foreach(PropertyInfo pi in t.GetProperties())
{
Type propertyType=pi.PropertyType;
if ( (propertyType.IsSerializable) && (!propertyType.IsArray) &&
(pi.CanWrite) && ( (propertyType.IsPublic) || (propertyType.IsEnum) ) )
{
object val=pi.GetValue(obj, null);
if (val != null)
{
bool isDefaultValue=false;
foreach(object attr in pi.GetCustomAttributes(false))
{
if (attr is DefaultValueAttribute)
{
DefaultValueAttribute dva=(DefaultValueAttribute)attr;
isDefaultValue=val.Equals(dva.Value);
}
}
if (!isDefaultValue)
{
TypeConverter tc=TypeDescriptor.GetConverter(propertyType);
if (tc.CanConvertTo(typeof(string)))
{
val=tc.ConvertTo(val, typeof(string));
xtw.WriteAttributeString(pi.Name, val.ToString());
}
else
{
Trace.WriteLine("Cannot convert "+pi.Name+" to a string value.");
}
}
}
else
{
}
}
}
xtw.WriteEndElement();
}
Finisher
The Finish()
method cleans up the text writer and returns the XML:
public string Finish()
{
Trace.Assert(xtw != null, "Must call Serializer.Start() first.");
xtw.WriteEndElement();
xtw.Flush();
xtw.Close();
Encoding e8=new UTF8Encoding();
xml=e8.GetString(ms.ToArray(), 1, ms.ToArray().Length-1);
return xml;
}
Deserialization
The deserializer expects that the instance has already been constructed. This is a very helpful shortcut to take, because constructing an object at runtime often requires a fully qualified assembly name, namespace, and other information. Furthermore, the type information for my runtime constructed classes is actually not available--only the instance is. For my particular requirement, this is not an issue.
Also, the deserializer will inspect each property for a default value and restore that value to the specified object unless it is being overridden in the XML.
Finally, when deserializing multiple objects, you must know the exact sequence that was used to serialize the objects, as an index pointing to the serialized object's element is passed in to the deserializer. Again, for my purposes, this restriction is not an issue.
public void Deserialize(object obj, int idx)
{
Trace.Assert(doc != null, "Must call Deserializer.Start() first.");
Trace.Assert(doc.ChildNodes.Count==3, "Incorrect xml format.");
Trace.Assert(idx < doc.ChildNodes[2].ChildNodes.Count,
"No element for the specified index.");
Trace.Assert(obj != null, "Cannot deserialize to a null object");
XmlNode node=doc.ChildNodes[2].ChildNodes[idx];
Type t=obj.GetType();
Trace.Assert(t.Name==node.Name, "Object name does not match element tag.");
foreach(PropertyInfo pi in t.GetProperties())
{
Type propertyType=pi.PropertyType;
foreach(object attr in pi.GetCustomAttributes(false))
{
if (attr is DefaultValueAttribute)
{
DefaultValueAttribute dva=(DefaultValueAttribute)attr;
if (node.Attributes[pi.Name] == null)
{
pi.SetValue(obj, dva.Value, null);
}
}
}
}
foreach(XmlAttribute attr in node.Attributes)
{
string pname=attr.Name;
string pvalue=attr.Value;
PropertyInfo pi=t.GetProperty(pname);
if (pi != null)
{
TypeConverter tc=TypeDescriptor.GetConverter(pi.PropertyType);
if (tc.CanConvertFrom(typeof(string)))
{
try
{
object val=tc.ConvertFrom(pvalue);
pi.SetValue(obj, val, null);
}
catch(Exception e)
{
Trace.WriteLine("Setting "+pname+" failed:\r\n"+e.Message);
}
}
}
}
}
Usage
Usage is very simple. Let's say we want to serialize a simple class (in this example, one that is constructed at compile time):
public class TestClass
{
protected string firstName;
protected string lastName;
[DefaultValue("Marc")]
public string FirstName
{
get {return firstName;}
set {firstName=value;}
}
[DefaultValue("Clifton")]
public string LastName
{
get {return lastName;}
set {lastName=value;}
}
public TestClass()
{
firstName="Marc";
lastName="Clifton";
}
}
Serializing an instance of this class would look like this:
Serializer s=new Serializer();
s.Start();
TestClass tc=new TestClass();
tc.FirstName="Joe";
tc.LastName="Smith";
s.Serialize(tc);
string text=s.Finish();
Resulting in XML that looks like this:
="1.0" ="utf-8"
-->
<Objects>
<TestClass FirstName="Joe" LastName="Smith" />
</Objects>
Deserialization of this object is done as:
Deserializer d=new Deserializer();
d.Start(text);
TestClass tc=new TestClass();
d.Deserialize(tc, 0);
After which, the properties of the class are set to "Joe" and "Smith".
Revisions
11/30/04 - Added support for property types that implement IList.
Conclusion
The code presented above meets a very specific requirement that I have. Even if you don't have this requirement, hopefully you'll gain something from the techniques demonstrated, especially the use of the type converter to convert to and from a string. This is an important "trick" to ensure that the serialized string is in a format that the deserializer can handle and avoids writing special case code for Font
, Color
, and other objects.