串行化是把一个对象存入磁盘的过程。应用程序的另一部分,甚至另一个应用程序都可以反串行化对象,使它的状态与串行化之前相同。.NET Framework为此提供了两种方式。
本节将介绍System.Xml.Serialization命名空间。它包含的类可用于把对象串行化为XML文档或流。这表示对象的公共属性和公共字段将转换为XML元素和/或属性。
System.Xml.Serialization命名空间中最重要的类是XmlSerializer。要串行化对象,首先需要实例化一个XmlSerializer对象,指定要串行化的对象类型,然后实例化一个流/写入器对象,把文件写入流/文档。最后一步是在XmlSerializer上调用Serializer()方法,给它传送流/写入器对象和要串行化的对象。
被串行化的数据可以为基本类型的数据、字段、数组和XmlElements和XmlAttribute对象格式的内嵌XML。
为了从XML文档中反串行化对象,应执行上述过程的逆过程。即创建一个流/读取器对象和一个XmlSerializer对象,然后给DeSerializer()方法传送该流/读取器对象。这个方法返回反串行化的对象,但需要转换为正确的类型。
注意:
XML串行化器不能转换私有数据,只能转换公共数据,它也不能串行化对象图表。
但是,这并不是一个严格的限制。对类进行仔细设计,就很容易避免突破这个限制。如果需要串行化公共数据和私有数据,以及包含许多嵌套对象的对象图形,就可以使用System. Runtime. Serialization. Formatters.Binary命名空间。
使用System.Xml.Serialization类可以进行的其他工作如下所示:
● 确定数据应是一个属性还是元素
● 指定命名空间
● 改变属性或元素名
对象和XML文档之间的链接是给类加上注释的定制C#属性,这些属性可以告诉串行化程序如何写入数据。.NET Framework中有一个工具xsd.exe,它可以帮助我们创建这些属性。xsd.exe可以完成如下任务:
● 从XDR模式文件中生成一个XML模式
● 从XML文件中生成XML模式
● 从XDS模式文件中生成 DataSet类
● 生成运行库类,运行库类包含XmlSerialization的定制属性
● 从已经开发出来的类中生成XSD
● 限制在代码中创建的元素
● 确定生成代码的编程语言(C#、VB.NET或JScript.NET)
● 在编译好的程序集中创建类中的模式
参见Framework文档说明书,了解xsd.exe命令行选项的详细内容。
尽管xsd.exe具备这些功能,但不一定用它为串行化创建类。这个过程是很简单的。下面介绍一个简单的应用程序,它串行化一个类,读取在本章前面保存的产品数据。该代码在SerialSample1文件夹中。示例代码的起始部分比较简单,创建一个新的Product对象pd,并给它填充一些数据:
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.ProductFTEL="Serialize Objects";
pd.QuantityPerUnit="6";
pd.ReorderLevel=1;
pd.SupplierID=1;
pd.UnitPrice=1000;
pd.UnitsInStock=10;
pd.UnitsOnOrder=0;
XmlSerializer类的Serialize方法实际执行串行化,它有6个重载方法。第一个重载方法的参数是要写入数据的流,可以是Stream、TextWriter或XmlWriter。在本例中,创建了一个基于TextWriter的对象tr。接着创建了基于XmlSerializer的对象sr。XmlSerializer需要知道要串行化的对象的类型信息,所以对要串行化的类型使用typeof关键字。在创建sr对象后,调用Serialize方法,其参数是tr(基于Stream的对象)和要串行化的对象,在本例中是pd。确保完成后关闭该数据流。
//new TextWriter and XmlSerializer
TextWriter tr=new StreamWriter("..\\..\\..\\serialprod.xml");
XmlSerializer sr=new XmlSerializer(typeof(Products));
//serialize object
sr.Serialize(tr,pd);
tr.Close();
}
下面介绍Products类,即要串行化的类。这个类与以前编写的其他类的惟一区别是给它增加了C#属性。这些属性中的XmlRootAttribute 和 XmlElementAttribute类由System.Attribute类继承而来。不要把这些属性与XML文档中的属性相混淆。C#属性仅是一些声明信息,在运行期间可以由CLR获取(详见第6章)。在本例中,添加一些描述如何串行化对象的属性:
//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;
}
在Products类定义前面的属性中调用的XmlRootAttribute()把这个类标识为根元素(在XML文件中,由串行化生成的)。包含XmlElementAttribute()的属性把该属性下面的成员看做为表示一个XML元素。
查看一下刚才在串行化过程中创建的XML文档,就会发现它与前面创建的其他XML文档非常类似。这就是本练习的目的。下面就是这个文档:
<?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>
这里没有任何不寻常的地方。可以以XML文档的任何方式来使用这个文档。可以对它进行转换,以HTML格式显示它,使用ADO.NET将其加载到DataSet中,用它加载XmlDocument,或者像在该示例中那样,对它进行反串行化,创建一个对象,该对象的状态与串行化前pd的状态一样(这就是第二个按钮的作用)。
接着添加另一个按钮事件处理程序,反串行化一个基于Products的新对象newPd。这次使用FileStream对象读取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);
再传入Product的类型信息,创建一个新XmlSerializer。然后就可以调用Deserialize()方法。注意在创建newPd对象时,仍需要进行显式的类型转换。此时newPd与pd的状态完全一样:
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();
}
这个示例比较简单。下面介绍一个使用XmlSerializer类的略微复杂的示例。这个示例使每个字段变成private,只能通过Product类中的get和set方法来访问,再给XML文件添加一个Discount属性,以说明属性是如何串行化的。
该示例在SerialSample2中,下面是新的Products类:
[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(AttributeFTEL="Discount")]
public int Discount
{
get {return disc;}
set
}
[XmlElementAttribute()]
public int ProductID
{
get {return prodId;}
set
}
...
// properties for most of the fields are not shown for sake of brevity
...
[XmlElementAttribute()]
public bool Discontinued
{
get {return discont;}
set
}
}
还需要对按钮单击事件的处理程序作如下修改:
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.ProductFTEL="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();
}
运行这段代码,得到的结果与前面一样,但有一个区别。输出结果(serialprod1.xml)如下所示:
<?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>
注意Products元素上的Discount属性。既然已经定义了该属性的访问器,就可以在属性中添加更复杂的有效性验证代码。
如果有派生的类和可能返回一个数组的属性,也可以使用XmlSerializer。下面介绍一个解决这些问题的复杂示例。
首先定义3个类Product、BookProduct (派生于Product)和Inventory (包含另外两个类):
public class Product
{
private int prodId;
private string prodName;
private int suppId;
public Product() {}
public int ProductID
{
get {return prodId;}
set
}
public string ProductName
{
get {return prodName;}
set
}
public int SupplierID
{
get {return suppId;}
set
}
}
public class BookProduct : Product
{
private string isbnNum;
public BookProduct() {}
public string ISBN
{
get {return isbnNum;}
set
}
}
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
}
}
在此我们只对Inventory类感兴趣。如果串行化这个类,就需要插入一个属性,该属性为每个要添加到数组中的类型包含一个XmlArrayItem构造函数。注意,XmlArrayItem是由XmlArrayItemAttribute类表示的.NET属性名。
这些构造函数的第一个参数是在串行化过程中创建的XML文档中的元素名。如果不使用ElementName参数,元素的名称就会与对象类型名相同(在本例中,就是Product 和 BookProduct)。必须指定的第二个参数是对象的类型。
如果属性返回一个对象数组或基本类型的数组,还要使用XmlArrayAttribute类。因为要在数组中返回不同的类型,所以使用XmlArrayItemAttribute,它允许进行更高级别的控制。
在button1_Click事件处理程序中,创建一个新的Product对象和一个新的BookProduct对象(newProd 和 newBook)。给每个对象的各种属性添加数据,再把这些对象添加到一个Product数组中。把这个数组作为参数,创建一个新的Inventory对象, 然后串行化Inventory对象,以便在以后重新创建它:
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.ProductFTEL="Product Thing";
newProd.SupplierID=10;
newBook.ProductID=101;
newBook.ProductFTEL="How to Use Your New Product Thing";
newBook.SupplierID=10;
newBook.ISBN="123456789";
//add the items to an array
Product[] addProd=;
//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();
}
下面是XML文档的内容(其代码包含在SerialSample3文件夹中):
<?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>
注意button2_Click事件处理程序执行Inventory对象的反串行化。注意在新建的newInv对象中,我们迭代了数组,以说明其数据保持不变:
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();
}
这些代码都很好地发挥了作用,但如果不能访问已经串行化的类型的源代码,该怎么办?如果没有源代码,就不能添加属性。此时可以采用另一种方式。可以使用XmlAttributes类和XmlAttributeOverrides类,这两个类可以完成刚才的任务,但不需要添加属性。下面的代码说明了这两个类的工作方式,这段代码在SerialSample4文件夹中。
对于这个示例,假定Inventory、Product和派生的BookProduct类在一个单独的DLL中,而且没有源代码。Product和BookProduct类与前面的示例相同,但应注意Inventory类中没有添加属性:
public class Inventory
{
private Product[] stuff;
public Inventory() {}
public Product[] InventoryItems
{
get {return stuff;}
set
}
}
下面处理button1_Click()事件处理程序中的串行化:
private void button1_Click(object sender, System.EventArgs e)
{
串行化过程的第一步是创建一个XmlAttributes对象,为每个要重写的数据类型创建一个XmlElementAttribute对象:
XmlAttributes attrs=new XmlAttributes();
attrs.XmlElements.Add(new XmlElementAttribute("Book",typeof(BookProduct)));
attrs.XmlElements.Add(new XmlElementAttribute("Product",typeof(Product)));
从中可以看出,我们给XmlAttributes类的 XmlElements集合添加了一个新的XmlElementAttribute。XmlAttributes类的属性对应于可以应用的属性,前面示例中的XmlArray和 XmlArrayItems仅是其中的几个属性而已。现在我们有一个XmlAttributes对象,并在XmlElements集合中添加了两个基于XmlElementAttribute的对象。
接着创建XmlAttributeOverrides对象:
XmlAttributeOverrides attrOver=new XmlAttributeOverrides();
attrOver.Add(typeof(Inventory),"InventoryItems",attrs);
这个类的Add方法有两个重载方法。第一个重载方法的参数是要重写的对象的类型信息和前面创建的XmlAttributes对象,本例中使用了第二个重载方法,其参数也是一个字符串值,该字符串是重写对象的成员。在本例中,要重写Inventory类中的InventoryItems成员。
下面把添加的XmlAttributeOverrides对象作为参数,创建XmlSerializer对象。现在XmlSerializer知道我们要重写的类和需要为这些类型返回的内容。
//create the Product and Book objects
Product newProd=new Product();
BookProduct newBook=new BookProduct();
newProd.ProductID=100;
newProd.ProductFTEL="Product Thing";
newProd.SupplierID=10;
newBook.ProductID=101;
newBook.ProductFTEL="How to Use Your New Product Thing";
newBook.SupplierID=10;
newBook.ISBN="123456789";
Product[] addProd=;
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();
}
如果执行Serialize方法,将得到如下XML输出:
<?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>
可以看出,得到的XML与前面的示例完全相同。为了反串行化对象,重新创建基于Inventory的对象,需要创建在串行化对象时创建的XmlAttributes、XmlElementAttribute 和 XmlAttribute Overrides对象。之后就可以读取XML,像以前那样重新创建Inventory对象了。下面的代码反串行化Inventory对象:
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();
}
注意,前几行代码与串行化对象所用的代码相同。
System.Xml.XmlSerialize命名空间提供了一个功能非常强大的工具集,可以把对象串行化到XML中。把对象串行化和反串行化到XML中替代了把对象保存为二进制格式,因此可以通过XML对对象进行其他处理。这将大大增强设计的灵活性。