Serializing Objects in XML

 
Chapter 11 - Manipulating XML
bySimon Robinsonet al.
Wrox Press 2002
  

Serializing is the process of persisting an object to disk. Another part of your application, or even a separate application, can deserialize the object and it will be in the same state it was in prior to serialization. The .NET Framework includes a couple of ways to do this.

In this section, we are going to take a look at the System.Xml.Serialization namespace, which contains classes used to serialize objects into XML documents or streams. This means that an object's public properties and public fields are converted into XML elements and/or attributes.

The most important class in the System.Xml.Serialization namespace is XmlSerializer . To serialize an object, we first need to instantiate an XmlSerializer object, specifying the type of the object to serialize. Then we need to instantiate a stream/writer object to write the file to a stream/document. The final step is to call the Serialize() method on the XMLSerializer , passing it the stream/writer object, and the object to serialize.

Data that can be serialized can be primitive types, fields, arrays, and embedded XML in the form of XmlElement and XmlAttribute objects.

To deserialize an object from an XML document, we go through the reverse process to that above. We create a stream/reader and an XmlSerializer object, and then pass the stream/reader to the Deserialize() method. This method returns the deserialized object, although it needs to be cast to the correct type.

Important 

The XML serializer cannot convert private data, only public data, and it cannot serialize object graphs.

However, these should not be serious limitations; by carefully designing your classes, they should be easily avoided. If you do need to be able to serialize public and private data as well as an object graph containing many nested objects, then you will want to use the System.Runtime.Serialization.Formatters.Binary namespace.

Some of the other things that you can do with System.Xml.Serialization classes are:

  • Determine if the data should be an attribute or element

  • Specify the namespace

  • Change the attribute or element name

The links between your object and the XML document are the custom C# attributes that annotate your classes. These attributes are what are used to inform the serializer how to write out the data. There is a tool that is included with the .NET Framework that can help you create these attributes for you if you wish; xsd.exe can do the following:

  • Generate an XML schema from an XDR schema file

  • Generate an XML schema from an XML file

  • Generate DataSet classes from an XSD schema file

  • Generate run-time classes that have the custom attributes for XmlSerialization

  • Generate an XSD file from classes that you have already developed

  • Limit which elements are created in code

  • Determine which programming language the generated code should be in (C#, VB.NET, or JScript.NET)

  • Create schemas from types in compiled assemblies

You should refer to the Framework documentation for details of command-line options for xsd.exe .

Despite these capabilities, you don't have to use xsd.exe to create the classes for serialization. The process is quite simple. Let's take a look at a simple application that serializes a class that reads in the Products data we saved earlier in the chapter. This can be found in the SerialSample1 folder. At the beginning of the example we have very simple code that creates a new Product object, pd , and fills it with some data:

   private void button1_Click(object sender, System.EventArgs e)     {     //new products object     Products pd=new Products();     //set some properties     pd.ProductID=200;     pd.CategoryID=100;     pd.Discontinued=false;     pd.ProductName="Serialize Objects";     pd.QuantityPerUnit="6";     pd.ReorderLevel=1;     pd.SupplierID=1;     pd.UnitPrice=1000;     pd.UnitsInStock=10;     pd.UnitsOnOrder=0;   

The Serialize() method of the XmlSerializer class actually performs the serialization, and it has six overloads. One of the parameters required is a stream to write the data out to. It can be a Stream , TextWriter , or an XmlWriter . In our example we create a TextWriter- based object, tr . The next thing to do is to create the XmlSerializer -based object sr . The XmlSerializer needs to know type information for the object that it is serializing, so we use the typeof keyword with the type that is to be serialized. After the sr object is created, we call the Serialize() method, passing in the tr ( Stream -based object), and the object that you want serialized, in this case pd . Be sure to close the stream when you are finished with it.

   //new TextWriter and XmlSerializer     TextWriter tr=new StreamWriter("..\..\..\serialprod.xml");     XmlSerializer sr=new XmlSerializer(typeof(Products));     //serialize object     sr.Serialize(tr,pd);     tr.Close();     }   

Now let's examine the Products class, the class that is to be serialized. The only differences between this and any other class that you may write are the C# attributes that have been added. The XmlRootAttribute and XmlElementAttribute classes in the attributes inherit from the System.Attribute class. Don't confuse these attributes with the attributes in an XML document. A C# attribute is simply some declarative information that can be retrieved at runtime by the CLR (see Chapter 6 for more details). In this case, the attributes describe how the object should be serialized:

   //class that will be serialized.     //attributes determine how object is serialized     [System.Xml.Serialization.XmlRootAttribute(Namespace="", IsNullable=false)]     public class Products     {     [System.Xml.Serialization.XmlElementAttribute(IsNullable=false)]     public int ProductID;     [System.Xml.Serialization.XmlElementAttribute(IsNullable=false)]     public string ProductName;     [System.Xml.Serialization.XmlElementAttribute()]     public int SupplierID;     [System.Xml.Serialization.XmlElementAttribute()]     public int CategoryID;     [System.Xml.Serialization.XmlElementAttribute()]     public string QuantityPerUnit;     [System.Xml.Serialization.XmlElementAttribute()]     public System.Decimal UnitPrice;     [System.Xml.Serialization.XmlElementAttribute()]     public short UnitsInStock;     [System.Xml.Serialization.XmlElementAttribute()]     public short UnitsOnOrder;     [System.Xml.Serialization.XmlElementAttribute()]     public short ReorderLevel;     [System.Xml.Serialization.XmlElementAttribute()]     public bool Discontinued;     }   

The XmlRootAttribute() invocation in the attribute above the Products class definition identifies this class as a root element (in the XML file produced upon serialization). The attribute containing XmlElementAttribute() identifies that the member below the attribute represents an XML element.

If we take a look at the XML document that is created during serialization, you will see that it looks like any other XML document that we may have created, which is the point of the exercise. Let's take a look at the document:

   <?xml version="1.0" encoding="utf-8"?>     <Products xmlns:xsd="http://www.w3.org/2001/XMLSchema"     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">     <ProductID>200</ProductID>     <ProductName>Serialize Objects</ProductName>     <SupplierID>1</SupplierID>     <CategoryID>100</CategoryID>     <QuantityPerUnit>6</QuantityPerUnit>     <UnitPrice>1000</UnitPrice>     <UnitsInStock>10</UnitsInStock>     <UnitsOnOrder>0</UnitsOnOrder>     <ReorderLevel>1</ReorderLevel>     <Discontinued>false</Discontinued>     </Products>   

There is nothing out of the ordinary here. We could use this any way that you would use an XML document. We could do a transform on it and display it as HTML, load into a DataSet using ADO.NET, load an XmlDocument with it, or, as you can see in the example, deserialize it and create an object in the same state that pd was in prior to serializing it (which is exactly what we're doing with our second button).

Next we will add another button event handler to deserialize a new Products -based object newPd . This time we will be using a FileStream object to read in the XML:

   private void button2_Click(object sender, System.EventArgs e)     {     //create a reference to products type     Products newPd;     //new filestream to open serialized object     FileStream f=new FileStream("..\..\..\serialprod.xml",FileMode.Open);   

Once again, we create a new XmlSerializer , passing in the type information of Product . We can then make the call to the Deserialize() method. Note that we still need to do an explicit cast when we create the newPd object. At this point newPd is in exactly the same state as pd was:

   //new serializer     XmlSerializer newSr=new XmlSerializer(typeof(Products));     //deserialize the object     newPd=(Products)newSr.Deserialize(f);     //load it in the list box.     listBox1.Items.Add(newPd.ProductName);     f.Close();     }   

The example that we just looked at is very simple let's look at a more complex example using the XmlSerializer class. We'll make each field private , accessible only via get and set properties in the Products class. We will also add a Discount attribute to the XML file, to demonstrate that attributes can be serialized too.

This example can be found in the SerialSample2 folder; here's what our new Products class looks like:

   [System.Xml.Serialization.XmlRootAttribute()]   public class Products {   private int prodId;     private string prodName;     private int suppId;     private int catId;     private string qtyPerUnit;     private Decimal unitPrice;     private short unitsInStock;     private short unitsOnOrder;     private short reorderLvl;     private bool discont;     private int disc;     //add the Discount attribute     [XmlAttributeAttribute(AttributeName="Discount")]     public int Discount     {     get {return disc;}     set {disc=value;}     }     [XmlElementAttribute()]     public int ProductID     {     get {return prodId;}     set {prodId=value;}     }     ...     // properties for most of the fields are not shown for sake of brevity     ...     [XmlElementAttribute()]     public bool Discontinued     {     get {return discont;}     set {discont=value;}     }     }   

You will also need to make the following modifications to the button click event handlers:

 private void button1_Click(object sender, System.EventArgs e) {    //new products object    Products pd=new Products();    //set some properties    pd.ProductID=200;    pd.CategoryID=100;    pd.Discontinued=false;    pd.ProductName="Serialize Objects";    pd.QuantityPerUnit="6";    pd.ReorderLevel=1;    pd.SupplierID=1;    pd.UnitPrice=1000;    pd.UnitsInStock=10;    pd.UnitsOnOrder=0;   pd.Discount=2;   //new TextWriter and XmlSerializer   TextWriter tr=new StreamWriter("..\..\..\serialprod1.xml");   XmlSerializer sr=new XmlSerializer(typeof(Products));    //serialize object    sr.Serialize(tr,pd);    tr.Close(); } private void button2_Click(object sender, System.EventArgs e) {    //create a reference to products type    Products newPd;    //new filestream to open serialized object   FileStream f=new FileStream("..\..\..\serialprod1.xml",FileMode.Open);   //new serializer    XmlSerializer newSr=new XmlSerializer(typeof(Products));    //deserialize the object    newPd=(Products)newSr.Deserialize(f);    //load it in the list box.    listBox1.Items.Add(newPd.ProductName);    f.Close(); } 

Running this code will give the same results as the earlier example, but with one difference. The output for this ( serialprod1.xml ) looks like this:

 <?xml version="1.0" encoding="utf-8"?> <Products xmlns:xsd="http://www.w3.org/2001/XMLSchema"            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"   Discount="2">   <ProductID>200</ProductID>    <ProductName>Serialize Objects</ProductName>    <SupplierID>1</SupplierID>    <CategoryID>100</CategoryID>    <QuantityPerUnit>6</QuantityPerUnit>    <UnitPrice>1000</UnitPrice>    <UnitsInStock>10</UnitsInStock>    <UnitsOnOrder>0</UnitsOnOrder>    <ReorderLevel>1</ReorderLevel>    <Discontinued>false</Discontinued> </Products> 

Notice the Discount attribute on the Products element. So, now that you have property accessors defined, you can add more complex validation code in the properties.

What about situations where we have derived classes, and possibly properties that return an array? XmlSerializer has that covered as well. Let's look at a slightly more complex example that deals with these issues.

First we define three new classes, Product , BookProduct (derived from Product ), and Inventory (which contains both of the other classes):

   public class Product     {     private int prodId;     private string prodName;     private int suppId;     public Product() {}     public int  ProductID     {     get {return prodId;}     set {prodId=value;}     }     public string ProductName     {     get {return prodName;}     set {prodName=value;}     }     public int SupplierID     {     get {return suppId;}     set {suppId=value;}     }     }     public class BookProduct : Product     {     private string isbnNum;     public BookProduct() {}     public string ISBN     {     get {return isbnNum;}     set {isbnNum=value;}     }     }     public class Inventory     {     private Product[] stuff;     public Inventory() {}     //need to have an attribute entry for each data type     [XmlArrayItem("Prod",typeof(Product)),     XmlArrayItem("Book",typeof(BookProduct))]     public Product[] InventoryItems     {     get {return stuff;}     set {stuff=value;}     }     }   

The Inventory class is the one of real interest to us here. If we are to serialize this class, we need to insert an attribute containing XmlArrayItem constructors for each type that can be added to the array. You should note that XmlArrayItem is the name of the .NET attribute represented by the XmlArrayItemAttribute class.

The first parameter supplied to these constructors is what we would like the element name to be in the XML document that is created during serialization. If we leave off the ElementName parameter, the elements will be given the same name as the object type ( Product and BookProduct in this case). The second parameter that must be specified is the type of the object.

There is also an XmlArrayAttribute class that you would use if the property were returning an array of objects or primitive type. Since we are returning different types in the array, we use XmlArrayItemAttribute , which allows the higher level of control.

In the button1_Click() event handler, we create a new Product object and a new BookProduct object ( newProd and newBook ). We add data to the various properties of each object, and add the objects to a Product array. We then create a new Inventory object and pass in the array as a parameter. We can then serialize the Inventory object to recreate it at a later time:

   private void button1_Click(object sender, System.EventArgs e)     {     //create new book and bookproducts objects     Product newProd=new Product();     BookProduct newBook=new BookProduct();     //set some properties     newProd.ProductID=100;     newProd.ProductName="Product Thing";     newProd.SupplierID=10;     newBook.ProductID=101;     newBook.ProductName="How to Use Your New Product Thing";     newBook.SupplierID=10;     newBook.ISBN="123456789";     //add the items to an array     Product[] addProd={newProd,newBook};     //new inventory object using the addProd array     Inventory inv=new Inventory();     inv.InventoryItems=addProd;     //serialize the Inventory object     TextWriter tr=new StreamWriter("..\..\..\order.xml");     XmlSerializer sr=new XmlSerializer(typeof(Inventory));     sr.Serialize(tr,inv);     tr.Close();     }   

This is what the XML document looks like:

   <?xml version="1.0" encoding="utf-8"?>     <Inventory xmlns:xsd="http://www.w3.org/2001/XMLSchema">     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">     <InventoryItems>     <Prod>     <ProductID>100</ProductID>     <ProductName>Product Thing</ProductName>     <SupplierID>10</SupplierID>     </Prod>     <Book>     <ProductID>101</ProductID>     <ProductName>How to Use Your New Product Thing</ProductName>     <SupplierID>10</SupplierID>     <ISBN>123456789</ISBN>     </Book>     </InventoryItems>     </Inventory>   

The button2_Click() event handler implements deserialization of the Inventory object. Notice that we iterate through the array in the newly-created newInv object to show that it is the same data:

   private void button2_Click(object sender, System.EventArgs e)     {     Inventory newInv;     FileStream f=new FileStream("..\..\..\order.xml",FileMode.Open);     XmlSerializer newSr=new XmlSerializer(typeof(Inventory));     newInv=(Inventory)newSr.Deserialize(f);     foreach(Product prod in newInv.InventoryItems)     listBox1.Items.Add(prod.ProductName);     f.Close();     }   

Serialization Without Sourcecode Access

Well this all works great, but what if you don't have access to the sourcecode for the types that are being serialized? You can't add the attribute if you don't have the source. There is another way. You can use the XmlAttributes class and the XmlAttributeOverrides class. Together these classes will allow you to accomplish exactly what we have just done, but without adding the attributes. Let's look at an example of how this works, taken from the SerialSample4 folder.

For this example, imagine that the Inventory , Product , and the derived BookProduct classes are in a separate DLL, and that we don't have the source. The Product and BookProduct classes are the same as in the previous example, but you should note that there are now no attributes added to the Inventory class:

   public class Inventory     {     private Product[] stuff;     public Inventory() {}     public Product[] InventoryItems     {     get {return stuff;}     set {stuff=value;}     }     }   

Let's now deal with the serialization in the button1_Click() event handler:

   private void button1_Click(object sender, System.EventArgs e)     {   

The first step in the serialization process is to create an XmlAttributes object, and an XmlElementAttribute object for each data type that you will be overriding:

   XmlAttributes attrs=new XmlAttributes();     attrs.XmlElements.Add(new XmlElementAttribute("Book",typeof(BookProduct)));     attrs.XmlElements.Add(new XmlElementAttribute("Product",typeof(Product)));   

Here you can see that we are adding new XmlElementAttribute objects to the XmlElements collection of the XmlAttributes class. The XmlAttributes class has properties that correspond to the attributes that can be applied; XmlArray and XmlArrayItems , which we looked at in the previous example, are just a few of these. We now have an XmlAttributes object with two XmlElementAttribute -based objects added to the XmlElements collection.

The next thing we have to do is create an XmlAttributeOverrides object:

   XmlAttributeOverrides attrOver=new XmlAttributeOverrides();     attrOver.Add(typeof(Inventory),"InventoryItems",attrs);   

The Add() method of this class has two overloads. The first one takes the type information of the object to override and the XmlAttributes object that we created earlier. The other overload, which is the one we are using, also takes a string value that is the member in the overridden object. In our case we want to override the InventoryItems member in the Inventory class.

When we create the XmlSerializer object, we add the XmlAttributeOverrides object as a parameter. Now the XmlSerializer knows which types we want to override and what we need to return for those types.

   //create the Product and Book objects     Product newProd=new Product();     BookProduct newBook=new BookProduct();     newProd.ProductID=100;     newProd.ProductName="Product Thing";     newProd.SupplierID=10;     newBook.ProductID=101;     newBook.ProductName="How to Use Your New Product Thing";     newBook.SupplierID=10;     newBook.ISBN="123456789";     Product[] addProd={newProd,newBook};         Inventory inv=new Inventory();     inv.InventoryItems=addProd;     TextWriter tr=new StreamWriter("..\..\..\inventory.xml");     XmlSerializer sr=new XmlSerializer(typeof(Inventory),attrOver);     sr.Serialize(tr,inv);     tr.Close();     }   

If we execute the Serialize() method we will end up with this XML output:

   <?xml version="1.0" encoding="utf-8"?>     <Inventory xmlns:xsd="http://www.w3.org/2001/XMLSchema">     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">     <Product>     <ProductID>100</ProductID>     <ProductName>Product Thing</ProductName>     <SupplierID>10</SupplierID>     </Product>     <Book>     <ProductID>101</ProductID>     <ProductName>How to Use Your New Product Thing</ProductName>     <SupplierID>10</SupplierID>     <ISBN>123456789</ISBN>     </Book>     </Inventory>   

As you can see, we get the same XML as we did with the earlier example. In order to deserialize this object and recreate the Inventory -based object that we started out with, we need to create all of the same XmlAttributes , XmlElementAttribute, and XmlAttributeOverrides objects that we created when we serialized the object. Once we do that we can read in the XML and recreate the Inventory object just as we did before. Here is the code to deserialize the Inventory object:

   private void button2_Click(object sender, System.EventArgs e)     {     //create the new XmlAttributes collection     XmlAttributes attrs=new XmlAttributes();     //add the type information to the elements collection     attrs.XmlElements.Add(new XmlElementAttribute("Book",typeof(BookProduct)));     attrs.XmlElements.Add(new XmlElementAttribute("Product",typeof(Product)));     XmlAttributeOverrides attrOver=new XmlAttributeOverrides();     //add to the Attributes collection     attrOver.Add(typeof(Inventory),"InventoryItems",attrs);     //need a new Inventory object to deserialize to     Inventory newInv;     //deserialize and load data into the listbox from deserialized object     FileStream f=new FileStream("..\..\..\inventory.xml",FileMode.Open);     XmlSerializer newSr=new XmlSerializer(typeof(Inventory),attrOver);     newInv=(Inventory)newSr.Deserialize(f);     if(newInv!=null)     {     foreach(Product prod in newInv.InventoryItems)     listBox1.Items.Add(prod.ProductName);     }     f.Close();     }   

Notice that the first few lines of code are identical to the code we used to serialize the object.

The System.Xml.XmlSerialization namespace provides a very powerful toolset for serializing objects to XML. By serializing and de-serializing objects to XML instead of to binary format, you are given the option of doing something else with this XML, greatly adding to the flexibility of your designs.

  


Professional C#. 2nd Edition
Performance Consulting: A Practical Guide for HR and Learning Professionals
ISBN: 1576754359
EAN: 2147483647
Year: 2002
Pages: 244

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net