65.9K
CodeProject 正在变化。 阅读更多。
Home

XML 数据文件、XML 序列化和 .NET

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (25投票s)

2003年8月28日

17分钟阅读

viewsIcon

345205

downloadIcon

6482

介绍了一种使用 XML Schema 和 xsd.exe 来构建 XML 数据文件的方法,以便于 XML 序列化。

CardfileSerializationDemo application image

Screenshot for PocketPC

引言

注意:我最近添加了此应用程序的 PocketPC 版本。

我正在阅读 Manster 关于 C# 个人组织者的文章 A C# Personal Organizer,该组织者的数据文件以 XML 格式存储。我实际上一直在为自己编写类似的东西, loosely based upon the old Windows Cardfile application。由于我刚收到一台新的 PocketPC,并想为它编写一些代码,所以我打算在我的桌面上建模一个应用程序,将一些数据保存到 XML 文件中,然后将数据传输到 PocketPC 和从 PocketPC 传输回来。我很好奇 Manster 是如何实现他的应用程序的,特别是当 XML 数据文件涉及时——也许他包含了我应该在他的应用程序中设计的某些内容。

实际上,我们的应用程序有些不同。我想存储的不仅仅是联系人信息,还有图像和我的个人笔记。对我来说,这意味着我有三种不同类型的卡片可以存储在我的卡片组中。但我也注意到,他采取了一种不同的方法来实际将他的数据转换为 XML 并将其读回。他使用 XmlTextWriter 手动创建 XML,实际上,这正是 PocketPC 需要做的。但在桌面端,我已经在其他几个应用程序中有效使用过一种替代方法——XML 序列化。

XML 序列化是 .NET 实现的一个过程,它可以轻松地将对象的公共实例数据转换为 XML 并将其读回。最终,您可能会认为它并不比自己编写 XML 更容易使用,但 XML 序列化确实有一些优点,我稍后将详细介绍。XML 序列化的核心是 .NET 用于通过网络传输数据的 Web 服务基础结构。考虑这一点很重要,因为这种序列化实现与 System.Runtime.Serialization 中发现的远程处理序列化程序不同,并且是独立的。我可能会将这些类称为“远程处理序列化”。不,您会在 System.Xml.Serialization 中找到 Web 服务 XML 序列化类。我将这些类称为“XML 序列化”本身。(我只能推测为什么 Framework 中存在两种不同的 XML 序列化实现,但实际上确实存在!)在这篇文章中,我实际上只介绍 Web 服务 XML 序列化。远程处理 XML 序列化需要另一篇文章,因为使用机制在某种程度上是不同的。

XML 序列化如何工作

XML 序列化非常易于使用,但我必须承认有时很难调试。我发现异常消息文本并不特别有用,让我只能猜测并重试。即便如此,任何 .NET 对象的几乎所有 public 属性都会自动序列化为 XML。XML 标签名由属性名生成,除非您另行指定。对象数组也会自动处理,即使数组是复杂类型的。数组元素数据像任何其他 .NET 对象(public 属性)一样进行序列化。

当然,.NET “知道”public 属性,并且知道它们是基于数组的还是非基于数组的,这取决于与类型关联的元数据。如果您创建一个具有名为“Name”的 public string 属性的 .NET 类,则可以使用类来枚举所有 public 类属性,并提供有关属性的信息,例如其名称。然后,序列化程序大致遵循这个简单的模型

(写入)

Stream XML document element opening tag in the form <type_name>
For each public property implemented by the class
  Read the property name
  Read the property value
  Stream XML in the form <name>value</name>
End for
Stream XML document element closing tag in the form </type_name>

(读取)

Create an instance of the type encoded within the XML
For each XML node within the document element
  Read the node name and value
  Assign the named property the value previously read
End for

当然,在此过程中可能会出现错误。XML 可能与您指定的对象类型不匹配,或者可能存在一般的 XML 错误。也可能存在无法序列化的 public object 属性实例,例如基于 IDictionary(如哈希表)的属性。(请注意,这对于 Framework 的 1.0 和 1.1 版本都是如此……未来版本可能会序列化 IDictionary 基于的属性。)

您还会在我的代码中看到我没有使用 [Serializable] 属性。此属性用于远程处理序列化,而对于纯 XML 序列化则不是必需的。对于 ISerializable 接口也是如此。

设计数据文件

当涉及到 XML 数据文件时,我们可以简单地编写一些 C# 或 VB 代码,添加一些 public 属性,然后让序列化程序处理细节。在许多情况下,这是可以的。但是,我更喜欢设计表示我数据的 XML,然后从该 XML 生成 C# 源文件。碰巧,这也是可能的,并且正是这种设计和实现过程,我将在其余的文章中重点介绍。而且,虽然我会在本节中引用一个具体的例子,但基本模式适用于任何 XML 数据文件。

当我设计 XML 数据文件时,我经常创建一个 XML 示例数据文件,然后从中创建一个模式。然后,我在 XML 和模式之间进行迭代,直到我拥有一个我喜欢的 XML 数据文件格式和一个可用于验证的代表性 XML 模式。这个过程与 XML 序列化配合得很好,因为我使用了一个随 .NET Framework 一起提供的实用程序,名为 xsd.exexsd.exe 以 XML 模式为输入,并将生成 C# 或 VB 源文件,这些文件在序列化时会生成模式中概述的确切 XML。如果我稍后更改模式,我只需再次运行 xsd.exe,就会重新生成匹配的源文件。

为了说明,让我们以我的卡片文件应用程序为例。使用某个应用程序,我们可以创建“卡片”,这些卡片可以模拟物理的索引卡。我们有三种类型的卡片——用于笔记的简单文本、简单的图像以及用于联系人信息的专用卡片。卡片被收集到一个名为“deck”的集合中。所以单个 XML 文件将代表一个卡组,卡组包含与该卡组相关的所有卡片。

一些小的复杂性在于,我想将属性与卡组关联起来,就像 Microsoft Word 将属性与文档关联起来一样。我还想将任何图像数据直接编码到 XML 流中。原因很简单,就是避免插入对图像的引用(如文件名或 URL),并且不必记得将图像与 XML 一起复制到我的 PocketPC 设备上。我希望 XML 数据文件是自包含的,即使可能很大。

我还想为卡片组指定应用程序版本,以便应用程序的未来版本可能需要更新的数据文件。或者更具体地说,应用程序的旧版本无法读取为新版本准备的数据文件,如果应用了大量的数据文件格式更改。简而言之,卡片组将有一个与它关联的版本号,我在加载数据文件时会检查它。如果版本不是我当时可以处理的版本,我将终止加载操作。

在“内务管理”方面,我知道我需要某种方法来标识单个卡片,所以我选择了一个简单的整数作为卡片标识符。但由于您应该能够任意添加和删除卡片,因此我需要某种方式来跟踪最后使用的卡片 ID。此信息也必须序列化,以便当卡组加载到应用程序中时,新卡片的添加将具有正确且唯一的 ID 值。

我提出的基本 XML 如下所示:

<Cards>
  <NextID/>
  <Version/>
  <Props>
    <Name/>
    <Author/>
    <Comments/>
  </Props>
  <Card>
    <Header>
      <Name/>
      <ID/>
      <Type/>
      <Created/>
      <Updated/>
    </Header>
    <Body>
      {item}
    </Body>
  </Card>
  <Card/>
  <Card/>
</Cards>

元素 <NextID/><Version/> 是简单类型。<Props/> 是复杂类型,但每个卡组只能有一个属性元素。<Card/> 也是复杂类型,但可以有一个或多个(从零个到多个)。

每张卡片都有标题和正文。标题包含卡片的名称、卡片的 ID、类型以及创建和更新日期/时间戳。正文包含卡片信息。也就是说,“item”可以是 stringimagecontact。我们将通过检查标题或第一个子元素的标签名来知道它的类型。数据类型信息看起来是冗余的,但它存储在标题中以方便仅处理标题,例如在排序或搜索时。这样,我就可以按字母顺序对所有联系人进行排序,而无需打开卡片正文来查看卡片实际上是否是 contact

卡片数据本身将只是一个单独的文本节点(note

<Note/>

一个 Base64 编码的节点(image

<Image/>

或者一个 contact 元素

<Contact>
  <FName/>
  <MName/>
  <LName/>
  <Addr1/>
  <Addr2/>
  <Addr3/>
  <City/>
  <State/>
  <PCode/>
  <Country/>
  <Company/>
  <HomePh/>
  <MobilePh/>
  <WorkPh/>
  <FaxPh/>
  <EMail/>
  <Notes/>
</Contact>

相应的模式如图所示

<?xml version="1.0" encoding="utf-8" ?>
<xs:schema id="Cardfile" targetNamespace="http://tempuri.org/Cardfile.xsd"
elementFormDefault="qualified" xmlns="http://tempuri.org/Cardfile.xsd"
xmlns:mstns="http://tempuri.org/Cardfile.xsd"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:complexType name="PropType">
    <xs:sequence>
      <xs:element name="Name" type="xs:string" />
      <xs:element name="Author" type="xs:string" />
      <xs:element name="Comments" type="xs:string" />
    </xs:sequence>
  </xs:complexType>
  <xs:complexType name="CardType">
    <xs:sequence>
      <xs:element name="Header">
        <xs:complexType>
          <xs:sequence>
            <xs:element name="Name" type="xs:string" />
            <xs:element name="ID" type="xs:nonNegativeInteger"/>
            <xs:element name="Type">
              <xs:simpleType>
                <xs:restriction base="xs:string">
                  <xs:enumeration value="Note" />
                  <xs:enumeration value="Contact" />
                  <xs:enumeration value="Image" />
                </xs:restriction>
              </xs:simpleType>
            </xs:element>
            <xs:element name="Created" type="xs:dateTime" />
            <xs:element name="Updated" type="xs:dateTime" />
          </xs:sequence>
        </xs:complexType>
      </xs:element>
      <xs:element name="Body">
        <xs:complexType>
          <xs:choice>
            <xs:element name="Image" type="xs:base64Binary" />
            <xs:element name="Note" type="xs:string" />
            <xs:element name="Contact">
              <xs:complexType>
                <xs:sequence>
                  <xs:element name="FName" type="xs:string" />
                  <xs:element name="MName" type="xs:string" />
                  <xs:element name="LName" type="xs:string" />
                  <xs:element name="Addr1" type="xs:string" />
                  <xs:element name="Addr2" type="xs:string" />
                  <xs:element name="Addr3" type="xs:string" />
                  <xs:element name="City" type="xs:string" />
                  <xs:element name="State" type="xs:string" />
                  <xs:element name="PCode" type="xs:string" />
                  <xs:element name="Country "type="xs:string" />
                  <xs:element name="Company "type="xs:string" />
                  <xs:element name="HomePh" type="xs:string" />
                  <xs:element name="MobilePh" type="xs:string" />
                  <xs:element name="WorkPh" type="xs:string" />
                  <xs:element name="FaxPh" type="xs:string" />
                  <xs:element name="EMail" type="xs:string" />
                  <xs:element name="Notes" type="xs:string" />
                </xs:sequence>
              </xs:complexType>
            </xs:element>
          </xs:choice>
        </xs:complexType>
      </xs:element>
    </xs:sequence>
  </xs:complexType>
  <xs:element name="Cards">
    <xs:complexType>
      <xs:sequence>
        <xs:element type="PropType" name="Props" minOccurs="1" 
            maxOccurs="1" />
        <xs:element name="NextID" type="xs:nonNegativeInteger" />
        <xs:element name="Version" type="xs:string" />
        <xs:element type="CardType" name="Card" minOccurs="0" 
            maxOccurs="unbounded" />
      </xs:sequence>
    </xs:complexType>
  </xs:element>
</xs:schema>

创建源文件

我可以创建基本 XML 文件,然后使用 xsd.exe 为我生成模式,但我个人不喜欢 xsd.exe 生成的模式格式,所以我手工创建模式。记住一点,模式将指导 xsd.exe 在创建源文件时,所以了解 XML 模式在某种程度上很重要。例如,考虑这个模式片段

<xs:complexType name="PropType">
  <xs:sequence>
    <xs:element name="Name" type="xs:string" />
    <xs:element name="Author" type="xs:string" />
    <xs:element name="Comments" type="xs:string" />
  </xs:sequence>
</xs:complexType>
<xs:element type="PropType" name="Props" minOccurs="1"

这将完全转化为 C# 代码

public class PropType
{
  public string Name;
  public string Author;
  public string Comments
}
public PropType Props;

卡片元素更复杂一些,因为我告诉 xsd.exe 实现多态选择

<xs:element name="Body">
  <xs:complexType>
    <xs:choice>
      <xs:element name="Image" type="xs:base64Binary" />
      <xs:element name="Note" type="xs:string" />
      <xs:element name="Contact">...</xs:element>
    </xs:choice>
  </xs:complexType>
</xs:element name="Body">

这个元素会转化为什么?嗯,在代码中,我们说“body”可以包含一个 string、一个与 Base64 相关的项,以及一些表示联系人信息的复杂元素。多态地表示这一点的方法是创建一个与 body 关联的 public 属性,其类型为 object。由于所有 .NET 类型都以 object 为基类,因此我们可以将任何类型的数据与 body 对象关联起来。我们可以使用标题的类型枚举来将其拉回。这被称为“弱类型”,通常好的设计会避免它。在这种情况下,如果我可以使用模式中的 <xs:union/> 元素,我就可以避免它,但不幸的是 xsd.exe 不处理联合。在这种情况下,我认为弱类型是合理的,因为我们只是标记了序列化数据的类型。

因此,这对应的 C# 源代码将是

public class Body
{
  public object Item;
}

但请注意,我们在这里丢失了信息。xsd.exe 会为我们生成此源代码,但 XML 序列化程序如何知道 datatypeItem”真正代表什么?答案是通过 xsd.exe 也注入到源代码中的属性

[System.Xml.Serialization.XmlElementAttribute("Image", typeof(System.Byte[]),
  DataType="base64Binary")]
[System.Xml.Serialization.XmlElementAttribute("Note", typeof(string))]
[System.Xml.Serialization.XmlElementAttribute("Contact", typeof(Contact))]
public object Item;

XmlSerializer 是执行实际序列化操作的对象,它在尝试序列化 public object Item 时会解释属性元数据。如果 item 的真实 datatype 是字节数组,序列化程序将自动将其序列化为 Base64 编码的 string。如果 object 类型是 string,则 string 内容将作为文本流出。如果 item object 类型是联系人,序列化程序将像处理任何其他 .NET 对象一样序列化 contact 对象。任何 Base64 转换都会为您处理,文本实体化(“<”变成“&lt;”,“&”变成“&”等等)也是如此。如果您序列化了法律公司 Jones & Jones 的名称,XML 将包含“Jones & Jones”,以避免不正确地解析 XML 特殊字符。

如果您采用我的 XML 卡片模式并通过 xsd.exe 运行它,您得到的源代码将与我在此处显示的略有不同,但这仅仅是因为它生成的类型名称略有不同。实际上,您得到的东西会非常接近我在此处显示的 UML

Card object UML static class diagram

xsd.exe 看到了我为属性和单个卡片指定的出现关系,并创建了一个属性(minOccurs = maxOccurs = 1)实例,但创建了一个卡片数组(minOccurs = 0maxOccurs = unbounded)。

然后,我进一步修改了源文件以适应我的喜好。例如,我更喜欢在可能的情况下使用 .NET 集合类而不是简单的数组,因此您会在源代码中看到我向卡组类添加了一个名为“Items”的 public 属性。然后,我想告诉序列化程序忽略这个 public 属性,因为 Card 数组属性将满足我的序列化需求。为此,我使用了另一个 XML 序列化属性 XmlIgnore

[System.Xml.Serialization.XmlIgnore()]
public CardCollection Items = new CardCollection();

然后,我修改了 public 卡片属性,即 XmlSerializer 实际序列化的那个属性,使用我的集合

[System.Xml.Serialization.XmlElementAttribute("Card")]
public CardType[] Card
{
  get { return Items.ToArray(); }
  set {
    Items.Clear();
    Items.AddRange(value);
  }
}

在这里,您会看到另一个序列化属性 XmlElementXmlElement 用于更改与对象属性关联的 XML 元素名称。在这种情况下,我们处理的是数组,因此每个数组元素都命名为 <Card/>CardCollection 类是我实现的一个类。请记住,如果您重新生成源文件,则需要重新实现任何自定义修改。

xsd.exe 还添加了另外两个有趣的序列化属性:XmlTypeXmlRoot

[System.Xml.Serialization.XmlTypeAttribute(
  Namespace="http://tempuri.org/Cardfile.xsd")]
[System.Xml.Serialization.XmlRootAttribute(
  Namespace="http://tempuri.org/Cardfile.xsd",
  IsNullable=false)]
public class Cards
{
  ...
}

XmlType 存在的原因是,在模式中我指定了一个目标命名空间,这决定了相关的 XML 文件必须应用一个与模式匹配的命名空间。XmlSerializer 需要此信息,此属性用于提供此信息。XmlRoot 用于标识根 XML 节点(表示文档元素的类)。如果没有其他输入,XmlSerializer 需要实现更复杂的算法来找出根可能是什么,如果可能确定的话。这个快捷方式元素通过明确指出 XML 序列化的根是什么来帮助序列化程序。

对于大多数情况,我展示的序列化属性就足够了,如果您使用 xsd.exe 来生成源文件,它会为您插入适当的属性。如果您在执行 xsd.exe 时遇到错误,您需要更正或更新模式以适应 xsd.exe,或者自己创建源文件。我遇到的大多数错误都来自我收到的模式,这些模式包含 xsd.exe 无法处理的模式元素(如 <xs:union/>)或模式元素流中的错误(嗯,那就是我创建模式时犯的错误)。

还有一个有时很有用的序列化元素,尽管 xsd.exe 可能不会为您注入它,但您在序列化复杂元素时可能需要它。该属性是 XmlInclude,它仅用于指定要序列化和反序列化的对象的类型。当您通过网络传输复杂数据类型(即,您创建的表示方法参数或返回类型的类)时,它在 Web 服务中尤其有用。

序列化您的数据文件

到目前为止,我们仅仅创建了 C# 文件,这些文件在序列化时代表我们所需的数据文件布局。一旦您创建了源文件,就可以使用它们了。您创建数据文件对象的方式与创建和使用其他 Framework 组件的方式相同。在这种情况下,示例允许您创建和填充卡片,将它们保存到磁盘,读取它们,并显示它们的内容。演示应用程序不是很花哨……我,嗯,没有时间完成我的“漂亮”卡片文件应用程序。但这段代码可能更好地演示了序列化概念,因为要弄清楚我的做法,需要梳理的代码更少。

将卡片保存到文件是一个非常简单的过程。我们只需创建一个 XmlSerializer 实例和一个关联的流写入器,使用序列化程序的 Serialize() 方法序列化卡组对象,然后关闭流。下面的“save”方法来自演示应用程序

private void SaveCards(string fileName)
{
  // Serialize the cards to a file
  StreamWriter writer = null;
  try
  {
    XmlSerializer ser = new XmlSerializer(typeof(Cards));
    writer =  new StreamWriter(fileName);
    ser.Serialize(writer, this._cards);
  } // try
  catch (Exception ex)
  {
    string strErr = String.Format("Unable to save cards, error '{0}'",
      ex.Message);

 

MessageBox.Show(strErr,"Card File Save Error",MessageBoxButtons.OK, MessageBoxIcon.Error); } // catch finally { if (writer != null) writer.Close(); writer = null; } // finally }

通过序列化程序的 Deserialize() 方法,反序列化已保存的卡组同样简单

private void LoadCards(string fileName)
{
  StreamReader reader = null;
  try
  {
    // Deserialize
    XmlSerializer ser = new XmlSerializer(typeof(Cards));
    reader =  new StreamReader(fileName);
    this._cards = (Cards)ser.Deserialize(reader);
    if ( this._cards == null ) throw new NullReferenceException(
         "Invalid card file");
  } // try
  catch (Exception ex)
  {
    string strErr = String.Format("Unable to load cards, error '{0}'",
      ex.Message);
    MessageBox.Show(strErr,"Card File Open Error",MessageBoxButtons.OK,
      MessageBoxIcon.Error);
  } // catch
  finally
  {
    if (reader != null) reader.Close();
    reader = null;
  } // finally
}

XML 序列化基础设施为我们处理了所有的 XML 转换工作,简化了我们的代码。我们仍然需要设计我们的 XML 并创建相关的 XML 模式,但从长远来看,使用 XML 序列化而不是直接使用 XmlTextReader /XmlTextWriter 或其他类似方法进行读/写,将使更改我们的 XML 数据文件格式变得更加容易。请注意,由于我有模式,我还可以添加一个步骤来加载卡片文件。我可以将文件作为 XML 加载到一个验证读取器中,并根据模式对其进行验证。如果验证通过,我才会将其反序列化为一组卡片对象。我在这里没有展示这一点,因为重点是序列化,但这是一个显而易见的扩展,我将其添加到了示例应用程序中。查找 LoadCards() 方法,了解我是如何从应用程序资源池中获取模式并将其与验证读取器一起在反序列化期间使用的。

PocketPC 版本

对于感兴趣的人来说,文章开头我提到我对编写应用程序的桌面和 PocketPC 版本都感兴趣。我的目标是共享数据文件。我现在已经完成了 PocketPC 的初始版本,并提供了源代码和 CAB 文件供您查看。

主要区别是什么?

应用程序的大部分内容都可以正常移植。ListView 控件有一些细微的差别,而且许多桌面支持的 Framework 在紧凑型 Framework 中不受支持(对我来说最重要的是缺少 ThreadException 事件和光标支持)。我还必须学会处理输入设备组件,至少是允许其显示(在子窗体中,放置一个空白的主菜单)。

主要的改变是序列化和反序列化。对于 PocketPC,我们没有 XPath 或 XmlSerializer 支持,因此序列化和反序列化更像是一项繁琐的工作。我选择通过 XmlDocument 进行序列化,并在进行过程中创建元素(而不是直接使用 XmlTextWriter )。我从文档元素开始,自上而下构建文件。对于反序列化,我试图限制期望,只请求在给定深度应该找到的节点。如果我找到了一个节点,那就太好了。如果没有,那么我要么继续,要么抛出异常(如果节点特别重要)。我选择将所有序列化和反序列化逻辑封装在一个单独的辅助类 CardSerializer 中。

当然,另一个主要的改变是用户界面。但考虑到屏幕空间有限,这是可以预料的。

我学到的最令人惊讶的事情是什么?

位图无法保存。您可以在桌面创建一个卡片文件,并在其中包含一个嵌入的图像卡,当您在 PocketPC 设备上反序列化它时,您将能够看到您的图像(无 GIF 动画)。但是紧凑型 Framework 不支持将位图字节流出,因此我无法允许您从头创建实际嵌入新图像的图像卡(您可以创建卡片,但只能持久化卡片名称)。我不确定微软为何选择以这种方式使位图不透明。我在网上找到了一些解决方案,但没有一个如我所愿。在这种情况下,我根本不允许您在图像卡中插入图像。

历史

  • 2003年8月26日首次发布
  • 2003年8月29日更新了文章宽度
  • 2003年9月1日添加了 PocketPC 源代码和演示

Kenn Scribner - 简介

Kenn 是几本 Windows 开发书籍的作者和合著者,包括:

  • MFC Programming Visual C++ 6.0 Unleashed
  • Teach Yourself ATL Programming in 21 Days
  • Understanding SOAP
  • Applied SOAP: Implementing .NET XML Web Services

他为其他几本 Windows 开发书籍做出了贡献,并为《PC Magazine》和《Visual C++ Developer's Journal》撰写了文章。他目前是 Wintellect 的首席顾问,并教授 XML 和 .NET Web 服务。

许可证

本文没有明确的许可附加,但可能包含文章文本或下载文件本身的用途条款。如有疑问,请通过下面的讨论区联系作者。作者可能使用的许可证列表可以在 此处 找到。

© . All rights reserved.