Chapter 25
Serialization

What’s in This Chapter

  • Serialization and deserialization
  • XML, JSON, and binary serialization
  • Using attributes to control serialization

Wrox.com Downloads for This Chapter

Please note that all the code examples for this chapter are available as a part of this chapter’s code download on the book’s website at www.wrox.com/go/csharp5programmersref on the Download Code tab.

Serialization is the process of converting one or more objects into a serial, stream-like format, often text. Deserialization is the reverse: restoring an object from its serialization.

Like the XML files described in the preceding chapter, serialization gives you a portable way to store data. You can serialize an object and send the serialization to another program, and then that program can deserialize the object.

You can also use serialization to allow a program to save and restore its state. For example, a drawing program might build a complicated data structure containing the objects the user drew. To save the drawing, the program could write the data structure’s serialization into a file. Later it could reload the serialization and display the objects.

The objects serialized can be relatively simple, such as a Customer or Person object, or they can be complex data structures including objects that have references to other objects. For example, you could serialize a CustomerOrder object that includes an array of Order objects each containing a list of OrderItem objects.

For something that can be so complicated, serialization is surprisingly easy in .NET.

This chapter explains serialization. It explains how to serialize and deserialize objects. It also explains attributes that you can use to control how objects of a particular class are serialized.

The .NET Framework gives you three choices for how data is stored in a serialization: XML data, JSON data, or binary data. The following sections explain how you can use each of these three choices.

XML Serialization

To serialize and deserialize objects in XML format, you use the XmlSerializer class’s Serialize and Deserialize methods. The methods work only for classes that have a parameterless constructor, either the default constructor or one that you created. They also work only for public properties and fields. All other properties and fields are ignored.

The Serialize and Deserialize methods are fairly easy to use; although, you do need to pass them some slightly exotic parameters. For example, you might expect the Serialize method to simply return a serialization string. Actually, it requires a stream, XmlWriter, or TextWriter parameter where it can write the serialization.

The following section explains the basic process of serializing and deserializing objects. The section after that describes some attributes you can use to change the way values are serialized.

Performing Serialization

The SerializeCustomer example program, which is available for download on this book’s website, demonstrates XML serialization and deserialization. Before you look at the code, here’s an overview of what the program does.

The example uses three classes: Customer, Order, and OrderItem. When it starts, the program creates a Customer object, which contains Order objects, which contain OrderItem objects. It then displays the Customer object’s data. It serializes the Customer object and displays the serialization. Finally, the program deserializes the XML data to create a new object and displays that object’s data.

Figure 25-1 shows the program displaying the Customer objects’ data and the serialization. If you look closely, you can see that the data in the left and right TextBoxes is the same, so the program did successfully serialize and deserialize the Customer object.

c25f001.tif

Figure 25-1: The SerializeCustomer example program serializes and deserializes a Customer object.

The following code shows the OrderItem class.

public class OrderItem
{
    public string Description;
    public int Quantity;
    public decimal UnitPrice;

    public OrderItem() { }
    public OrderItem(string description, int quantity, decimal unitPrice)
    {
        Description = description;
        Quantity = quantity;
        UnitPrice = unitPrice;
    }
}

This class has Description, Quantity, and UnitPrice fields; a parameterless constructor; and an initializing constructor.

The following code shows the Order class.

public class Order
{
    public DateTime OrderDate;
    public OrderItem[] OrderItems;

    public Order() { }
    public Order(DateTime orderDate, params OrderItem[] orderItems)
    {
        OrderDate = orderDate;
        OrderItems = orderItems;
    }
}

This class includes an OrderDate field and an array of OrderItem objects. It also has a parameterless constructor and an initializing constructor.

The following code shows the Person class.

// Customer and related classes.
public class Customer
{
    public string FirstName, LastName;
    public List<Order> Orders = new List<Order>();

    public Customer() { }
    public Customer(string firstName, string lastName, params Order[] orders)
    {
        FirstName = firstName;
        LastName = lastName;
        foreach (Order order in orders) Orders.Add(order);
    }
}

This class has FirstName and LastName fields. It holds information about orders in a List<Order>.

When the program starts, it uses the following code to create a Customer object and display its data.

// Create some OrderItems.
OrderItem item1 = new OrderItem("Pencil", 12, 0.25m);
OrderItem item2 = new OrderItem("Notepad", 6, 1.00m);
OrderItem item3 = new OrderItem("Binder", 1, 3.50m);
OrderItem item4 = new OrderItem("Tape", 12, 0.75m);
// Create some Orders.
Order order1 = new Order(new DateTime(2014, 4, 4), item1, item2);
Order order2 = new Order(new DateTime(2014, 4, 17), item3, item4);

// Create a Customer.
Customer customer = new Customer("Rod", "Stephens", order1, order2);

// Display the Customer.
DisplayCustomer(originalTextBox, customer);

This code just creates some objects and sets their fields’ values. It then calls the DisplayCustomer method to display the Customer object’s values in a TextBox. It isn’t very interesting so it isn’t shown here. Download the example to see how it works.

Next, the program uses the following code to serialize the Customer object and display the serialization.

// Create a serializer that works with the Customer class.
XmlSerializer serializer = new XmlSerializer(typeof(Customer));

// Create a TextWriter to hold the serialization.
string serialization;
using (TextWriter writer = new StringWriter())
{
    // Serialize the Customer.
    serializer.Serialize(writer, customer);
    serialization = writer.ToString();
}

// Display the serialization.
serializationTextBox.Text = serialization;

The code first creates an XmlSerializer object to perform the serialization (and later the deserialization). It passes the XmlSerializer constructor the type of the class that it will serialize. In this example, the serializer object knows only how to serialize and deserialize Customer objects.

The serializer can serialize only into streams, XmlWriters, and TextWriters, so this example creates a TextWriter to hold the serialization. It calls the serializer’s Serialize method, passing it the Customer object to serialize. The program saves the serialization in a string and displays the string in a TextBox.

The following code shows how the program deserializes the serialization.

// Create a stream from which to read the serialization.
using (TextReader reader = new StringReader(serialization))
{
    // Deserialize.
    Customer newCustomer = (Customer)serializer.Deserialize(reader);

    // Display the deserialization.
    DisplayCustomer(deserializedTextBox, newCustomer);
}

The serializer can deserialize only from streams, XmlReaders, and TextReaders, so the program creates a reader to read from the serialization string.

The program then calls the Deserialize method, passing it the reader. The Deserialize method returns a nonspecific object so the program casts it into a Customer object.

The program finishes by displaying the deserialized object.

The following code shows the object’s serialization.

<?xml version="1.0" encoding="utf-16"?>
<Customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <FirstName>Rod</FirstName>
  <LastName>Stephens</LastName>
  <Orders>
    <Order>
      <OrderDate>2014-04-04T00:00:00</OrderDate>
      <OrderItems>
        <OrderItem>
          <Description>Pencil</Description>
          <Quantity>12</Quantity>
          <UnitPrice>0.25</UnitPrice>
        </OrderItem>
        <OrderItem>
          <Description>Notepad</Description>
          <Quantity>6</Quantity>
          <UnitPrice>1.00</UnitPrice>
        </OrderItem>
      </OrderItems>
    </Order>
    <Order>
      <OrderDate>2014-04-17T00:00:00</OrderDate>
      <OrderItems>
        <OrderItem>
          <Description>Binder</Description>
          <Quantity>1</Quantity>
          <UnitPrice>3.50</UnitPrice>
        </OrderItem>
        <OrderItem>
          <Description>Tape</Description>
          <Quantity>12</Quantity>
          <UnitPrice>0.75</UnitPrice>
        </OrderItem>
      </OrderItems>
    </Order>
  </Orders>
</Customer>

If you look through the serialization, it makes reasonably intuitive sense. A Customer element represents a Customer object. It contains FirstName and LastName elements to hold its simple data values. The Orders element holds a sequence of Order elements to represent the Customer object’s List<Order>.

Each Order element holds other elements that contain information about the program’s Order object. The OrderItems element holds a sequence of OrderItem objects to represent an OrderItem object’s OrderItems array.

Controlling Serialization

The serialization shown in the preceding section is straightforward and reasonably intuitive, but there are times when you might want to change the way an object is serialized.

For example, the previous serialization is easy to understand but it’s quite verbose. Perhaps you would rather store this data:

<OrderItem>
  <Description>Tape</Description>
  <Quantity>12</Quantity>
  <UnitPrice>0.75</UnitPrice>
</OrderItem>

Like this:

<OrderItem Description="Tape" Quantity="12" UnitPrice="0.75" />

The result is more concise and possibly easier to read.

For another example, suppose your program saves a customer’s order data and you want to use that data in another program. Unfortunately, the other program stores a Customer object’s order information in an Order[] named CustomerOrders instead of a List<Order> named Orders.

At this point you might realize that you could use XSLT to transform your serialization into a format that the other program could deserialize. That would certainly work (and would let you make other transformations that you can’t with the techniques that follow) but it would be extra work and require another processing step.

Fortunately, the System.Xml.Serialization namespace defines attributes that you can use to gain some control over the serialization process. You add these attributes to classes or the properties and fields of classes that you want to serialize. They can indicate that a value should be stored in the serialization as an attribute instead of an element. They can also change the name of a value in the serialization or indicate that a value should be completely ignored.

For example, consider the following versions of the Customer, Order, and OrderItem classes. Here I’ve removed the constructors (which are the same as the previous versions) to save space. The attributes are highlighted in bold.

public class Customer
{
    [XmlAttribute]
    public string FirstName, LastName;
    [XmlArray("CustomerOrders")]
    public List<Order> Orders = new List<Order>();
}

public class Order
{
    [XmlIgnore]
    public DateTime OrderDate;
    [XmlArray("Items")]
    public OrderItem[] OrderItems;
}

public class OrderItem
{
    [XmlAttribute]
    public string Description;
    [XmlAttribute]
    public int Quantity;
    [XmlAttribute("PriceEach")]
    public decimal UnitPrice;
}

In the Customer class, the XmlAttribute attribute indicates that the FirstName and LastName fields should be serialized as XML attributes instead of elements. The XmlArray attribute indicates that the Customer class’s Orders list should be called CustomerOrders in the serialization. (Lists and arrays are both serialized in the same way so that the XmlArray attribute doesn’t change the way this is serialized. It just changes the name.)

In the Order class, the XmlIgnore attribute indicates that the OrderDate field should not be serialized at all. The XmlArray attribute makes the OrderItems array serialize with the name Items.

In the OrderItem class, the three XmlAttribute attributes indicate that all three fields should be stored as XML attributes and that the UnitPrice field should be stored as PriceEach in the serialization.

The SerializeCustomerWithAttributes example program is identical to the SerializeCustomer program except it uses the new definitions of the Customer, Order, and OrderItem classes. The following code shows the XML serialization it produces.

<?xml version="1.0" encoding="utf-16"?>
<Customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns:xsd="http://www.w3.org/2001/XMLSchema"
          FirstName="Rod" LastName="Stephens">
  <CustomerOrders>
    <Order>
      <Items>
        <OrderItem Description="Pencil" Quantity="12" PriceEach="0.25" />
        <OrderItem Description="Notepad" Quantity="6" PriceEach="1.00" />
      </Items>
    </Order>
    <Order>
      <Items>
        <OrderItem Description="Binder" Quantity="1" PriceEach="3.50" />
        <OrderItem Description="Tape" Quantity="12" PriceEach="0.75" />
      </Items>
    </Order>
  </CustomerOrders>
</Customer>

This is still reasonably intuitive and more concise than the previous version.

The following table lists the most useful attributes that you can use to control serialization. (The names of the classes that implement these attributes all end with Attribute. For example, the XmlArray attribute is implemented by the XmlArrayAttribute class. If you want to look up any of these online, use their full class names.)

AttributePurpose
XmlArrayChanges the name by which the array or list is serialized.
XmlArrayItemIndicates a type that can be in an array. For example, suppose the People array contains Person objects, some of which might be from the Author subclass. Then you would use XmlArrayItem twice to indicate that the array might contain Person and Author objects.
XmlAttributeSerializes a field as an attribute instead of an element. Optionally sets the name of the attribute.
XmlElementSpecifically indicates the field will be serialized as an XML element. This attribute allows you to change the XML element’s name.
XmlEnumEnables you to specify the names by which enumeration values are serialized. For example, suppose the enumeration MealSize defines values Small, Medium, and Large. You could use this attribute to make the serialization call those values Tall, Grande, and Brobdignagian.
XmlIgnoreMakes the serializer omit a field from the serialization.
XmlRootControls the name and namespace for the element generated for a serialization’s root element. For example, the attribute [XmlRoot("Client")] in front of the Customer class would make the serializer name the root element Client. This would not affect Customer objects that are not the root element. (See XmlType.)
XmlTextMakes the serializer store a value as XML text. An object can have only one text value. (The serializer cannot put more than one text value between the object’s start and end tags.)
XmlTypeControls the name and namespace for the element generated for a class. For example, if you place the attribute [XmlType("Item")] in front of the OrderItem class, then all OrderItem objects are serialized as Item elements.

The DataContractSerializer class can also use XML to serialize and deserialize objects. The process is similar to using the DataContractJsonSerializer class, which is described in the next section.

JSON Serialization

Like XML, JSON (JavaScript Object Notation) is a data storage language. JSON is somewhat simpler and often more concise than XML so it is preferred by some developers.

JSON data mostly consists of name:value pairs where names are text and values can have one of the following data types:

  • Number
  • String
  • Boolean
  • Array (a sequence of values separated by commas and enclosed in brackets [ ])
  • Object (a collection of key:value pairs with pairs separated by commas and the whole collection surrounded by braces {})
  • null

The following code shows a JSON representation of a Customer object that contains two Order objects; each Order object holds two OrderItem objects.

{
  "FirstName":"Rod",
  "LastName":"Stephens",
  "Orders":
  [
    {
      "OrderDate":"/Date(1396591200000-0600)/",
      "OrderItems":
        [
          {"Description":"Pencil","Quantity":12,"UnitPrice":0.25},
          {"Description":"Notepad","Quantity":6,"UnitPrice":1.00}
        ]
    },
    {
      "OrderDate":"/Date(1397714400000-0600)/",
      "OrderItems":
      [
        {"Description":"Binder","Quantity":1,"UnitPrice":3.50},
        {"Description":"Tape","Quantity":12,"UnitPrice":0.75}
      ]
    }
  ]
}

As is the case with XML serialization, you can use attributes to control how classes are serialized by the DataContractJsonSerializer class. The following section explains the basic JSON serialization and deserialization processes. The section after that describes attributes you can use to control the serialization.

Performing Serialization

The DataContractJsonSerializer class serializes and deserializes objects using the JSON format. The class’s WriteObject method serializes an object. Its ReadObject method deserializes an object.

The SerializeCustomerJson example program, which is available for download on this book’s website, uses the following code to serialize and deserialize a Customer object in the JSON format.

// Create a serializer that works with the Customer class.
DataContractJsonSerializer serializer =
    new DataContractJsonSerializer(typeof(Customer)); 

// Create a stream to hold the serialization.
using (MemoryStream stream = new MemoryStream())
{
    // Serialize the Customer.
    serializer.WriteObject(stream, customer);

    // Convert the stream into a string.
    stream.Seek(0, SeekOrigin.Begin);
    string serialization;
    using (StreamReader reader = new StreamReader(stream))
    {
        serialization = reader.ReadToEnd();

        // Display the serialization.
        serializationTextBox.Text = serialization;

        // Deserialize from the stream.
        stream.Position = 0;
        Customer newCustomer = (Customer)serializer.ReadObject(stream);

        // Display the deserialization.
        DisplayCustomer(deserializedTextBox, newCustomer);
    }
}

The code first creates a DataContractJsonSerializer object.

That object can write serializations into a stream, XmlDictionaryWriter, or XmlWriter. This example creates a MemoryStream to hold the serialization. The code then calls the serializer’s WriteObject method to write the Customer object’s serialization into the stream.

To get the serialization as a string, the program rewinds the stream and creates a StreamReader associated with the stream. It uses that object’s ReadToEnd method to get the text from the stream and displays the serialization in a TextBox.

To deserialize the Customer object, the program uses the same MemoryStream (because it already contains the serialization). The code rewinds the stream and uses the serializer’s ReadObject method to read the Customer object from the serialization.

The code finishes by calling the DisplayCustomer method (not shown here) to display the object’s data.

Controlling Serialization

You can use attributes to control a JSON serialization much as you can use attributes to control an XML serialization. JSON is a simpler format, however, so there are fewer attributes available. For example, JSON doesn’t let you store values as element attributes (as in <Customer FirstName="Zaphod">) so there’s no attribute for that.

If you don’t give a class any attributes, the serializer tries to serialize all of its public properties and methods.

You can use the XmlSerializable attribute to explicitly mark a class as serializable. If you do, the serializer still tries to serialize all its public properties and methods.

In both of these cases, when you don’t give a class any attributes or you give it the XmlSerializable attribute, you can give a field the IgnoreDataMember attribute to indicate that the field should not be serialized.

In addition to using the XmlSerializable attribute, you can mark a class as serializable by giving it the DataContract attribute. This attribute’s properties also let you change the name used to represent the class in the serialization.

If you give a class the DataContract attribute, then the serializer’s default behavior changes. Instead of serializing all public fields and properties that are not marked with IgnoreDataMember, the serializer serializes only public fields and properties that are marked with the DataMember attribute. The DataMember attribute also lets you change a member’s name or set the order in which it is included in the object’s serialization.

The following code shows an OrderItem class with some serialization control attributes.

[DataContract(Name = "Item")]
public class OrderItem
{
    [DataMember]
    public string Description;

    [DataMember]
    public int Quantity;

    [DataMember(Name = "PriceEach")]
    public decimal UnitPrice;
}

The DataContract attribute indicates that the class is serializable and that any fields or properties without the DataMember attribute will be ignored. It also makes the serializer use the name Item when it writes an OrderItem into a serialization.

The first two DataMember attributes indicate that the Description and Quantity fields should be included in serializations.

The last DataMember attribute indicates that the PriceEach field should be included in serializations and that it should be called PriceEach in serializations.

Binary Serialization

The XML and JSON serializers are fairly easy to use and produce reasonably intuitive results. However, they have a couple of drawbacks. For example, there are some data types such as images that they cannot serialize. (Exercise 4 explains one way around the problem.) They also cannot serialize data structures that contain cycles.

For example, suppose you build an organizational model with Department objects representing your company’s departments and Employee objects representing employees. The Department class has an Employees property that is a List<Employee> that contains references to the employees in that department. The Employee class has a Department property that is a reference to the department where that employee works.

The XML and JSON serializers cannot serialize this structure because the Department object contains references to Employee objects, and the Employee objects have references to the Department object. These references form a cycle and the serializers don’t know how to handle cycles.

The BinaryFormatter class serializes and deserializes data and can handle both images and cycles.

The SerializeDepartment example program, which is available for download on this book’s website, demonstrates the BinaryFormatter. It uses the following Department and Employee classes (constructors omitted).

[Serializable]
public class Department
{
    public Image Logo;
    public string Name;
    public List<Employee> Employees = new List<Employee>();
}

[Serializable]
public class Employee
{
    public string Name;
    public Department Department;
}

The BinaryFormatter can work only with classes that are decorated with the Serializable attribute.

The example program uses the following code to serialize and deserialize a Department object.

// Create a BinaryFormatter.
IFormatter formatter = new BinaryFormatter();

// Create a stream to hold the serialization.
using (MemoryStream stream = new MemoryStream())
{
    // Serialize.
    formatter.Serialize(stream, department);

    // Display a textual representation of the serialization.
    byte[] bytes = stream.ToArray();
    string serialization = BitConverter.ToString(bytes).Replace("-", " ");

    // Display the serialization.
    serializationTextBox.Text = serialization;

    // Deserialize.
    stream.Seek(0, SeekOrigin.Begin);
    Department newDepartment = (Department)formatter.Deserialize(stream);

    // Display the new Department's data.
    deserializedPictureBox.Image = newDepartment.Logo;
    deserializedTextBox.Text = DisplayDepartment(newDepartment);
}

The code starts by creating a BinaryFormatter object. A BinaryFormatter uses only streams for serialization, so the program creates a MemoryStream. It then uses the formatter’s Serialize method to serialize the Department object into the stream.

Next, the code converts the serialization stream into a byte array. It converts that array into a string showing the serialization’s hexadecimal contents and displays the result in a TextBox.

To deserialize the serialization, the code rewinds the MemoryStream, and then calls the formatter’s Deserialize method. The code finishes by displaying the reconstituted Department object’s logo and employee data. (The DisplayDepartment method is straightforward and unrelated to serialization, so it isn’t shown here. Download the example to see how it works.)

Summary

This chapter explained techniques you can use to serialize and deserialize data. It explained how you can use the XmlSerializer class to use XML serializations, the DataContractJsonSerializer class to use JSON serializations, and the BinaryFormatter class to make binary serializations.

Binary serializations have the disadvantage that you can’t easily read them to see what they contain. They have the advantage that they can contain data types such as images that the other serializations cannot. They can also serialize data that contains cyclical references.

You can use attributes to influence the way objects are stored in XML and JSON serializations, but most of the process is automatic. The serializers use reflection to determine what data is included in a class. The next chapter explains how you can use reflection in your programs to get information about the properties, methods, and events contained in a class.

Exercises

  1. Write a program similar to the SerializeCustomer example program but that works with an array of Student objects. Give the Student class FirstName and LastName properties (not fields). The program should
    • Create an array of Students.
    • Display the original Students.
    • Serialize the array storing FirstName and LastName as attributes.
    • Display the serialization.
    • Deserialize the array.
    • Display the deserialized Students.

    Hint: Override the Student class’s ToString method. Then set a ListBox’s DataSource property equal to an array to display the Students it contains.

  2. Make a classroom editor similar to the one shown in Figure 25-2. Make the program store Student objects in a List<Student>. Display the list by setting a ListBox’s DataSource property equal to it. When the program ends, save the list’s serialization into a file. When the program restarts, reload the serialization.
    c25f002.tif

    Figure 25-2: For Exercise 2, make a classroom editor that saves and restores a list of Student objects when the program stops and starts.

    Hints: Note that you don’t need to use strings to serialize and deserialize. Just serialize and deserialize directly in and out of a file. Before you change the list to add, remove, or edit a Student, set the ListBox’s DataSource property to null. After the change, set the DataSource property back to the list.

  3. Make a static XmlTools class. Give it generic Serialize and Deserialize methods that serialize and deserialize objects to and from strings. Modify the program you wrote for Exercise 1 to test the methods.
  4. The XmlSerializer class cannot serialize images, but you can help it along by creating a public property that gets and sets an image in a format that it understands.

    Write a program similar to the SerializeCustomer example program but that works with a single Student object. Give the Student class FirstName, LastName, and Picture properties.

    Hint: Also give the class the following property that gets and sets the picture as an array of bytes.

    // Return the Picture as a byte stream.
    public byte[] PictureBytes
    {
        get     // Serialize
        {
            if (Picture == null) return null;
            using (MemoryStream stream = new MemoryStream())
            {
                Picture.Save(stream, ImageFormat.Png);
                return stream.ToArray();
            }
        }
        set     // Deserialize.
        {
            if (value == null) Picture=  null;
            else
            {
                using (MemoryStream stream = new MemoryStream(value))
                {
                    Picture = new Bitmap(stream);
                }
            }
        }
    }
  5. Make a friendship tracking program similar to the one shown in Figure 25-3. When the user clicks a name in the ListBox on the left, the program checks that person’s friends in CheckedListBox on the right.
    c25f003.tif

    Figure 25-3: For Exercise 5, make a friendship editor that saves and restores a list of Person objects when the program stops and starts.

    Use the following Person class to store information about the people.

    public class Person
    {
        public string Name;
        public List<Person> Friends = new List<Person>();
        public override string ToString()
        {
            return Name;
        }
    }

    Hints: Store the person information in a List<Person>. Serialize and deserialize the data into a file when the program stops and starts. If the data file doesn’t exist when the program starts, just create some Person objects. (A real application would let the user add, edit, and delete Person objects, perhaps as in the program you wrote for Exercise 2.)

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.77.117