第10章 LINQ to XML
第10章 LINQ to XML
10.1 架构概述——DOM 和 LINQ to XML 的 DOM
XML 文档可以用一棵对象树完整的表示,这称为“文档对象模型(document object model)”
LINQ to XML 由两部分组成:
- XML DOM,简称为 X-DOM
- 大约 10 个查询运算符
LINQ 也可以用于查询 W3C 标准的旧 DOM,不过 X-DOM 对 LINQ 查询更为友好:
- X-DOM 部分方法可以返回
IEnumerable 序列; - X-DOM 的构造器支持通过 LINQ 构建对象树。
Tips
W3C 标准的 DOM 对应 C# 中的
XmlDocument;X-DOM 对应 C# 中的XDocument 等一系列类型。更多内容见12 System.Xml 的使用
10.2 X-DOM 概览
XObject 是所有类型的基类,XElement 和 XDocument 是所有容器类型的基类
以如下代码为例,它对应的 X-DOM 如图:
string xml = @"
<customer id='123' status='archived'>
<firstname>Joe</firstname>
<lastname>Bloggs<!--nice name--></lastname>
</customer>
";
XElement customer = XElement.Parse(xml);
XObject
XObject 为抽象类,是所有 XML 内容(XML Content)的 基 类,它内含一个指向 父 元素( Parent element)的属性、一个指向 XDocument 的(可选)属性。
XNode
XNode 为抽象类,是大多数 XML 内容的基类(不含 XAttribute )。XNode 指向 父 元素(Element),不会指向 子 节点(Node)。
XContainer
我们在 XNode 提到,XNode 不会指向 子 节点。指向 子 节点的工作由它的派生类 XContainer 完成。
XContainer 为抽象类,用于处理子项。它也是 XElement 和 XDocument 的基类。
XElement
XElement 引入了诸如 Name、Value 等成员,用于管理特性。多数 XElement 仅包含一个 XText 节点,Value 用于快捷地 get、set 其内容。
XDocument
XDocument 封装了根节点的 XElement ,添加了 XDeclaration 、一系列处理指令及其他根元素+功能。
与 W3C DOM 不同,XDocument 是可选的,因此我们可以高效移动任意子节点至其他 X-DOM 中。
10.2.1 加载和解析
XElement 和 XDocument 提供了静态 Load 和 Parse 方法,用于从现有源建立 X-DOM 树。支持的源有:
-
Load 方法:通过文件建立 X-DOM:URI、Stream、
TextReader、XmlReaderXDocument fromWeb = XDocument.Load ("http://albahari.com/sample.xml"); XElement fromFile = XElement.Load (@"e:\media\somefile.xml"); XElement config = XElement.Parse (@" <configuration> <client enabled='true'> <timeout>30</timeout> </client> </configuration>"); -
Parse 方法:通过字符串建立 X-DOM
Tips
XNode 也提供了一个静态方法 ReadFrom ,从 XmlReader 中实例化+填充任意类型的节点(node)。与Load 不同,它每次仅读取一个完整节点,因此我们可以用它进行手动读取。
10.2.2 保存和序列化
任何 node 实例都可以通过其 ToString 方法输出 XML 格式的字符串,通过 WriteTo 方法将数据写入 XmlWriter 中。
Tips
通过
ToString 获得的字符串包含缩进、换行等格式化内容,可以通过传入SaveOptions.DisableFormatting 参数关闭该特性。注意:若原始 XML 内容包含格式化内容,即使传入
SaveOptions.DisableFormatting 参数,仍会保持原缩进样式。
XElement 和 XDocument 提供了 Save 方法将 X-DOM 保存至 URI、Stream、TextWriter、XmlWriter 中。该方法会自动添加 XML 声明(见10.7.2 XML 声明(declaration))。
10.3 实例化 X-DOM
10.3.0 构造器 + Add 方法
任意 XContainer 的子类都可以使用构造器 + Add 方法创建 X-DOM 树,方法如下:
XElement lastName = new XElement ("lastname", "Bloggs");
lastName.Add (new XComment ("nice name"));
XElement customer = new XElement ("customer");
customer.Add (new XAttribute ("id", 123));
customer.Add (new XElement ("firstname", "Joe"));
customer.Add (lastName);
customer.Dump();
<customer id="123">
<firstname />
<lastname>Bloggs<!--nice name--></lastname>
</customer>
其中 Name 参数必选, Value 参数可选(可以在创建完成后再设置 Value 值)。Value 对应 XText 节点,它会被隐式创建。
10.3.1 函数式构建(Functional Construction)
X-DOM 支持“函数式”构建(源自函数式编程 functional programming),用法如下:
new XElement ("customer", new XAttribute ("id", 123),
new XElement ("firstname", "joe"),
new XElement ("lastname", "bloggs",
new XComment ("nice name")
)
)
Eureka
XElement 利用了params 关键字实现该效果:public XElement(XName name, params object?[] content)
优点有2:
-
和 XML 自身结构相似;
-
它可以使用 LINQ 的
select 语句。以如下代码为例,其中
Customers 为 EF Core 实例。new XElement ("customers", from c in Customers.AsEnumerable() select new XElement ("customer", new XAttribute ("id", c.ID), new XElement ("name", c.Name, new XComment ("nice name") ) ) )
10.3.2 指定内容(Specifying Content)
实际上,10.3.1 函数式构建(Functional Construction)利用了 C# 中的可选参数数组,XElement 的构造器和 XContainer 的 Add 方法定义如下:
public XElement(XName name, params object?[] content)
public void Add (params object[] content)
XContainer 将可选参数数组的所有对象都转为了 Node 或 Attribute ,其处理逻辑如下:
- 忽略 null 对象;
-
XNode、XStreamingElement 对象,添加至 Node 集合中; -
XAttribute 对象,添加至 Attribute 集合中; -
string 对象,包装成 XText 节点,添加至 Node 集合中; -
IEnumerable 对象,遍历所有内容,按照 1~4 步处理; - 其他:将对象转化为
string ,按照步骤 4 处理。
Tips
object 包含ToString 方法,所有object 都可以转化为XText 节点,因此不存在无效对象。此外,
XContainer 在调用ToString 前会检查对象是否是如下类型,是则调用XmlCovert,保证序列化不受CultureInfo 影响,符合 XML 格式规则:
float、double、decimal、bool、DateTime、DateTimeOffset、TimeSpan
10.3.3 自动深度克隆(Automatic Deep Cloning)
如XObject中提到,所有元素都包含 Parent Element 指针。当实例已有 Parent,将其赋值给其他 XContainer 时,将自动进行深克隆:
var address =
new XElement ("address",
new XElement ("street", "Lawley St"),
new XElement ("town", "North Beach")
);
var customer1 = new XElement ("customer1", address);
var customer2 = new XElement ("customer2", address);
customer1.Element ("address").Element ("street").Value = "Another St";
customer2.Element ("address").Element ("street").Value.Dump(); // 输出 Lawley St
Extra
因 X-DOM 深拷贝的特性,它的实例化没有任何副作用,这也是“函数式编程”的特点。
10.4 导航和查询(Navigating and Querying)
XNode 和 XContainer 定义了方法和属性用于游历 X-DOM 树。与常规 DOM 不同,这些函数返回单个值或 IEnumerable<T> 对象,而非 IList<T>,因此需要通过 LINQ 进行查询。
Warn
在 X-DOM 中,Element 和 Attribute 的 name 是大小写敏感的,与 XML 一致。
10.4.1 导航至子节点
Tips
带 * 的方法可以在序列(sequences)上使用(由 LINQ 支持)。
Info
本节用到的 XML 内容均为:
<bench> <toolbox> <handtool>Hammer</handtool> <handtool>Rasp</handtool> </toolbox> <toolbox> <handtool>Saw</handtool> <powertool>Nailgun</powertool> </toolbox> <!--Be careful with the nailgun--> </bench>
10.4.1.1 FirstNode()、LastNode() 和 Nodes()
这三个方法(属性)用于操作 直接 子节点,Nodes() 返回 所有直接子节点序列(sequences )。以如下代码为例:
var bench =
new XElement ("bench",
new XElement ("toolbox",
new XElement ("handtool", "Hammer"),
new XElement ("handtool", "Rasp")
),
new XElement ("toolbox",
new XElement ("handtool", "Saw"),
new XElement ("powertool", "Nailgun")
),
new XComment ("Be careful with the nailgun")
);
bench.FirstNode.ToString(SaveOptions.DisableFormatting).Dump ("FirstNode");
bench.LastNode.ToString(SaveOptions.DisableFormatting).Dump ("LastNode");
foreach (XNode node in bench.Nodes())
Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");
FirstNode:
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
LastNode:
<!--Be careful with the nailgun-->
Nodes():
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
<!--Be careful with the nailgun-->.
Tips
FirstNode 和LastNode 的返回值类型为 XNode ,Nodes 的返回值类型为 IEnumerable<XNode> 。
10.4.1.2 检索 elements
Elements() 方法返回 XElement 类型的单层子节点:
foreach (XNode node in bench.Elements("handtool"))
Console.WriteLine(node.ToString(SaveOptions.DisableFormatting) + ".");
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
Elements() 可以返回指定名称的元素:
int toolboxCount = bench.Elements ("toolbox").Count();
Summary
从上面两个例子可以看出,
Nodes() 与Elements() 的区别:
-
Nodes() 不 支持寻找指定元素;-
Elements() 只列出 XElement 成员。
Elements() 与 LINQ
<bench> <toolbox> <handtool>Hammer</handtool> <handtool>Rasp</handtool> </toolbox> <toolbox> <handtool>Saw</handtool> <powertool>Nailgun</powertool> </toolbox> <!--Be careful with the nailgun--> </bench>
如下代码查询含有 Nailgun 的 toolBox:
var toolboxWithNailgun =
from toolbox in bench.Elements()
where toolbox.Elements().Any (tool => tool.Value == "Nailgun")
select toolbox.Value;
如下代码查询所有 handtool :
var handTools =
from toolbox in bench.Elements()
from tool in toolbox.Elements()
where tool.Name == "handtool"
select tool.Value;
如下代码返回指定名称的元素:
var count = bench.Elements().Where(e => e.Name == "toolbox").Count();
等价于:
int toolboxCount = bench.Elements ("toolbox").Count();
Elements() 与 IEnumerable<T> where T : XContainer
XContainer.Elements() 方法的 LINQ 查询与 XContainer.Nodes() 方法的 LINQ 查询等价,之前的示例还可以写为:
from toolbox in bench.Nodes().ofType<XElement>()
where ...
但是 XContainer 有额外的扩展方法,XElement 作为它的子类同样可以用它处理元素序列。使用方式形下:
var handTools2 =
from tool in bench.Elements ("toolbox").Elements ("handtool")
select tool.Value.ToUpper();
上述查询,第一次调用的 Elements 方法绑定的是 XContainer 的实例方法,而第二次 Elements 方法则绑定到了扩展方法上。
Eureka
Nodes 方法的返回值类型是IEnumerable<XNode>,Elements 方法的返回值是IEnumerable<XElements>,而 LINQ 的Elements 方法不支持IEnumerable<XNode>,因此无法对Nodes 使用Elements 方法。
10.4.1.3 检索单个元素(element)
Element() 方法等价于 LINQ 中的 FirstOrDefault ,返回单层子节点匹配到的第一个元素,若元素不存在,返回 null。
Tips
Element("xyz").Value 调用在 xyz 元素不存在时将 抛出 NullReferenceException 。XElement 为string 类型定义了显式转换,可以通过强制类型避免此异常。即:string xyz = (string)settings.Element ("xyz");当然,我们也可以使用
?.
10.4.1.4 获取子元素:Descendants 和 DescendantNodes
XContainer 提供了 Descendants 方法和 DescendantNodes 方法,用于访问全部子元素(Element)或全部子节点(Node)(以至整棵树)。
Descendants 方法可以接收一个元素名称,返回所有子元素 (XElement 对象)。
DescendantNodes 方法不接收参数,返回所有类型的子节点(包括 XText)。
以如下代码为例,输出内容如下:
/*输出 XElement 元素
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
<handtool>Rasp</handtool>
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
<powertool>Nailgun</powertool>
*/
foreach (var node in bench.Descendants())
Console.WriteLine(node.ToString(SaveOptions.DisableFormatting));
/* 输出全部节点
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
Hammer
<handtool>Rasp</handtool>
Rasp
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
Saw
<powertool>Nailgun</powertool>
Nailgun
<!--Be careful with the nailgun-->
*/
foreach (XNode node in bench.DescendantNodes())
Console.WriteLine (node.ToString (SaveOptions.DisableFormatting));
10.4.2 导航至父节点
XNode 及其子类(XDocument 除外)可以使用 AncestorXXX 方法导航至父节点,父节点的类型必然是 XElement 。
Ancestors 返回一个序列,第一个元素是 Parent,第二个元素是 Parent.Parent ,直至根元素。
Tips
XDocument 不是任何节点的父节点,但是任何XObject 都可以通过 Document 属性访问XDocument。
Tips
可以使用 LINQ 查询根元素:
var root = bench.AncestorsAndSelf().Last();上述代码不使用
Ancestors 方法,是因为bench 本身可能就是根节点。
如果存在
XDocument,也可以通过 XObject.Document.Root 属性获取根节点。
10.4.3 导航至同级节点
可以像链表一样使用 PreviousNode 和 NextNode 属性遍历节点。
Extra
事实上,节点在内部确实是以(单)链表的方式存储,因此
PreviousNode 属性的效率较低。
10.4.4 导航至节点的 Attribute
Attribute 方法接受 name 参数,返回 0~1 个元素的序列(一个 XML 元素不能包含同名 Attribute)。
Tips
上述是
XElement 中的方法,XAttribute 类型还提供了Parent 属性、PreviousAttribute 和NextAttribute 属性。
10.5 更新 X-DOM
10.5.1 简单的值更新
SetValue 方法和 Value 属性用于替换/设置 Element 或 Attribute 的当前值。SetValue 方法接受 object 类型的数据,Value 属性仅接受 string 类型的数据。
二者赋值时,新值将替换所有子节点。
Tips
SetValue 方法内部实际调用的也是Value 属性。
10.5.2 更新子节点(Node)和 Attribute
上述方法都用于更新当前节点。
10.5.2.1 SetElementValue 方法和 SetAttributeValue 方法
这两个方法将自动实例化 XElement/XAttribute 对象,并作为 子 元素添加至 当前 元素中,若有同名 Element/Attribute 则进行覆盖:
XElement settings = new XElement ("settings");
settings.SetElementValue ("timeout", 30);
settings.SetElementValue ("timeout", 60);
<settings>
<timeout>30</timeout>
</settings>
<settings>
<timeout>60</timeout>
</settings>
10.5.2.2 Add 方法和 AddFirst 方法
Add 方法向内部节点的队尾插入节点; AddFirst 向内部节点的排头插入节点。
Tips
Add 方法定义在XContainer 中,AddAfterSelf 定义在XNode 中。
10.5.2.3 RemoveNodes 方法、 RemoveAttributes 方法和 RemoveAll 方法
RemoveNodes 方法用于移除持有的全部节点, RemoveAttributes 方法用于移除持有的全部 Attribute。 RemoveAll 可以一次性将二者全部移除。
10.5.2.4 ReplaceXXX 方法
等价于 RemoveXXX 方法 + Add 方法。
10.5.3 通过父节点更新子节点
上述方法操作的是当前节点的父节点(Parent),因此父节点不能为 null 。
AddBeforeSelf 方法和 AddAfterSelf 方法
用于在当前节点的前、后插入其他节点。
Remove 方法
用于在父节点中移除当前节点。
ReplaceWith 方法
用于在父节点中替换当前节点
10.5.3.1 移除节点或属性序列(LINQ)
System.Xml.Linq 提供了一系列扩展方法用于从父节点移除元素。后续代码对应的 XML 如下:
<contacts>
<customer name="Mary" />
<customer name="Chris" archived="true" />
<supplier name="Susan">
<phone archived="true">012345678<!--confidential--></phone>
</supplier>
</contacts>
Elements().Remove()
从10.4.1.2 检索 elements可知,Elements 方法返回的是单层子节点,因此如下代码只会移除当前层的子节点:
contacts.Elements()
.Where (e => (bool?) e.Attribute ("archived") == true)
.Remove();
<contacts>
<customer name="Mary" />
<supplier name="Susan">
<phone archived="true">012345678<!--confidential--></phone>
</supplier>
</contacts>
Descendants().Remove()
从10.4.1.4 获取子元素:Descendants 和 DescendantNodes可知,Descendants 方法返回所有层次的子节点,因此如下代码会移除任何匹配到的子节点:
contacts.Descendants()
.Where (e => (bool?) e.Attribute ("archived") == true)
.Remove();
<contacts>
<customer name="Mary" />
<supplier name="Susan" />
</contacts>
综合使用
以下代码移除了注释为“confidential”的联系人:
contacts.Elements()
.Where (
e => e.DescendantNodes().OfType<XComment>().Any (c => c.Value == "confidential")
)
.Remove();
<contacts>
<customer name="Mary" />
<customer name="Chris" archived="true" />
</contacts>
10.6 使用 Value
10.6.1 设置 Value
如10.5.1 简单的值更新所述:
SetValue 方法和Value 属性用于替换/设置 Element 或 Attribute 的当前值。SetValue 方法接受 object 类型的数据,Value 属性仅接受 string 类型的数据。
Warn
通过
Value 设置值时,DataTime 要使用 XmlConvert 转化数据。
SetValue 和XElement/XAttribute 的构造器会自动调用 XmlConvert 对数据格式化,保证了数据格式的正确性。
10.6.2 获得 Value
XElement/XAttribute 内部定义了诸多显式转换(如下类型),因此可以直接通过自定义转换获取 Value。
- 标准数值类型
-
string、bool、DateTime(Offset)、TimeSpan、Guid - 上述值类型的
Nullable<> 版本。
XElement e = new XElement ("now", DateTime.Now);
DateTime dt = (DateTime) e;
XAttribute a = new XAttribute ("resolution", 1.234);
double res = (double) a;
Suggestion
XML 的元素和 Attribute 不会记录数据的原始类型,上述显式转换可能执行失败。推荐将代码包裹在 try/catch 块中,并捕获
FormatException 异常。
10.6.2.1 XML 对象与空运算符
Element 方法和 Attribute 方法的返回值非常适合转化为 Nullable<> 类型,以如下代码为例,程序不会因为“timeout”不存在而抛出异常:
int timeout1 = (int) x.Element ("timeout");
int? timeout2 = (int?) x.Element ("timeout");
配合空合并运算(??)可以去除最终结果中的可空类型。如下代码在 resolution 属性不存在时返回 1.0:
double resolution = (double?) x.Attribute ("resolution") ?? 1.0;
10.6.3 值与混合内容节点
XML 是允许混合内容的,形式如下:
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
要得到上述 X-DOM,需通过 XText 节点:
XElement summary =
new XElement ("summary",
new XText ("An XAttribute is "),
new XElement ("bold", "not"),
new XText (" an XNode")
);
<!--输出-->
<summary>An XAttribute is <bold>not</bold> an XNode</summary>
其中 summary 的 Value 如下,它拼接了各个子节点的 Value:
An XAttribute is not an XNode
Tips
实际传入 string 也是可以的,构造器内部会隐式转为
XText:XElement summary = new XElement ("summary", "An XAttribute is ", new XElement ("bold", "not"), " an XNode" );
10.6.4 自动连接 XText 节点
向 XElement 中添加简单内容(字符串)时,X-DOM 会将内容附加至现有 XText :
// 1 个 XText节点
var e1 = new XElement ("test", "Hello"); e1.Add ("World");
e1.Nodes().Count().Dump (); // 输出 1
// 1 个 XText节点
var e2 = new XElement ("test", "Hello", "World");
e2.Nodes().Count().Dump (); // 输出 1
如果显式创建、添加 XText 节点,则会得到多个子节点:
// 2 个 XText节点
var e3 = new XElement ("test", new XText ("Hello"), new XText ("World"));
e3.Nodes().Count().Dump (); // 输出 2
XElement 不会连接这两个 XText 节点,节点对象的标识均得到保留。即便如此,其 ToString 输出的内容仍是拼接的:
<test>HelloWorld</test>
10.7 文档和声明
10.7.1 XDocument
XDocument 可接受的内容包括:
XElement |
XDeclaration |
XDocumentType |
XProgressingInstruction |
XComment |
|
|---|---|---|---|---|---|
| 数量 | 1 | 1 | 1 | 多个 | 多个 |
| 是否必选 | 是 | 否 | 否 | 否 | 否 |
其中 XElement 作为 X-DOM 的根节点。
若 XDocument 未定义 XDeclaration ,调用 XDocument.Save 时,会自动添加默认的 XML 声明:
// 未定义 XDeclaration
var value = new XDocument (
new XElement("test", "data")
);
<!--Save 方法生成的内容:-->
<?xml version="1.0" encoding="utf-16"?>
<test>data</test>
10.7.2 XML 声明(declaration)
10.7.2.1 XML 声明的作用
XDeclaration 对象主要用于指导 XML 的序列化进程,影响的内容有二:
- 文本编码标准
- 声明中的 encoding 和 standalone 如何定义
XDeclaration 构造器接受三个参数:version、encoding 和 standalone。
ExtraNotice
XML 写入器(writer)会忽略指定的 version 信息,总是写入“1.0”。
XML 声明中的编码方式必须使用 IETF 编码方式书写,例如“utf-16”。
10.7.2.2 XElement 和 XDocument 遵循的声明规则
XML 声明用于保证文件被阅读器(reader)正确解析(parse)并理解。XElement 和 XDocument 都遵循以下声明规则:
- 调用
Save 方法将内容写入文件,总是 会自动 写入 XML 声明。 - 调用
Save 方法将内容写入XmlWriter 时,除非XmlWriter 特别指定,否则 会 写入 XML 声明。 -
ToString 方法 不会 生成XML 声明。
Tips
如果不想让
XmlWriter 生成 XML 声明,可以设置XmlWriterSettings 对象的OmitXmlDeclaration 和ConformanceLevel 属性。
Notice
XNode 的WriteTo 方法向XmlWriter 写入, 也会 添加 XML 声明。
10.7.2.3 将 XML 声明输出为字符串
若要将 XDocument 序列化为 string,且包含声明,需使用 Save 方法:
var doc =
new XDocument (
new XDeclaration ("1.0", "utf-8", "yes"),
new XElement ("test", "data")
);
var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };
using (XmlWriter xw = XmlWriter.Create (output, settings))
doc.Save (xw);
<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<test>data</test>
Warn
上述代码即使我们设置编码格式为 utf-8,实际输出的是 utf-16。因为我们输出的对象是
StringBuilder,编码必然是 utf-16。XmlWriter 会自动判断实际输出编码格式,这有效避免了编码格式错误导致的异常。正因此,为避免输出错误的编码格式,
XDocument.ToString 不会包含XDeclaration 内容:var doc = new XDocument ( new XDeclaration ("1.0", "utf-8", "yes"), new XElement ("test", "data") ); doc.ToString().Dump();输出: <test>data</test>
10.8 名称(Name)和 命名空间(namespace)
XML 的 namespace 用于避免 命名 冲突。例如 nil 可能有多种含义,但在 http://www.w3.org/2001/xmlschema-instance 命名空间下,表示 C# 中的 null。
10.8.1 XML 中的命名空间
XML 中的 namespace 通过 Attribute 声明:
<customer xmlns="http://domain.com/xmlspace">
<address>
<postcode>02138</postcode>
</address>
</customer>
上述 XML 中,address 和 postcode 也 属于 http://domain.com/xmlspace 命名空间。若不希望子节点继承父节点的命名空间,需显式的令子节点 namespace 为 空 :
<customer xmlns="http://domain.com/xmlspace">
<address xmlns="">
<postcode>02138</postcode>
</address>
</customer>
当然,我们也可以按照10.8.1.1 前缀(namespace 别名)中的方式,为父节点分配前缀。
Info
关于专门设为空的 namespace,我仅在 XAML 中见过一次这样的应用。见x:XData
10.8.1.1 前缀(namespace 别名)
以如下 XML 为例,一次性完成了两步操作(定义和使用):
-
xmlns:nut 定义了前缀 nut; -
nut:customer 将前缀分配至 当前 元素。
<nut:customer xmlns:nut="http://domain.com/xmlspace"/>
Notice
拥有前缀的元素,它的子元素 不会 自动使用相同的 namespace。在如下 XML 中,firstname 的 namespace 分别为 空 和 nut :
<nut:customer xmlns:nut="http://domain.com/xmlspace"> <firstname>Joe</firstname> </nut:customer><nut:customer xmlns:nut="http://domain.com/xmlspace"> <nut:firstname>Joe</nut:firstname> </customer>
在 XAML 中我们会同时引入多个 namespace,此时可以通过前缀区分不同 namespace 下的成员:
<Window x:Class="WpfApp1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WpfApp1" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> </Grid> </Window>
10.8.1.2 Attribute 与 namespace
XML 中的 Attribute 若要标记 namespace,必须通过前缀。如:
<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123" />
Warn
未用前缀限定的 Attribute 默认使用 空 的 namespace,它不从父元素继承默认 namespace。
一般来说,Attribute 是元素的本地特征,不需要 namespace。通用 Attribute、元数据 Attribute 例外,譬如之前提到的 W3C 中的 nil 代表了 C# 中的 null:
<customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<firstname>Joe</firstname>
<lastname xsi:nil="true" />
</customer>
10.8.2 在 X-DOM 中指定 namespace
为 X-DOM 添加 namespace 的方法有二:
本地名称前用 大括号 指定
以如下代码为例:
new XElement ("{http://domain.com/xmlspace}customer",
new XAttribute("{http://domain.com/xmlspace}id", "123"),
"Bloggs"
);
<customer p1:id="123"
xmlns:p1="http://domain.com/xmlspace"
xmlns="http://domain.com/xmlspace">
Bloggs
</customer>
使用 XNamespace
使用方式如下:
XNamespace ns = "http://domain.com/xmlspace";
new XElement(ns + "data",
new XAttribute(ns + "id", 456),
"123"
);
<data p1:id="456"
xmlns:p1="http://domain.com/xmlspace"
xmlns="http://domain.com/xmlspace">
123
</data>
XNamespace 和 XName 都定义了与 string 类型的隐式转换;XNamespace 还重载了 + 运算符,返回类型为 XName 。
X-DOM 的所有构造器和方法,都使用 XName 类型作为 Element/Attribute 的名称参数,因此我们可以使用 XNamespace + string 的方式传入参数。
10.8.3 X-DOM 和默认 namespace
在 X-DOM 中,不存在“继承 namespace”的概念,若想继承父项 namespace,每个成员都需要 显式指定 。而 X-DOM 在读取或输出 XML 时,若父子 namespace 相同,将 自动缺省子项的 namespace :
XNamespace ns = "http://domain.com/xmlspace";
var data =
new XElement (ns + "data",
new XElement (ns + "customer", "Bloggs"),
new XElement (ns + "purchase", "Bicycle")
);
<data xmlns="http://domain.com/xmlspace">
<customer>Bloggs</customer>
<purchase>Bicycle</purchase>
</data>
若父项指定了 namespace,子项未指定,子项的 namespace 会标记为 空 :
XNamespace ns = "http://domain.com/xmlspace";
var data =
new XElement (ns + "data",
new XElement ("customer", "Bloggs"),
new XElement ("purchase", "Bicycle")
);
<data xmlns="http://domain.com/xmlspace">
<customer xmlns="">Bloggs</customer>
<purchase xmlns="">Bicycle</purchase>
</data>
Warn
当成员的 namespace 不为 空 ,查找元素时传入的 Name 需包含 namespace 信息,例如:
XElement x = data.Element (ns + "customer"); // OK XElement y = data.Element ("customer"); // null
Suggest
上述指定 namespace 的方式显然很麻烦,我们可以在后期统一指定 namespace:
foreach (XElement e in data.DescendantsAndSelf()) if (e.Name.Namespace == "") e.Name = ns + e.Name.LocalName;
10.8.4 添加前缀
namespace 在 XML 中本质是 Attribute ,因此我们可以通过 XAttribute 为成员添加前缀。该 Attribute 的 Name 为 XNamespace.Xmlns + 别名 ,Value 为对应的 namespace。以如下 X-DOM 为例:
<data xmlns="http://domain.com/space1">
<element xmlns="http://domain.com/space2">value</element>
<element xmlns="http://domain.com/space2">value</element>
<element xmlns="http://domain.com/space2">value</element>
</data>
插入前缀方式为:
<ns1:data xmlns:ns1="http://domain.com/space1" xmlns:ns2="http://domain.com/space2">
<ns2:element>value</ns2:element>
<ns2:element>value</ns2:element>
<ns2:element>value</ns2:element>
</ns1:data>
XNamespace ns1 = "http://domain.com/space1";
XNamespace ns2 = "http://domain.com/space2";
var mix =
new XElement (ns1 + "data",
new XElement (ns2 + "element", "value"),
new XElement (ns2 + "element", "value"),
new XElement (ns2 + "element", "value")
);
// 插入 namespace
mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);
// 或
mix.Add(new XAttribute(XNamespace.Xmlns + "ns1", ns1));
mix.Add(new XAttribute(XNamespace.Xmlns + "ns2", ns2));
前缀对于 Attribute 同样有效:
XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
var nil = new XAttribute (xsi + "nil", true);
var cust =
new XElement ("customers",
//new XAttribute (XNamespace.Xmlns + "xsi", xsi),
new XElement ("customer",
new XElement ("lastname", "Bloggs"),
new XElement ("dob", nil),
new XElement ("credit", nil)
)
);
cust.SetAttributeValue(XNamespace.Xmlns + "xsi", xsi);
<customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<customer>
<lastname>Bloggs</lastname>
<dob xsi:nil="true" />
<credit xsi:nil="true" />
</customer>
</customers>
Tips
前缀的引入不会影响 X-DOM 内部的实际结构,它仅在输入、输出时才会用到(如序列化和反序列化)。
Error
虽然 namespace 的声明方式和 XAttribute 极其相似,但不可以用如下方式声明:
var mix = new XElement("data", new XAttribute("xmlns", "{http://domain.com/space1}"));
xmlns 是 xml 中的特殊关键字,上述代码mix 在使用时将抛出XmlException 异常。
xmlns 特性是 XML 中的一个特殊 特性 ,专门用于声明 namespace。
10.9 注解(Annotations)
注解用于存放私有数据,可以附加在任何的 XObject 上,X-DOM 将其视为黑盒。有如下方法操作注解对象:
// 添加或移除
public void AddAnnotation (object annotation)
public void RemoveAnnotations<T>() where T : class
// 检索
public T Annotation<T>() where T : class
public IEnumerable<T> Annotations<T>() where T : class
public T Annotation<T>() where T : class
public IEnumerable<T> Annotations<T>() where T : class
注解使用 Type 作为键(必须是引用类型)。用法如下:
XElement e = new XElement ("test");
e.AddAnnotation (new CustomData { Message = "Hello" } );
e.Annotations<CustomData>().First().Message.Dump();
e.RemoveAnnotations<CustomData>();
e.Annotations<CustomData>().Count().Dump();
class CustomData { internal string Message; }
Error
在10.3.3 自动深度克隆(Automatic Deep Cloning)中我们提到,
XObject 如果有父项,该节点赋值给其他父项时会进行深拷贝。但注解不参与该拷贝,它所在的节点进行拷贝时,新节点的注解为空。
10.10 将数据映射到 X-DOM #delay# 用不到,看不懂,剩余内容推迟再看
我们可以使用 LINQ 将数据从数据源映射至 X-DOM 中,只要该数据源支持 LINQ 查询。
例如我们要通过LINQ查询得到形如下方的 XML:
<customers>
<customer id="1">
<name>Sue</name>
<buys>3</buys>
</customer>
...
</customers>
var customers =
new XElement ("customers",
new XElement ("customer", new XAttribute ("id", 1),
new XElement ("name", "Sue"),
new XElement ("buys", 3)
)
);
在新版 EF 上的操作如下:
var customers =
new XElement ("customers",
from c in Customers.AsEnumerable()
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("name", c.Name),
new XElement ("buys", c.Purchases.Count)
)
);
// or
var sqlQuery =
from c in Customers.AsEnumerable()
select
new XElement ("customer", new XAttribute ("id", c.ID),
new XElement ("name", c.Name),
new XElement ("buys", c.Purchases.Count)
);
var customers = new XElement ("customers", sqlQuery);
<customers>
<customer id="1">
<name>Tom</name>
<buys>3</buys>
</customer>
<customer id="2">
<name>Harry</name>
<buys>2</buys>
</customer>
...
</customers>

浙公网安备 33010602011771号