Книга: C# для профессионалов. Том II

Сериализация объектов в XML

Сериализация объектов в XML

Сериализация является процессом сохранения объекта на диске. Другая часть приложения или даже другое приложение могут десериализовать объект и он будет в том же состоянии, в каком он был до сериализации. Платформа .NET содержит два способа выполнения сериализации. Рассмотрим пространство имен System.Xml.Serialization.

Как показывает имя, сериализация производится в формате XML. Это делается преобразованием открытых свойств объекта и открытых полей в элементы и/или атрибуты. Сериализатор XML не может преобразовать скрытые данные, а только открытые. Представляйте это как способ сохранения состояния объекта. Если необходимо сохранить скрытые данные, то используйте BinaryFormatter в пространстве имен System.Runtime.Serialization.Formatters.Binary. Можно также:

? Определить, должны ли данные быть атрибутом или элементом.

? Определить пространство имен.

? Изменить имя атрибута или элемента.

Вместе с возможностью сериализовать только открытые данные, невозможно сериализовать графы объектов (объекты, которые достижимы из сериализуемого объекта). Это не является серьезным ограничением. При тщательном проектировании классов этого легко можно избежать. Если необходимо иметь возможность сериализовать открытые и скрытые данные, а также граф объектов, содержащий множество вложенных объектов, то можно будет воспользоваться пространством имен System.Runtime.Serialization.Formatters.Binary.

Данные для сериализации могут быть примитивными типами данных, полями, массивами и XML, встроенным в форму объектов XmlElement и XmlAttribute. Связью между объектом и документом XML являются специальные атрибуты, которые аннотируют классы. Эти атрибуты используются для того, чтобы информировать сериализатор, как записать данные.

На платформе .NET существует инструмент, помогающий создавать атрибуты,— это утилита xsd.exe, которая может делать следующее:

? Генерировать схему XML из файла схемы XDR

? Генерировать схему XML из файла XML

? Генерировать классы DataSet из файла схемы XSD

? Генерировать классы времени выполнения, которые имеют специальные атрибуты для XmlSerilization

? Генерировать XSD из классов, которые уже были разработаны

? Ограничивать список элементов, которые создаются в коде

? Определять, на каком языке программирования должен быть представлен генерируемый код (C#, VB.NET, или JScript.NET)

? Создавать схемы из типов в компилированных сборках

В документации платформы можно найти подробное описание параметров командной строки.

Несмотря на предлагаемые возможности, вовсе не обязательно использовать xsd.exe, чтобы создать классы для сериализации. Рассмотрим простое приложение, которое сериализует класс, считывающий данные о продуктах, сохраненных ранее в этой главе (пример находится в папке SerialSample1). В начале идет очень простой код, который создает новый объект Product, pd, и записывает некоторые данные:

private void button1_Click(object sender, System.EventArgs e) {
 // новый объект Products
 Products pd=new Products();
 // задать некоторые свойства
 pd.ProductXD=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;

Метод Serialize класса XmlSerializer имеет шесть перегружаемых версий. Одним из требуемых параметров является поток для записи в него данных. Это может быть Stream, TextWriter или XmlWriter. В данном случае мы создали объект tr на основе TextWriter. Затем необходимо создать объект sr на основе XmlSerializer. XmlSerializer должен знать информацию о типе сериализуемого объекта, поэтому используется ключевое слово typeof с указанием типа, который должен быть сериализован. После создания объекта sr вызывается метод Serialize, в который передается tr (объект на основе Stream) и объект, который необходимо сериализовать, в данном случае pd. Не забудьте закрыть поток, когда закончите с ним работу.

 //новый TextWriter и XmlSerializer
 TextWriter tr=new StreamWriter("......serialprod.xml");
 XmlSerializer sr=new XmlSerializer(typeof(Products));
 // сериализуем объект
 sr.Serialize(tr,pd);
 tr.Close();
}

Здесь мы добавляем событие другой кнопки для создания нового объекта newPd на основе Products. В этот раз мы будем использовать объект FileStream для чтения XML:

private void button2_Click(object sender, System.EventArgs e) {
 // создаем ссылку на тип Products Products newPd;
 // новый файловый поток для открытия сериализованного объекта
 FileStream f=new FileStream("......serialprod.xml", FileMode.Open);

Здесь создается новый объект XmlSerializer, таким образом передается информация о типе Product. Затем можно вызвать метод Deserialize. Отметим, что нам по-прежнему необходимо делать явное преобразование типа, когда создается объект newPd. В этом месте newPd имеет такое же состояние, как и pd:

 // новый Serializer
 XmlSerializer newSr=new XmlSerializer(typeof(Products));
 // десериализация объекта
 newPd=(Products)newSr.Deserialize(f);
 // загружаем его в окно списка.
 listBox1.Items.Add(newPd.ProductName);
 f.Closed();
}

Теперь мы проверим класс Products. Единственное различие между ним и любым другим классом, который можно записать, состоит в добавленных атрибутах. Не путайте эти атрибуты с атрибутами в документе XML. Эти атрибуты расширяют класс SystemAttribute. Атрибут является некоторой декларативной информацией, которая может извлекаться во время выполнения с помощью CLR (см. в главе 6 более подробно). В данном случае добавляются атрибуты, которые описывают, как объект должен быть сериализован:

//класс, который будет сериализован,
//атрибуты определяют, как объект сериализуется.
[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;
}

Созданный документ XML выглядит, как любой другой документ XML, который мы могли бы создать.

<?xml version="1.0" ?>
<Products xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <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, загрузить его в DataSet с помощью ADO.NET, загрузить с его помощью XmlDocument, как в примере, десериализовать его и создать объект в том же состоянии, которое имел pd перед своей сериализацией (что соответствует событию второй кнопки).

Рассмотренный только что пример является очень простым. Обычно имеется ряд методов получения (get) и задания (set) свойств для работы с данными в объекте. Но что, если объект состоит из двух других объектов, или выводится из базового класса, из которого следуют и другие классы?

Такие ситуации обрабатываются с помощью класса XmlSerializer. Давайте усложним пример (находится в SerialSample2). Для большей реалистичности сделаем каждое свойство доступным через методы get и set:

private void button1_Click(object sender, System.EventArgs e) {
 // новый объект products
 Products pd=new Products();
 // задать некоторые свойства
 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;
 //новые TextWriter и XmlSerializer
 TextWriter tr=new StreamWriter("......serialprod1.xml");
 XmlSerializer sr=new XmlSerializer(typeof(Products));
 // сериализируем объект
 sr.Serialize(tr, pd);
 tr.Close();
}
private void button2_Click(object sender, System.EventArgs e) {
 //создать ссылку на тип Products
 Products newPd;
 // новый файловый поток для открытия сериализуемого объекта
 FileStream f=new FileStream("......serialprod1.xml", FileMode.Open);
 // новый сериализатор
 XmlSerializer newSr=new XmlSerializer(typeof(Products));
 //десериализуем объект
 newPd=(Products)newSr.Deserialize(f);
 //загрузить его в окно списка.
 listBox1.Items.Add(newPd.ProductName);
 f.Close();
}
//класс, который будет сериализован.
//атрибуты определяют, как объект сериализуется
[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;
 // добавлен атрибут Discount
 [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 unitsOrOrder;}
  set {unitsOnOrder=value;}
 }
 [XmlElementAttribute()]
 public short ReorderLevel {
  get {return reorderLvl;}
  set {reorderLvl=value;}
 }
 [XmlElementAttribute()]
 public pool Discontinued {
  get {return discont;}
  set {discont=value;}
 }
}

Выполнение этого кода вместо класса Products в предыдущем примере даст те же самые результаты с одним исключением. Мы добавили атрибут Discount, тем самым показав, что атрибуты также могут быть сериализуемыми. Вывод выглядит следующим образом (serialprod1.xml):

<?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="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>

Отметим атрибут Discount элемента Products. Поэтому теперь, когда определены средства задания и получения свойств, можно добавить более сложную проверку кода в свойствах.

Что можно сказать о ситуации, когда имеются производные классы и, возможно, свойства, которые возвращают массив? XmlSerializer также это охватывает. Давайте обсудим более сложную ситуацию.

В событии button1_Click создается новый объект на основе Product и новый объект на основе BookProduct(newProd и newBook). Мы добавляем данные в различные свойства каждого объекта и помещаем объекты в массив на основе Product. Затем создается новый объект на основе Inventory, которому в качестве параметра передается массив. Затем можно сериализовать объект Inventory, чтобы впоследствии его восстановить:

private void button1_Click(object sender, System.EventArgs e) {
 // создать новые объекты книги и книжной продукции
 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 с помощью массива addProd
 Inventory inv=new Inventory();
 inv.InventoryItems=addProd;
 // сериализуем объект Inventory
 TextWriter tr=new StreamWriter("......order.xml");
 XmlSerializer sr=new XmlSerializer(typeof(Inventory));
 sr.Serialize(tr, inv);
 tr.Close();
}

Отметим в событии button2_Click, что мы просматриваем массив во вновь созданном объекте 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.Inventory Items) listBox1.Items.Add(prod.ProductName);
 f.Close();
}
public class inventory {
 private Product[] stuff;
 public Inventory() {}

Мы имеем XmlArrayItem для каждого типа, который может быть добавлен в массив. Первый параметр определяет имя элемента в создаваемом документе XML. Если опустить этот параметр ElementName, то элементы получат имя типа объекта (в данном случае Product и BookProduct). Существует также класс XmlArrayAttribute, который будет использоваться, если свойство возвращает массив объектов или примитивных типов. Так как мы возвращаем в массиве различные типы, то используется объект XmlArrayItemAttribute, который предоставляет более высокий уровень управления:

 // необходимо иметь запись атрибута для каждого типа данных
 [XmlArrayItem("Prod", typeof(Product)), XmlArrayItem("Book", typeof(BookProduct))]
 //public Inventory(Product [] InventoryItems) {
 // stuff=InventoryItems;
 //}
 public Product[] InventoryItems {
  get {return stuff;}
  set {stuff=value;}
 }
}
//класс Product
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;}
 }
}
// Класс Bookproduct
public class BookProduct: Product {
 private string isbnNum;
 public BookProduct() {}
 public string ISBN {
  get {return isbnNum;}
  set {isbnNum=value;}
 }
}

В этот пример добавлено два новых класса. Класс Inventory будет отслеживать то, что добавляется на склад. Можно добавлять продукцию на основе класса Product или класса BookProduct, который расширяет Product. В классе Inventory содержится массив добавленных объектов и в нем могут находиться как BookProducts, так и Products. Вот как выглядит документ XML:

<?xml version="1.0" ?>
<Inventory xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <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>

Все это работает прекрасно, но как быть в ситуации, когда нет доступа к исходному коду типов, которые будут сериализироваться? Невозможно добавить атрибут, если отсутствует исходный код. Существует другой способ. Можно воспользоваться классами XmlAttributes и XmlAtrtributeOverrides. Вместе эти классы позволят выполнить в точности то, что только что было сделано, но без добавления атрибутов. Вот пример, находящийся в папке SerialSample4:

private void button1_Click(object sender, System.EventArgs e) {
 // создать объект XmlAttributes XmlAttributes attrs=new XmlAttributes();
 // добавить типы объектов, которые будут сериализированы
 attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));
 attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));
 XmlAttributeOverrides attrOver=new XmlAttributeOverrides();
 //добавить к коллекций атрибутов
 attrOver.Add(typeof(Inventory), "InventoryItems", attrs);
 // создать объекты Product и Book
 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};
 //Product[] addProd={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();
}
private void button2_Click(object sender, System.EventArgs e) {
 //необходимо выполнить тот же процесс для десериализации
 // создаем новую коллекцию XmlAttributes
 XmlAttributes attrs=new XmlAttributes();
 // добавляем информацию о типе к коллекции элементов
 attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));
 attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));
 XmlAttributeOverrides attrOver=new XmlAttributeOverrides();
 //добавляем к коллекции Attributes (атрибутов)
 attrOver.Add(typeof(Inventory), "InventoryItems", attrs);
 //нужен новый объект Inventory для десериализаций в него
 Inventory newInv;
 // десериализуем и загружаем данные в окно списка из
 // десериализованного объекта
 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();
}
// это те же классы, что и в предыдущем примере
// за исключением удаленных атрибутов
// из свойства InventoryItems для Inventory
public class Inventory {
 private Product[] stuff;
 public Inventory() {}
 public Product[] InventoryItems {
  get {return stuff;}
  set {stuff=value;}
 }
}
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;}
 }
}

Это тот же пример, что и раньше, но первое, что необходимо заметить,— здесь нет добавленных в класс Inventory атрибутов. Поэтому в данном случае представьте, что классы Inventory, Product и производный класс BookProduct находятся в отдельной DLL, и у нас нет исходного кода.

Первым шагом в процессе является создание объекта на основе XmlAttributes, и объекта XmlElementAttribute для каждого типа данных, который будет переопределяться:

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

Здесь мы добавляем новый XmlElementAttribute к коллекции XmlElements класса XmlAttributes. Класс XmlAttributes имеет свойства, соответствующие атрибутам, которые могут применяться; XmlArray и XmlArrayItems, которые мы видели в предыдущем примере, являются только парой. Теперь мы имеем объект XmlAttributes с двумя объектами на основе XmlElementAttribute, добавленными к коллекции XmlElements. Далее создадим объект XmlAttributeOverrides:

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

Meтод Add имеет две перегружаемые версии. Первая получает информацию о типе переопределяемого объекта и объект XmlAttributes, который был создан ранее. Вторая версия та, что мы используем, получает также с строковое значение, которое является членом в переопределенном объекте. В нашем случае мы хотим переопределить член InventoryItems в классе Inventory.

Теперь создадим объект XmlSerializer, добавляя объект XmlAttributeOverridesв качестве параметра. XmlSerializer уже знает, какие типы мы хотим переопределить и что нам нужно вернуть для этих типов. Если выполнить метод Serialize, то получится следующий вывод XML:

<?xml version="1.0"?>
<Inventory xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
 <Products>
  <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 и XmlAttributeOverrides, которые создаются при сериализации объекта. Когда это будет сделано, можно прочитать XML и воссоздать объект Inventory, как это делалось раньше. Вот код для десериализации объекта Inventory:

private void button2_Click(object sender, System.EventArgs e) {
 XmlAttributes attrs=new XmlAttributes();
 attrs.XmlElements.Add(new XmlElementAttribute("Book", typeof(BookProduct)));
 attrs.XmlElements.Add(new XmlElementAttribute("Product", typeof(Product)));
 XmlAttributeOverrides attrOver=new XmlAttributeOverrides();
 attrOver.Add(typeof(Inventory), "InventoryItems", attrs);
 Inventory newInv;
 FileStream f=new FileStream("......inventory.xml", FileMode.Open);
 XmlSerializer newSr=new XmlSerializer(typeof(Inventory), attrOver);
 newInv=(Inventory)newSr.Deserialize(f);
 foreach(Product prod, in newInv.InventoryItems) listBox1.items.Add(prod.ProductName);
 f.Close();
}

Отметим, что первые несколько строк кода идентичны коду, который использовался для сериализации объекта.

Пространство имен XmlSerialize предоставляет мощный набор инструментов. Сериализуя объекты в XML, а не в двоичный формат, мы получаем дополнительные возможности, что может действительно увеличить гибкость проектирования.

Оглавление книги


Генерация: 1.458. Запросов К БД/Cache: 3 / 0
поделиться
Вверх Вниз