Serializing Objects in XML


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.

This section looks 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 or attributes or both.

The most important class in the System.Xml.Serialization namespace is XmlSerializer. To serialize an object, you first need to instantiate an XmlSerializer object, specifying the type of the object to serialize. Then you 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, you reverse the process in the previous example. You 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, you will want to use the System.Runtime.Serialization.Formatters.Binary namespace.

Some of the other tasks that you can accomplish 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. The xsd.exe tool, which is included with the .NET Framework, can help create these attributes for you. 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 runtime 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#, Visual Basic .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. The following is a simple application that serializes a class. At the beginning of the example, you have very simple code that creates a new Product object, pd, and fills it with some data:

  private void button1_Click(object sender, EventArgs e) {   //new products object   Product pd = new Product();   //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 nine overloads. One of the parameters required is a stream to write the data to. It can be a Stream, TextWriter, or an XmlWriter parameter. In the example, you 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 you use the typeof keyword with the type that is to be serialized. After the sr object is created, you 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(Product)); //serialize object sr.Serialize(tr, pd); tr.Close(); webBrowser1.Navigate(AppDomain.CurrentDomain.BaseDirectory + "//serialprod.xml"); 

Next is the Products class, the class 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 7, “Delegates and Events,” 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()]   public class Product {     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;          //added 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;}     }          [XmlElementAttribute()]     public string ProductName {       get {return prodName;}       set {prodName=value;}     }     [XmlElementAttribute()]     public int SupplierID {       get {return suppId;}       set {suppId=value;}     }          [XmlElementAttribute()]     public int CategoryID {       get {return catId;}       set {catId=value;}     }          [XmlElementAttribute()]     public string QuantityPerUnit {       get {return qtyPerUnit;}       set {qtyPerUnit=value;}     }          [XmlElementAttribute()]     public Decimal UnitPrice {       get {return unitPrice;}       set {unitPrice=value;}     }          [XmlElementAttribute()]     public short UnitsInStock {       get {return unitsInStock;}       set {unitsInStock=value;}     }          [XmlElementAttribute()]     public short UnitsOnOrder {       get {return unitsOnOrder;}       set {unitsOnOrder=value;}     }          [XmlElementAttribute()]     public short ReorderLevel {       get {return reorderLvl;}       set {reorderLvl=value;}     }     [XmlElementAttribute()]     public bool Discontinued {       get {return discont;}       set {discont=value;}     }       } 

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 you take a look at the XML document created during serialization, you will see that it looks like any other XML document that you might have created, which is the point of the exercise. Here’s the document:

  <?xml version="1.0" encoding="utf-8"?> <Products xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance                          xmlns:xsd="http://www.w3.org/2001/XMLSchema" Discount="0">    <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. You could use this any way that you would use an XML document. You could transform it and display it as HTML, load it 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 you’re doing with the second button).

Next, you add another button event handler to deserialize a new Products-based object, newPd. This time you use a FileStream object to read in the XML:

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

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

  //new serializer   XmlSerializer newSr = new XmlSerializer(typeof(Product));   //deserialize the object   newPd = (Product)newSr.Deserialize(f);   f.Close(); } 

What about situations where you have derived classes and possibly properties that return an array? XmlSerializer has that covered as well. Here’s a slightly more complex example that deals with these issues.

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

  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 interest here. If you are to serialize this class, you 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 you would like the element name to be in the XML document that is created during serialization. If you 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 types. Because you are returning different types in the array, you use XmlArrayItemAttribute, which allows the higher level of control.

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

  private void button4_Click(object sender, EventArgs e) {   //create the XmlAttributes boject   XmlAttributes attrs = new XmlAttributes();      //add the types of the objects that will be serialized   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);      //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();   webBrowser1.Navigate(AppDomain.CurrentDomain.BaseDirectory + "//inventory.xml"); } } 

This is what the XML document looks like:

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

The button2_Click() event handler implements deserialization of the Inventory object. Note that you 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 Source Code Access

Well, this all works great, but what if you don’t have access to the source code 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 enable you to accomplish exactly what you have just done, but without adding the attributes. This section looks at an example of how this works.

For this example, imagine that the Inventory, Product, and derived BookProduct classes are in a separate DLL and that you 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;}    } } 

Next, you 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 you 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 you looked at in the previous example, are just a couple of these. You now have an XmlAttributes object with two XmlElementAttribute-based objects added to the XmlElements collection.

The next thing you 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 you created earlier. The other overload, which is the one you are using, also takes a string value that is the member in the overridden object. In this case, you want to override the InventoryItems member in the Inventory class.

When you create the XmlSerializer object, you add the XmlAttributeOverrides object as a parameter. Now the XmlSerializer knows which types you want to override and what you 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 you execute the Serialize() method, you get this XML output:

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

As you can see, you get the same XML as you did with the earlier example. To deserialize this object and recreate the Inventory-based object that you started out with, you need to create all of the same XmlAttributes, XmlElementAttribute, and XmlAttributeOverrides objects that you created when you serialized the object. Once you do that, you can read in the XML and recreate the Inventory object just as you 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(); } 

Note that the first few lines of code are identical to the code you used to serialize the object.

The System.Xml.XmlSerialization namespace provides a very powerful tool set for serializing objects to XML. By serializing and deserializing 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# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

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