在 C#/VB.Net 中读写 XML





5.00/5 (12投票s)
对象图的序列化/反序列化技巧和窍门
![]() |
目录
引言
本文回顾了在 .Net 中读写 XML 的方法。这些方法适用于树形结构(另请参阅 SQLite 数据存储),以及几乎任何具有类似复杂性质的结构。本回顾包含对大型项目而言可能很有趣的技术,在这些项目中,隐藏实现细节(使用非公共数据模型)是稳定软件开发的关键 [1]。本文讨论了 XSD 与 DataContractSerializer
类的用法。
背景
XML 格式广泛用于存储数据,但我未能找到一个 .Net 示例代码项目用于将 XML 数据存储/加载到/从树形结构中。更不用说使用接口隐藏模型细节或将 XSD(数据契约)与 DataContractSerializer 一起使用。
XmlSerializer
.Net 框架提供了一个非常简单的 XmlSerializer 功能来处理 XML 数据。此功能只需要一个具有 public get
和 set
属性的数据模型。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 中并不复杂。
重要的是要注意,这种简单的序列化方法仅在我们可以提供以下条件时才有效:
- 公共类,具有 公共默认构造函数,并且
- 公共属性,具有用于
get
和set
的公共访问器
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]。然后,像 XMLSerializer
或 DataContractSerializer
这样的服务类会使用定义的接口方法来驱动每个对象的序列化过程。该接口需要实现 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
接口要求模型中的每个类都实现 ReadXml
、WriteXml
和 GetSchema
方法。GetSchema
方法是必需的,但如果返回 null,则已实现。其他两个方法需要实现,并且并非非常直接,尤其是在读取 XML 时,因为读取过程应该足够灵活以处理不同的情况,但仍然足够好以正确读取所有内容。
可以通过一个以上的类来处理通过 IXmlSerializable
接口的反序列化过程。一个广为人知的类是 .Net 1.1 以来一直存在的 XMLSerializer
类,而较新的 DataContractSerializer
则是在 .Net 3.5 中添加的。
XMLSerializer
要求:
- 建模类以公共类的形式存在,
- 具有内部构造函数,
- 属性的 get/set 可以是私有的(属性不是必需的)。
此实现可以在 IXmlSerializable_V1.zip 示例代码中进行验证,其中一个初步的类似原型的实现基于 IXmlSerializable
接口,并通过 RootItems
和 Children
集合上的 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
中的解决方案还将模型数据类移到单独的程序集中,并实现了接口以测试这些类可以被限制到什么程度。
现在,我们可以为 Node
和 ModelRoot
类使用私有属性设置器和内部类构造函数,这些类仍然必须是公共的才能与 IXmlSerializable
接口实现和 IXmlSerializer
服务类正常工作。
这意味着 TreeModelLib
库的客户端可以看到建模类,但它们无法再创建这些类,也无法在不通过定义的接口的情况下操作数据项。
因此,使用 XMLSerializer
隐藏实现细节无法完全实现(尽管我们做得相当接近)。另一方面,DataContractSerializer
[9] 在实现 IXmlSerializable
接口时,也可以使用内部类模型和私有属性设置访问器(请参阅示例下载 V3_DataContractSerializer.zip)。也就是说,我们可以将 DataContractSerializer
与建模类上的 IXmlSerializable
接口一起使用,以完全隐藏通过 XML 保存和加载数据的实现细节。
这两个服务类,XMLSerializer
和 DataContractSerializer
[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 序列化技术有一个更好的了解。
一如既往,任何关于遗漏重要项目或可能适用的改进的反馈都非常受欢迎。
参考文献
- [1] 在 C#/VB.Net 中使用高级 WPF TreeView - 第 5 部分
- [5] 自定义序列化 - 第 2 部分
- [6] XmlAttributes
- [7] 如何正确实现 IXmlSerializable
- [8] IXmlSerializable 接口
XmlWriter 类
XmlReader 类
- [9] Windows Communication Foundation (WCF)
数据传输与序列化
使用 XML Schema 导入和导出 DataContractSerializer
XsdDataContractExporter 类用于生成 XSD
XsdDataContractImporter 类
- [10] 数据契约模式参考