Search

Wednesday 5 October 2011

Serialization 101 - Part III: XML Serialization

Introduction

In the final part in this series on serialization, we are going to take a look at XML serialization.

Now unlike binary and SOAP serialization, which we looked at in parts I and II respectively, XML serialization requires the use of a completely different framework.

The Math Game

For this article, we are going to use the example of a simple math game. The user is presented with a random sum and they have to provide the answer, gaining two points for each correct answer and losing 1 point for each incorrect answer. The application keeps the score and allows the user to save a game in progress to continue it at a later point. The class diagram is shown below (click to zoom):

Making Objects Serializable

Now, unlike binary and SOAP serialization where objects are non-serializable unless explicitly stated otherwise through use of the Serializable attribute; with XML serialization all objects are implicitly serializable and do not require the use of such an attribute. That said, as you will see below, we still end up liberally decorating our classes with various different attributes to further refine the serialization process.

Firstly, let's take a look at the Question class:

// C#
[XmlRoot(ElementName = "question")]
public class Question
{
    [XmlAttribute(AttributeName = "left")]
    public int LeftOperand { get; set; }

    [XmlAttribute(AttributeName = "right")]
    public int RightOperand { get; set; }

    [XmlAttribute(AttributeName = "operator")]
    public Operator Operator { get; set; }
}
' Visual Basic
<XmlRoot(ElementName:="question")>
Public Class Question

    <XmlAttribute(AttributeName:="left")>
    Public Property LeftOperand As Integer

    <XmlAttribute(AttributeName:="right")>
    Public Property RightOperand As Integer

    <XmlAttribute(AttributeName:="operator")>
    Public Property [Operator] As [Operator]

End Class

Note how the class is decorated with an XmlRoot attribute. This attribute controls how an object of this class should be serialized if it is the root element of the XML document. In this case we use it to make sure the element is rendered in lower case.

Next, notice the three properties of the class are decorated with XmlAttribute attributes. By default the XML serializer serializes all properties as XML elements. By using this attribute, we override this behaviour, serializing the properties as attributes instead. At the same time, we are also changing the attribute name to something more concise.

Next we will take a look at the UserAnswer class:

// C#
[XmlRoot(ElementName = "answer")]
public class UserAnswer
{
    [XmlElement(ElementName = "question")]
    public Question Question { get; set; }

    [XmlElement(ElementName = "value")]
    public int Answer { get; set; }

    public bool IsCorrect
    {
        get { return Answer == Question.CorrectAnswer; }
    }
}
' Visual Basic
<XmlRoot(ElementName:="answer")>
Public Class UserAnswer

    <XmlElement(ElementName:="question")>
    Public Property Question As Question

    <XmlElement(ElementName:="value")>
    Public Property Answer As Integer

    Public ReadOnly Property IsCorrect As Boolean
        Get
            Return Answer = Question.CorrectAnswer
        End Get
    End Property
End Class

Here we are using the XmlElement attribute to override the default names given to the XML elements upon serialization. By default, the serializer will name any XML elements or attributes exactly as the corresponding property is named, however in our example, we want the elements in lower case.

It is also worth noting that because the IsCorrect property is read-only, it will not be serialized. However, if the property was not read-only and we didn't want it serialized, we would simply decorated it with the XmlIgnore attribute.

Now finally, the Game class:

// C#
[XmlRoot(ElementName = "game")]
public class Game
{
    [XmlArray(ElementName = "answers")]
    [XmlArrayItem(ElementName = "answer")]
    public UserAnswersCollection Answers { get; set; }

    public int Score
    {
        get { return (Answers.Count(x => x.IsCorrect) * 2) - Answers.Count(x => !x.IsCorrect); }
    }
}
' Visual Basic
<XmlRoot(ElementName:="game")>
Public Class Game

    <XmlArray(ElementName:="answers")>
    <XmlArrayItem(ElementName:="answer")>
    Public Property Answers As UserAnswersCollection

    Public ReadOnly Property Score As Integer
        Get
            Return (Answers.Where(Function(x) x.IsCorrect).Count() * 2) - Answers.Where(Function(x) Not x.IsCorrect).Count()
        End Get
    End Property
End Class

Note how the Answers property is decorated with both an XmlArray and XmlArrayItem attribute. This specifies that the Answers collection is to be serialized as an array with an element name of "answers". Each answer in the collection will be serialized as an individual element named "answer".

OK, Let's Start Serializing

In order to serialize and de-serialize our objects, we need to use an XmlSerializer object. The example below shows how to use the XmlSerializer to save the current game:

// C#
public void SaveGame(Game game, string fileName)
{
    using (Stream fileStream = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None))
    {
        XmlSerializer serializer = new XmlSerializer(typeof(Game));
        serializer.Serialize(fileStream, game);
    }
}
' Visual Basic
Public Sub SaveGame(ByVal game As Game, ByVal fileName As String)
    Using fileStream As Stream = New FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None)
        Dim serializer As XmlSerializer = New XmlSerializer(GetType(Game))
        serializer.Serialize(fileStream, game)
    End Using
End Sub

As with binary and SOAP serialization, we can serialize to any object which inherits from the Stream class. In our example, we use a FileStream object, but this could just as easily have been a NetworkStream object.

Finally, for completeness, the code for loading a previously-saved game from disk

// C#
public Game LoadGame(string fileName)
{
    using (Stream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        XmlSerializer deserializer = new XmlSerializer(typeof(Game));
        return (Game)deserializer.Deserialize(fileStream);
    }
}
' Visual Basic
Public Function LoadGame(ByVal fileName As String) As Game
    Using fileStream As Stream = New FileStream(fileName, FileMode.Create, FileAccess.Read, FileShare.Read)
        Dim deserializer As XmlSerializer = New XmlSerializer(GetType(Game))
        Return DirectCast(deserializer.Deserialize(fileStream), Game)
    End Using
End Function

Below is an example of the XML produced when serializing a game:

<?xml version="1.0"?>
<game xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <answers>
    <answer>
      <question left="6" right="6" operator="Addition" />
      <value>12</value>
    </answer>
    <answer>
      <question left="8" right="2" operator="Multiplication" />
      <value>16</value>
    </answer>
    <answer>
      <question left="8" right="8" operator="Division" />
      <value>1</value>
    </answer>
    <answer>
      <question left="1" right="3" operator="Multiplication" />
      <value>4</value>
    </answer>
  </answers>
</game>

A Quick Note on SOAP

As the SOAP message format is based on XML, you can use XML serialization for serializing and de-serializing objects to and from SOAP messages. Indeed, this method is now preferred over using the SoapFormatter discussed in the previous article. You can find further details on this in the MSDN documentation.

Summary

XML serialization provides the means to serialize objects in a human-readable form and uses a completely separate framework from binary and SOAP serialization. It is also the preferred method for serializing objects to SOAP messages.

You can download the source code for the math game here.

No comments:

Post a Comment