Introduction
The process of converting the data in an object to XML is called serialization. The reverse of serialization, loading XML into the object, is called deserialization. Shallow serialization converts the public properties and fields into XML while deep serialization converts both public and private members into XML. System.Xml.Serialization.XmlSerializer
is used to perform shallow serialization.
As much as I like using the XmlSerializer
, the limitations it has are enough to drive a man crazy. XmlSerializer
doesn't support most of the types defined in the System.Collections
namespace. I rarely write code that requires serialization without one or more of the collection types! Fed up with these limitations, I finally took the time to create a custom serializer called CustomXmlSerializer
. This article demonstrates how to use CustomXmlSerializer
to serialize/deserialize various IEnumerable
list types.
Background
If all you are interested in doing is serializing CollectionBase
abstract classes, then you may want to view the article, "Serialize custom collections of CollectionBase".
Capabilities
CustomXmlSerializer
is intended to support serialization/deserialization of any object.
CustomXmlSerializer
was specifically designed to support the following IEnumerable
array types:
System.Array
System.Collections.ArrayList
System.Collections.BitArray
System.Collections.CollectionBase
System.Collections.DictionaryBase
System.Collections.Hashtable
System.Collections.Queue
System.Collections.SortedList
System.Collections.Stack
There is limited support for the types defined in System.Collections.Generic
.
In order to support both serialization and deserialization for the System.Collections
namespace, CustomXmlSerializer
makes the following assumptions:
- The
Add
method and the Item
property will be exposed from abstract classes based on an ICollection
, IList
, ArrayList
, BitArray
, CollectionBase
, DictionaryBase
, HashTable
, or SortedList
object. - The
Enqueue
and Peek
methods will be exposed from abstract classes based on the Queue
object. - The
Push
and Peek
methods will be exposed from abstract classes based on the Stack
object.
Limitations
IDictionary
When the IncludeClassNameAttribute
is set to False
, CustomXmlSerializer
uses Reflection in an attempt to detect the variable type that will be added to the collection. A class that implements the IDictionary
interface always returns a DictionaryEntry
object which always has a value of type Object
.
In order for CustomXmlSerializer
to correctly deserialize the XML, it must know what data type to instantiate. The class name provides the instructions to instantiate the correct object. When using any collection that implements the IDictionary
interface, set the property IncludeClassNameAttribute
to True
during serializiation.
Queue
Like the IDictionary
classes, when serializing a Queue
object, you must set the IncludeClassNameAttribute
to True
.
Structures
CustomXmlSerializer
is unable to deserialize structures. Structures are boxed, and this makes them very difficult to deserialize. If deserialization is required, use a class instead of a structure.
Generics
CustomXmlSerializer
provides limited support for generics. CustomXmlSerializer
can serialize abstract generic classes, but not the base generic class. For example, System.Collections.Generic.Dictionary
can't be serialized as a variable. But, if an abstract class is created that inherits from System.Collections.Generic.Dictionary
, then serialization and deserialization work just fine.
From the perspective of serialization, it is best to use standard objects for data classes. The classes that need to be serialized should be well defined classes. Although generics give us the ability to create type-safe reusable classes, I wouldn't consider them well defined. The type of data a generic stores isn't official until run-time, and from the generic's perspective, each instantiation can be defined as a different type.
The use of generics makes more sense for helper classes where you need to support different types with the same code. I have come to this conclusion after having seen that there aren't any good ways to serialize generics referencing other generics. The use of generic data objects forces the developer to support serialization with the IXmlSerializable
interface. The best articles I have found so far discussing serializing generics is DevX's "Serializing, Consuming, and Binding Generics in .NET 2.0", and Microsoft's "An Introduction to C# Generics".
Before I get any hate mail on this topic, I am not saying that you shouldn't use generics. The concept of generics is a powerful new addition to the VS.NET developer's arsenal. I'm just saying don't go generic crazy just because you can. If you need to support serialization, you are better off avoiding generic classes.
CustomXmlSerializer Members
CustomXmlSerializer
serializes and deserializes objects into and from XML. Write operations serialize the object into various target mediums. Read operations deserialize the object from various source mediums.
Figure 1: CustomXmlSerializer Class Diagram
Table 1: CustomXmlSerializer Property Members
Property | Description |
---|
CDataStorage | Serialize all string values into XML CData tags. |
IncludeClassNameAttribute | Record the name of the class when serializing to ensure that the class can be deserialized. |
IgnoreWarnings | Ignore warnings, allowing deserialization to continue when an unsupported type is encountered. |
Method | Defines how to serialize/deserialize the class. Supports the following values: SerializationMethod.Shallow (default) or SerializationMethod.Deep . |
Table 2: CustomXmlSerializer Method Members
Method | Parameters | Description |
---|
ReadXML | Overloaded routine. | Deserializes the XML into the target object. |
ByVal reader As System.Xml.XmlReader, ByVal target As Object | Loads the XML from the specified reader into the specified target. |
ByVal node As System.Xml.XmlNode, ByVal target As Object | Loads the XML from the specified XmlNode into the specified target. |
ByVal document As System.Xml.XmlDocument, ByVal target As Object | Loads the XML from the specified document into the specified target. |
ByVal path As String, ByVal target As Object | Loads the XML from the specified file into the specified target. |
ByVal text As System.Text.StringBuilder, ByVal target As Object | Loads the XML from the specified StringBuilder into the specified target. |
WriteDocument | ByVal source As Object | Serializes the source object into an XmlDocument following "Shallow Copy" business logic. |
WriteFile | ByVal source As Object, ByVal path As String, Optional ByVal replaceFile As Boolean = False | Serializes the source object into a file following "Shallow Copy" business logic. |
WriteString | ByVal source As Object | Serializes the source object into a string following "Shallow Copy" business logic. |
WriteText | ByVal source As Object | Serializes the source object into a StringBuilder following "Shallow Copy" business logic. |
WriteXML | ByVal source As Object, ByVal writer As System.Xml.XmlWriter, Optional ByVal propertyName As String = Nothing | Serializes the source object into an XmlWriter following "Shallow Copy" business logic. |
Using CustomXmlSerializer
The following example demonstrates how to make serializable data objects for use with a Web Service using CustomXmlSerializer
.
Example Data Object Model
Several data objects were created to demonstrate how CustomXmlSerializer
works (see Figure 2). The example has been modeled to show how the serializer can handle several layers of object relationships.
Figure 2: Example Data Objects Class Diagram
The Buyer
object has to implement the System.XML.Serialization.IXmlSerializable
interface because some of the class types in the diagram can't be serialized by System.XML.Serialization.XmlSerializer
. Normally, a great deal of work would need to be performed to support the interface. But, using CustomXmlSerializer
, we can reduce hundreds of lines of complex code down to just four lines of code.
Listing 1: Supporting IXmlSerializable
<Serializable()> Public Class Buyer_
Implements System.Xml.Serialization.IXmlSerializable
#Region "Implements IXmlSerializable"
Public Function GetSchema() As System.Xml.Schema.XmlSchema _
Implements System.Xml.Serialization.IXmlSerializable.GetSchema
Return Nothing
End Function
Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) _
Implements System.Xml.Serialization.IXmlSerializable.ReadXml
Dim _XMLReader As New CustomXmlSerializer
_XMLReader.ReadXML(reader, Me)
End Sub
Public Sub WriteXml(ByVal writer As System.Xml.XmlWriter) _
Implements System.Xml.Serialization.IXmlSerializable.WriteXml
Dim _XMLWriter As New CustomXmlSerializer
_XMLWriter.WriteXML(Me, writer)
End Sub
#End Region
Loading the Object Model
Before the serialization capabilities can be demonstrated, a Buyer
class has to be created and loaded with default values.
Listing 2: Loading the Example Data Objects
Private Function CreateBuyer() As Buyer
Dim _Person As New Buyer
_Person.Identifier = 24
_Person.Name = "Joe Schmoe"
_Person.EMailAddress = "JoeSchmoe@FutureHomeOwners.com"
_Person.PhoneNumber = "666-777-8888"
Dim _Question As Profile.Question
_Question = New Profile.Question
_Question.Identifier = 1
_Question.Text = "Target Price Range"
_Question.Tip = "Select the price range that you" & _
" are able/willing to pay for a new home"
_Question.Type = QuestionTypes.SelectOne
_Question.Options = New System.Collections.ArrayList
_Question.Options.Add("$200,000 - $250,000")
_Question.Options.Add("$250,000 - $300,000")
_Question.Options.Add("$300,000 - $500,000")
_Question.Options.Add("$500,000 or More")
_Question.OtherOptions = New System.Collections.ArrayList
_Question.OtherOptions.Add("Test")
_Question.OtherOptions.Add(CType(1, Integer))
_Question.OtherOptions.Add(CType(1.1, Double))
ReDim _Question.TestArray(2)
_Question.TestArray(0) = "Test Array 1"
_Question.TestArray(1) = "Test Array 2"
_Person.Questionnaire.Questions.Add(_Question)
_Question = New Profile.Question
_Question.Identifier = 2
_Question.Text = "Building Style"
_Question.Tip = "Identify the styles that you like"
_Question.Type = QuestionTypes.SelectMany
_Question.Options = New System.Collections.ArrayList
_Question.Options.Add("Modern")
_Question.Options.Add("Gothic")
_Question.Options.Add("European")
_Question.Options.Add("Spanish")
_Question.Options.Add("Colonial")
_Person.Questionnaire.Questions.Add(_Question)
_Question = New Profile.Question
_Question.Identifier = 3
_Question.Text = "Special Requests"
_Question.Tip = "Document any additional needs/wants that we should consider"
_Question.Type = QuestionTypes.Text
_Person.Questionnaire.Questions.Add(_Question)
Return _Person
End Function
Serializing the Object Model into XML
The loaded Buyer
object is then serialized into a StringBuilder
and the results are displayed. You have the option of manually serializing the class as shown in Listing 3, or you can use the helper method, WriteText
, as shown in Listing 4.
Listing 3: Serializing the Data Objects Manually
Dim _Person As Buyer = CreateBuyer()
Dim _xmlText As New System.Text.StringBuilder
Dim _TextStreamWriter As New System.IO.StringWriter(_xmlText)
Dim _xmlWriter As New System.Xml.XmlTextWriter(_TextStreamWriter)
_Person.WriteXml(_xmlWriter)
MsgBox(_xmlText.ToString, MsgBoxStyle.OKOnly, "Serialization Completed")
Listing 4: Serializing the Data Objects Using the Helper Routine
Dim _Person As Buyer = CreateBuyer()
Dim _XMLWriter As New CustomXmlSerializer
Dim _xmlText As New System.Text.StringBuilder = _XmlWriter.WriteText(_Person)
MsgBox(_xmlText.ToString, MsgBoxStyle.OKOnly, "Serialization Completed")
When you run the example, you will notice that the carriage returns and spaces are missing. CustomXmlSerializer
excludes these characters to conserve space. The carriage returns and spaces were manually added to the example output below to make it easier to read.
In the generated output, you can see that all classes and properties become elements, with their values inside the element tags. Attributes are used by CustomXmlSerializer
to identify how to deserialize the data back into the target objects. One of the attributes used by the deserialization logic is the className
attribute.
The className
is used to help the deserializer understand what object to instantiate. The className
attribute is only required when you have different value types stored in the same list. The OtherOptions
tag shows an example requiring the className
because of the various child class types.
If the className
attribute is excluded, the deserializer attempts to instantiate the child classes based on the element tag name of the first child element. Using this approach, the deserializer expects all children to be of the same type. The Options
tag represents an example that would work without class names in the child elements.
The TestArray
element shows how a string variable array (System.Array
type) was serialized. This demonstrates that CustomXmlSerializer
can serialize and deserialize array values.
Listing 5: Example Output from Serialized Data Objects
<Buyer className="Example.Buyer">
<Identifier>24</Identifier>
<Name>Joe Schmoe</Name>
<EMailAddress>JoeSchmoe@FutureHomeOwners.com</EMailAddress>
<PhoneNumber>666-777-8888</PhoneNumber>
<Questionnaire className="Example.Profile.Questionnaire">
<Questions className="Example.Profile.Questions">
<Question className="Example.Profile.Question">
<OtherOptions className="System.Collections.ArrayList">
<String className="System.String">Test</String>
<Int32 className="System.Int32">1</Int32>
<Double className="System.Double">1.1</Double>
</OtherOptions>
<TestArray size="3" className="System.Array" type="System.String">
<System.Array.Item point="0">
<String className="System.String">Test Array 1</String>
</System.Array.Item>
<System.Array.Item point="1">
<String className="System.String">Test Array 2</String>
</System.Array.Item>
</TestArray>
<Identifier>1</Identifier>
<Text>Target Price Range</Text>
<Tip>Select the price range that you are able/willing to pay for a new home</Tip>
<Type>SelectOne</Type>
<Options className="System.Collections.ArrayList">
<String className="System.String">$200,000 - $250,000</String>
<String className="System.String">$250,000 - $300,000</String>
<String className="System.String">$300,000 - $500,000</String>
<String className="System.String">$500,000 or More</String>
</Options>
<TestArray2 className="System.Collections.ArrayList">
<String className="System.String">Test</String>
<Int32 className="System.Int32">1</Int32>
<Double className="System.Double">1.1</Double>
</TestArray2>
</Question>
<Question className="Example.Profile.Question">
<OtherOptions />
<TestArray size="1" className="System.Array" type="System.String">
<System.Array.Item point="0">
<String className="System.String" />
</System.Array.Item>
</TestArray>
<Identifier>2</Identifier>
<Text>Building Style</Text>
<Tip>Identify the styles that you like</Tip>
<Type>SelectMany</Type>
<Options className="System.Collections.ArrayList">
<String className="System.String">Modern</String>
<String className="System.String">Gothic</String>
<String className="System.String">European</String>
<String className="System.String">Spanish</String>
<String className="System.String">Colonial</String>
</Options>
<TestArray2 />
</Question>
<Question className="Example.Profile.Question">
<OtherOptions />
<TestArray size="1" className="System.Array" type="System.String">
<System.Array.Item point="0">
<String className="System.String" />
</System.Array.Item>
</TestArray>
<Identifier>3</Identifier>
<Text>Special Requests</Text>
<Tip>Document any additional needs/wants that we should consider</Tip>
<Type>Text</Type>
<Options />
<TestArray2 />
</Question>
</Questions>
<Responses className="Example.Profile.Responses" />
<Identifier>0</Identifier>
<DateCreated>1/1/0001 12:00:00 AM</DateCreated>
<DateModified>1/1/0001 12:00:00 AM</DateModified>
<State>Created</State>
<ChangeCount>0</ChangeCount>
</Questionnaire>
<PreferredPlans />
<SelectedPlan />
</Buyer>
Deserializing the XML into the Object Model
To deserialize the XML back into the Buyer
object, we create a new Buyer
. Next, the XML string value is read back into the buyer. You can manually deserialize the object as shown in Listing 6, or use the helper routine, ReadXml
, as shown in Listing 7.
Listing 6: Deserializing the XML back into the Example Data Objects Manually
_Person = New Buyer
Dim _textStreamReader As New System.IO.StringReader(_xmlText.ToString)
Dim _xmlReader As New System.Xml.XmlTextReader(_textStreamReader)
_Person.ReadXml(_xmlReader)
Listing 7: Deserializing the XML back into the Example Data Objects Using the Helper Routine
_Person = New Buyer
Dim _XMLReader As New CustomXmlSerializer
_Person = _XMLReader.ReadXml(_xmlText, _Person)
Points of Interest
Using Reflection to serialize classes was fairly straightforward until generics entered the picture. From the very beginning, deserialization was very difficult to implement.
I ran into problems with identifying what assembly contained the classes needing to be instantiated. To get around the problem, I used the assembly of the parent class. Therefore, all classes to be deserialized will need to be in the same assembly.
Additionally, dictionaries support key/value pairs. To associate the key with the value, the serializer records the key as an attribute of the value's element tag.
History
Date | Version # | Change History |
---|
06/17/2006 | 1.0 | Initial release of the VB version.
- Supports serialization and deserialization of all
System.Collection list types. - Supports serialization of
System.Array .
|
06/30/2006 | 1.1 | Bug Fixes (for details, refer to inline "Fix" comments)
- Values of
Nothing /null are no longer assigned. Assigning a value of Nothing /null was unreliable for some of the data types. - Added code to verify if an
IEnumerable was deserialized.
New Features
- Initial release of C# version.
- Supports both custom VB.NET and C# data types.
- If class definitions match, supports [serializing/deserializing] C# [to/from] VB.NET.
- Code changes to the VB.NET version to simplify conversion to C#, and continued maintenance in both languages.
- In addition to shallow serialization, now supports deep serialization through the
Method member. - Supports 2003 and 2005 without any code changes.
- Deserializing VS 2003 [to/from] VS 2005 supported.
|
07/06/2006 | 1.2 |
- Added ability to deserialize
System.Array types.
|
07/13/2006 | 1.3 |
- Fixed a few bugs associated with
System.Array serialization/deserialization. - Added support for serializing/deserializing
IEnumerable child classes.
|
08/01/2006 | 1.4 |
- Fixed a problem that occurred when converting the VB code into C#.
- The routine
SaveValue has been modified in the C# version. Special thanks to a friend who pointed out the issue!
|
12/09/2006 | 1.5 |
- Fixed problem with deserializing collections that implement the
IDictionary interface. - Added support for
XmlIgnoreAttribute attribute.
|
12/12/2006 | 1.6 |
- Fixed problem with deserializing
Queue collections.
|