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

在 C#/VB.Net 中读写 XML

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2017年12月21日

CPOL

11分钟阅读

viewsIcon

61947

downloadIcon

8978

对象图的序列化/反序列化技巧和窍门

目录

引言

本文回顾了在 .Net 中读写 XML 的方法。这些方法适用于树形结构(另请参阅 SQLite 数据存储),以及几乎任何具有类似复杂性质的结构。本回顾包含对大型项目而言可能很有趣的技术,在这些项目中,隐藏实现细节(使用非公共数据模型)是稳定软件开发的关键 [1]。本文讨论了 XSD 与 DataContractSerializer 类的用法。

背景

XML 格式广泛用于存储数据,但我未能找到一个 .Net 示例代码项目用于将 XML 数据存储/加载到/从树形结构中。更不用说使用接口隐藏模型细节或将 XSD(数据契约)与 DataContractSerializer 一起使用。

XmlSerializer

.Net 框架提供了一个非常简单的 XmlSerializer 功能来处理 XML 数据。此功能只需要一个具有 public getset 属性的数据模型。XmlSerializer 会考虑每个属性进行序列化,并根据给定的标记(例如:使用 XmlIgnore)或可用的数据类型和每个属性的实际名称来决定格式。本节将展示我们如何使用这种简单的方法进行快速的初步实现,这通常足以满足快速原型的需求。

读写树形 XML 数据

 

 

 

随附的演示应用程序 Easy_Xml_V1.zip 展示了一种简单的 XML 持久化方法,用于将 XML 数据读写(反序列化)到/从树形结构对象模型。树形结构包含一个根对象、一组根项及其子项。

这种简单的 XmlSerializer 方法需要上述对象模型结构和主方法中所示的工作流程(其余部分仅为演示设置)。

var rootModel = new ModelRoot("My Test Tree");
    rootModel.RootItems.Add(makeTree());                     // Create tree in-memory

    string result = null;
    XmlSerializer serializer = new XmlSerializer(typeof(ModelRoot));

    using (StreamWriter writer = new StreamWriter(@"data.xml"))  // Write Xml to file
////    using (var writer = new StringWriter())                  // Write Xml to string
    {
      serializer.Serialize(writer, rootModel);
      result = writer.ToString();                // Convert result to string to read below
    }
    
    ModelRoot resultTree = null;                        // Re-create tree from XML
    using (var reader = XmlReader.Create(@"data.xml")) // Read Xml Data from file
////    using (var reader = new StringReader(result)) // Read Xml Data from string
    {
      XmlSerializer deserializer = new XmlSerializer(typeof(ModelRoot));
      resultTree = (ModelRoot) deserializer.Deserialize(reader);
    }
    
    Console.WriteLine("Debug Deserialized XML for {0}", resultTree);
    var items = TreeLib.BreadthFirst.Traverse.LevelOrder(resultTree.RootItems, i => i.Children);
    foreach (var item in items)
    {
      Console.WriteLine("Level: {0} Node: '{1}'", item.Level, item.Node);
    }
Dim rootModel = New ModelRoot("My Test Tree") ' Create tree in-memory
rootModel.RootItems.Add(makeTree())

Dim result As String = Nothing                ' Write Xml to file
Dim serializer As XmlSerializer = New XmlSerializer(GetType(ModelRoot))

Using writer As StreamWriter = New StreamWriter("data.xml")
''''    using writer = New StringWriter()                  ' Write Xml to string
    serializer.Serialize(writer, rootModel)
    result = writer.ToString() ' Convert result to string to read below
End Using

Dim resultTree As ModelRoot = Nothing        ' Re-create tree from XML
Using reader = XmlReader.Create("data.xml")  ' Read Xml Data from file
''''    using reader = New StringReader(result) ' Read Xml Data from string
    Dim deserializer As XmlSerializer = New XmlSerializer(GetType(ModelRoot))
    resultTree = CType(deserializer.Deserialize(reader), ModelRoot)
End Using

Console.WriteLine("Deserialized XML for {0}", resultTree)
Dim items = TreeLib.BreadthFirst.Traverse.LevelOrder(resultTree.RootItems, Function(i) i.Children)
For Each item In items
    Console.WriteLine("Level: {0} Node: '{1}'", item.Level, item.Node)
Next

上面的代码将 XML 生成到字符串或文件中,并从字符串或文件中读取 XML,具体取决于我们使用的是

  • StreamWriter(@"data.xml")XmlReader.Create(@"data.xml")
  • StringWriter()StringReader(result)

如上所示的方法(注释掉以查看其工作原理)。

列表中底部的 foreach 循环用于访问(遍历)树中的每个节点并显示其内容。我们可以看到,在考虑树形结构时,将数据保存(序列化)和加载(反序列化)到 XML 中并不复杂。

重要的是要注意,这种简单的序列化方法仅在我们可以提供以下条件时才有效:

  • 公共类,具有 公共默认构造函数,并且
  • 公共属性,具有用于 getset公共访问器

XML 元素的命名默认基于类名和属性名,但可以通过适当的装饰项将其与模型分离 [6]

这里要避免的一个陷阱是直接引用父项,因为这会导致循环结构,XML 无法对其进行序列化(这是 XML 格式的限制)。但这并不意味着我们不能在模型中拥有父指针,如果我们觉得需要的话。我们可以简单地将 Parent 属性应用于 [XmlIgnore] [6] 标记,从而避免这种情况(因为 Parent 属性对于 XML 序列化不是必需的),或者我们可以使用 ParentId 来解析父关系,如下面的代码所示(假设 Id 在整个树结构中是唯一的)。

public class Node
{
   private Node _Parent = null;

   public string Id      { get; set; }
   
   public List<node> Children  { get; set; }

   [XmlIgnore]
   public Node Parent
   {
     get
     {
       return _Parent;
     }
     
     set
     {
       if (_Parent != value)
       {
         _Parent = value;
         
         if (_Parent != null)
           ParentId = _Parent.Id;
         else
           ParentId = string.Empty;
       }
       
     }
   }

   public string ParentId      { get; set; }
}</node>
Public Class Node

    Private _Parent As Node = Nothing

    Public Property Id As String

    Public Property Children As List(Of Node)

        <xmlignore>
    Public Property Parent As Node
        Get
            Return _Parent
        End Get

        Set(ByVal value As Node)
            If _Parent <> value Then
                _Parent = value
                If _Parent IsNot Nothing Then ParentId = _Parent.Id Else ParentId = String.Empty
            End If
        End Set
    End Property

    Public Property ParentId As String
End Class</xmlignore>

ParentId 可以在加载时通过使用最后一个列表底部附近的 foreach 循环和字典(如之前为 级别顺序转换 所解释的)转换为父引用。

上述序列化和反序列化代码需要知道根的类,并且可以在不同“存储类型”之间完成,例如字符串、文件等——通过模板类定义可以更方便地实现用于不同类的功能(有关完整列表,请参阅 Easy_Xml_V2.zip 解决方案)。

public class XmlStorage
{
  public static string WriteXmlToString<t>(T rootModel)
  {
    using (var writer = new StringWriter())     // Write Xml to string
    {
      XmlSerializer serializer = new XmlSerializer(typeof(T));
      serializer.Serialize(writer, rootModel);
      return writer.ToString();                // Convert result to string to read below
    }
  }

  public static void WriteXmlToFile<t>(string filename, T rootModel)
  {
    using (StreamWriter writer = new StreamWriter(filename))  // Write Xml to file
    {
      XmlSerializer serializer = new XmlSerializer(typeof(T));
      serializer.Serialize(writer, rootModel);
    }
  }

  public static T ReadXmlFromString<t>(string input)
  {
    using (var reader = new StringReader(input))   // Read Xml Data from string
    {
      XmlSerializer deserializer = new XmlSerializer(typeof(T));
      return (T) deserializer.Deserialize(reader);
    }
  }
  
  public static T ReadXmlFromFile<t>(string filename)
  {
    using (var reader = XmlReader.Create(filename))    // Read Xml Data from file
    {
      XmlSerializer deserializer = new XmlSerializer(typeof(T));
      return (T)deserializer.Deserialize(reader);
    }
  }
}</t></t></t></t>
Public Class XmlStorage
  ' Write Xml to String
  Public Shared Function WriteXmlToString(Of T)(ByVal rootModel As T) As String
    Using writer = New StringWriter()
      Dim serializer As XmlSerializer = New XmlSerializer(GetType(T))
      serializer.Serialize(writer, rootModel)
      Return writer.ToString()
    End Using
  End Function

  ' Write Xml to file
  Public Shared Sub WriteXmlToFile(Of T)(ByVal filename As String, ByVal rootModel As T)
    Using writer As StreamWriter = New StreamWriter(filename)
      Dim serializer As XmlSerializer = New XmlSerializer(GetType(T))
      serializer.Serialize(writer, rootModel)
    End Using
  End Sub

  ' Read Xml from String
  Public Shared Function ReadXmlFromString(Of T)(ByVal input As String) As T
    Using reader = New StringReader(input)
      Dim deserializer As XmlSerializer = New XmlSerializer(GetType(T))
      Return CType(deserializer.Deserialize(reader), T)
    End Using
  End Function

  ' Read Xml from file
  Public Shared Function ReadXmlFromFile(Of T)(ByVal filename As String) As T
    Using reader = XmlReader.Create(filename)
      Dim deserializer As XmlSerializer = New XmlSerializer(GetType(T))
      Return CType(deserializer.Deserialize(reader), T)
    End Using
  End Function
End Class

因此,这就是简单的解决方案,只要结构相对较小(几千个对象)并且可以公开访问构成模型的对象,它就应该适用。

但是,如果我们想隐藏 XML 反序列化的实现细节,因为这是大型项目的一部分,我们需要最大程度地减少出错的可能性,该怎么办?或者对于需要对反序列化过程进行更多控制的项目怎么办?这就是 IXmlSerializable 接口和 DataContractSerializer 可以派上用场的地方,我们将在接下来的部分中看到。

实现 IXmlSerializable

 

 

 

必须由表示数据模型的类来实现 IXmlSerializable 接口 [7]。然后,像 XMLSerializerDataContractSerializer 这样的服务类会使用定义的接口方法来驱动每个对象的序列化过程。该接口需要实现 3 个方法。

public interface IXmlSerializable
{
  XmlSchema GetSchema ();
  void ReadXml ( XmlReader reader );
  void WriteXml ( XmlWriter writer );
}
Public Interface IXmlSerializable

  Function GetSchema() As XmlSchema
  Sub ReadXml(ByVal reader As XmlReader)
  Sub WriteXml(ByVal writer As XmlWriter)

End Interface

IXmlSerializable 接口要求模型中的每个类都实现 ReadXmlWriteXmlGetSchema 方法。GetSchema 方法是必需的,但如果返回 null,则已实现。其他两个方法需要实现,并且并非非常直接,尤其是在读取 XML 时,因为读取过程应该足够灵活以处理不同的情况,但仍然足够好以正确读取所有内容。

可以通过一个以上的类来处理通过 IXmlSerializable 接口的反序列化过程。一个广为人知的类是 .Net 1.1 以来一直存在的 XMLSerializer 类,而较新的 DataContractSerializer 则是在 .Net 3.5 中添加的。

XMLSerializer 要求:

  • 建模类以公共类的形式存在,
  • 具有内部构造函数
  • 属性的 get/set 可以是私有的(属性不是必需的)。

此实现可以在 IXmlSerializable_V1.zip 示例代码中进行验证,其中一个初步的类似原型的实现基于 IXmlSerializable 接口,并通过 RootItemsChildren 集合上的 Count 属性持久化一个集合。在每个 ReadXml() 方法中读取 XML 时,会使用此 Count 属性来逐个加载每个子条目。

IXmlSerializable_V1.zip 中所示的用于在 XML 中处理集合数据的简化解决方案,在 IXmlSerializable_V2.zip 的演示代码中得到了改进,我们在其中读取和写入每个集合中的项,而无需使用 Count 属性。这是可能的,因为 XmlReader 允许我们知道我们是在 XML 集合的末尾还是在下一个子元素的开头。

public void ReadXml(System.Xml.XmlReader reader) // ModelRoot class
{
  Name = reader.GetAttribute("Name");

  Version = int.Parse(reader.GetAttribute("Version"));
  MinorVersion = int.Parse(reader.GetAttribute("MinorVersion"));

  reader.ReadStartElement();
  reader.MoveToContent();
  while (reader.NodeType == System.Xml.XmlNodeType.Whitespace)
      reader.Read();

  if (reader.NodeType != System.Xml.XmlNodeType.EndElement)
  {
    reader.ReadStartElement("RootItems");

    reader.MoveToContent();
    while (reader.NodeType == System.Xml.XmlNodeType.Whitespace)
        reader.Read();

    if (reader.NodeType != System.Xml.XmlNodeType.EndElement)
    {
      var nodeSer = new XmlSerializer(typeof(Node));
      while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
      {
        var nextNode = (Node)nodeSer.Deserialize(reader);
        _RootItems.Add(nextNode);

        while (reader.NodeType == System.Xml.XmlNodeType.Whitespace)
          reader.Read();
      }
      reader.ReadEndElement();
    }
    reader.ReadEndElement();
  }
}
Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements IXmlSerializable.ReadXml

  Name = reader.GetAttribute("Name")
  Version = Integer.Parse(reader.GetAttribute("Version"))
  MinorVersion = Integer.Parse(reader.GetAttribute("MinorVersion"))

  reader.ReadStartElement()
  reader.MoveToContent()

  While reader.NodeType = System.Xml.XmlNodeType.Whitespace
      reader.Read()
  End While

  If reader.NodeType <> System.Xml.XmlNodeType.EndElement Then
    reader.ReadStartElement("RootItems")
    reader.MoveToContent()
    While reader.NodeType = System.Xml.XmlNodeType.Whitespace
        reader.Read()
    End While

    If reader.NodeType <> System.Xml.XmlNodeType.EndElement Then
      Dim nodeSer = New XmlSerializer(GetType(Node))

      While reader.NodeType <> System.Xml.XmlNodeType.EndElement
        Dim nextNode = CType(nodeSer.Deserialize(reader), Node)
        _RootItems.Add(nextNode)

        While reader.NodeType = System.Xml.XmlNodeType.Whitespace
            reader.Read()
        End While
      End While

      reader.ReadEndElement()
    End If

    reader.ReadEndElement()
  End If
End Sub

上面列表中的 var nodeSer = new XmlSerializer(typeof(Node)); 语句是必需的,因为应该使用 XmlSerializer 来写入每个子节点的开始和结束标签。

IXmlSerializable_V2.zip 中的解决方案还将模型数据类移到单独的程序集中,并实现了接口以测试这些类可以被限制到什么程度。

现在,我们可以为 NodeModelRoot 类使用私有属性设置器和内部类构造函数,这些类仍然必须是公共的才能与 IXmlSerializable 接口实现和 IXmlSerializer 服务类正常工作。

这意味着 TreeModelLib 库的客户端可以看到建模类,但它们无法再创建这些类,也无法在不通过定义的接口的情况下操作数据项。

因此,使用 XMLSerializer 隐藏实现细节无法完全实现(尽管我们做得相当接近)。另一方面,DataContractSerializer [9] 在实现 IXmlSerializable 接口时,也可以使用内部类模型和私有属性设置访问器(请参阅示例下载 V3_DataContractSerializer.zip)。也就是说,我们可以将 DataContractSerializer 与建模类上的 IXmlSerializable 接口一起使用,以完全隐藏通过 XML 保存和加载数据的实现细节。

这两个服务类,XMLSerializerDataContractSerializer [9],并不完全实现相同的行为。我注意到的一个区别是,DataContractSerializer 产生的 System.Xml.XmlNodeType.Whitespace 更频繁,而 XMLSerializer 可以轻松地从一个开始标签浏览到另一个开始标签,从不提及它们之间的空白字符。这些细节并不难处理,一旦清楚行为差异所在,但当从一个序列化器更改为另一个序列化器时,仔细调试现有代码非常重要。

本文的其余部分回顾了 DataContractSerializer 实现的细节,而 XMLSerializer 实现则包含在随附的下载中——主要是因为 XMLSerializer 无法实现隐藏的类模型,而 DataContractSerializer 是一个较新的类,应该优先于 XMLSerializer,因为它提供了相同的功能以及更多高级功能。

DataContractSerializer

 

 

DataContractSerializer [9] 在 DataContractSerializer_V1.zip 中的实现与之前讨论的 IXmlSerializable_V2.zip 示例非常相似。主要区别在于我们使用 DataContractSerializer 而不是 XmlSerializer。这需要对 System.Runtime.Serialization 程序集进行引用,并在该命名空间中添加 using 语句。我们可以用 DataContractStorage 类替换之前使用的 Storage 类来启动每个反序列化过程。我们还必须用特定于 DataContractSerializer 的模式替换之前使用的读取和写入子项的模式。

public void ReadXml(System.Xml.XmlReader reader)
{
...
  reader.ReadStartElement("Children");

  reader.MoveToContent();
  while (reader.NodeType == System.Xml.XmlNodeType.Whitespace)
    reader.Read();

  if (reader.NodeType != System.Xml.XmlNodeType.EndElement)
  {
    while (reader.NodeType != System.Xml.XmlNodeType.EndElement)
    {
      var dataContractSerializer = new DataContractSerializer(typeof(Node));
      var nextNode = (Node)dataContractSerializer.ReadObject(reader);
      _ChildItems.Add(nextNode);

      while (reader.NodeType == System.Xml.XmlNodeType.Whitespace)
          reader.Read();
    }
    reader.ReadEndElement();
  }
...
}
Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements IXmlSerializable.ReadXml
...
  reader.MoveToContent()

  While reader.NodeType = System.Xml.XmlNodeType.Whitespace
      reader.Read()
  End While

  If reader.NodeType <> System.Xml.XmlNodeType.EndElement Then
    reader.ReadStartElement("Children")
    reader.MoveToContent()

    While reader.NodeType = System.Xml.XmlNodeType.Whitespace
        reader.Read()
    End While

    If reader.NodeType <> System.Xml.XmlNodeType.EndElement Then

      While reader.NodeType <> System.Xml.XmlNodeType.EndElement
        Dim dataContractSerializer = New DataContractSerializer(GetType(Node))
        Dim nextNode = CType(dataContractSerializer.ReadObject(reader), Node)

        _ChildItems.Add(nextNode)
        While reader.NodeType = System.Xml.XmlNodeType.Whitespace
            reader.Read()
        End While
      End While

      reader.ReadEndElement()
    End If

    reader.ReadEndElement()
...
End Sub
public void WriteXml(System.Xml.XmlWriter writer)
{
...
  writer.WriteStartElement("Children");
  foreach (var item in Children)
  {
    var dataContractSerializer = new DataContractSerializer(typeof(Node));
    dataContractSerializer.WriteObject(writer, item);
  }
writer.WriteEndElement();
...
}
Public Sub ReadXml(ByVal reader As System.Xml.XmlReader) Implements IXmlSerializable.ReadXml
...
writer.WriteStartElement("Children")

For Each item In Children
Dim dataContractSerializer = New DataContractSerializer(GetType(Node))
dataContractSerializer.WriteObject(writer, item)
Next

writer.WriteEndElement()
...
End Sub

这就是使用具有完全隐藏数据模型实现的 XML 序列化器所需要的一切。到目前为止,本文中讨论的所有解决方案在 XML Schema Definition (XSD) 的使用方面仍然很基础,XSD 通常用于确保所有数据项都符合预期的约束。下一节将使用 DataContractSerializer 来评估这一点,以使事情在生产环境中发生的故障更加稳健。

让事物更可靠

 

 

 

本节讨论了如何将 XML Schema Definition (XSD) [10] 与 DataContractSerializer [9] 一起使用,以确保传输的数据项的一致性在生产环境中符合预期。XsdDataContract.zip 解决方案包含 2 个直接取自引用的 MSDN 文章的项目。

XsdDataContractExporter 项目展示了如何使用 DataContractSerializer [9] 来基于给定的数据模型创建数据契约。导出的 XSD 在细节等方面没有什么可说的,但也许这些细节对其他人有用。

XmlSchemaSet_Sample 展示了如何使用 XSD 文件表示来控制使用 XmlReader 读取 XML 时的解析过程。该项目表明 XmlReaderSettings 类可以包含多个模式定义(短模式或 XSD),这些模式又可以用于初始化 XmlReader。然后,XmlReader 可以使用回调函数在必要时报告任何错误或警告。

XmlSchemaSet_Sample 项目中吸取的教训已应用于 DataContractSerializer_V2.zip 示例。此示例扩展了读取 XML 方法签名以适应其他模式。

public static IModelRoot ReadXmlFromString<t>(string input
                                            , XmlSchemaSet schemas = null
                                            , ValidationEventHandler validationCallBack = null)

public static IModelRoot ReadXmlFromString<t>(string input
                                            , XmlSchemaSet schemas = null
                                            , ValidationEventHandler validationCallBack = null)

</t></t>
Public Shared Function ReadXmlFromFile(Of T)(ByVal filename As String,
     ByVal Optional schemas As XmlSchemaSet = Nothing,
     ByVal Optional validationCallBack As ValidationEventHandler = Nothing) As IModelRoot

Public Shared Function ReadXmlFromString(Of T)(ByVal input As String,
    ByVal Optional schemas As XmlSchemaSet = Nothing,
    ByVal Optional validationCallBack As ValidationEventHandler = Nothing) As IModelRoot

这些模式被传递给 XmlReader,以便在 XML 不符合 TreeModel.xsd 文件中指定的预期时报告信息。我们可以通过打开 TreeModel.xsd 文件并指定一个当前未实现的属性来验证这一点。

<xsd:attributeGroup name ="ModelRootAttribs">
  <xsd:attribute name="Version" default="1" type="xsd:int" />
  <xsd:attribute name="MinorVersion" default="0" type="xsd:int" />
  <xsd:attribute name="Name" type="xsd:string" use="required" />
  <xsd:attribute name="Test" type="xsd:string" use="required" />
</xsd:attributeGroup>

...应该会产生以下输出

Validation Error: The required attribute 'Test' is missing.
Exception The required attribute 'Test' is missing. at line 2 position 2

结论

.Net 框架还支持用于将对象序列化为二进制格式的 ISerializable 接口。这种形式的序列化不属于本文的讨论范围,因为它不具有互操作性,并且与 IXmlSerializable 接口非常相似。我实际上并没有尝试过,但我预计将 IXmlSerializable 接口与 zip 容器一起使用可以产生与 ISerializable 接口相当的性能和空间需求(尤其是在读取数据时)。如果您想了解有关从压缩数据容器读取可互操作 XML 的性能提示,请参阅本文随附的 04_sqlite_tut.zip 演示应用程序。

.Net 框架对 XML 的支持非常广泛,本文中提到的接口和技术绝不是完整的。一个经常用于生成 XSD 或从 XSD 生成模型类的工具是 XML Schema Definition Tool (Xsd.exe) 工具。当快速生成具有复杂约束的对象模型时,此工具也很有用,但其应用细节肯定是另一篇文章的主题。一个类似的工具,但用于 DataContractSerializer,是 ServiceModel Metadata Utility Tool (Svcutil.exe),本文也未涵盖。

.Net 对 XML 序列化的支持如此广泛,以至于“是否可以用 XML 完成某事?”这个问题很快被“如何完成?”这个问题取代。这又引出了工作代码示例的问题。我希望本文能就这些问题提供一些见解,并让您对 XML 序列化技术有一个更好的了解。

一如既往,任何关于遗漏重要项目或可能适用的改进的反馈都非常受欢迎。

参考文献

© . All rights reserved.