了解 XAML 节点流结构和概念

在 .NET Framework XAML 服务中实现的 XAML 读取器和 XAML 编写器基于 XAML 节点流的设计概念。 XAML 节点流的概念就是一组 XAML 节点。 在此概念中,XAML 处理器在 XAML 中遍历这些节点关系的结构,每次处理一个节点。 在任何时候,一个打开的 XAML 节点流中仅存在一个当前记录或当前位置,API 的许多方面仅报告可从该位置获得的信息。 可以将 XAML 节点流中的当前节点描述为一个对象、一个成员或一个值。 通过将 XAML 视为 XAML 节点流,XAML 读取器可以与 XAML 编写器通信,并且使程序能够在涉及 XAML 的加载路径或保存路径操作期间查看、改变 XAML 节点流的内容或与之交互。 XAML 读取器和编写器 API 设计以及 XAML 节点流概念类似于以前的相关读取器和编写器设计及概念,如 XML Document Object Model (DOM) 以及 XmlReaderXmlWriter 类。 本主题介绍 XAML 节点流概念并介绍如何在 XAML 节点级别编写与 XAML 表示形式交互的例程。

本主题包括下列各节。

  • 将 XAML 加载到 XAML 读取器中
  • 基本的读取节点循环
  • 处理当前节点
  • 遍历并进入对象节点
  • 值转换器和 XAML 节点流
  • XAML 节点流中的 XAML 和 XML 语言定义的成员
  • 节点顺序
  • 相关主题

将 XAML 加载到 XAML 读取器中

基本的 XamlReader 类不声明将初始 XAML 加载到 XAML 读取器中的特定方法。 而是由派生类声明和实现加载技术,包括 XAML 的输入源的常规特征和约束。 例如,XamlObjectReader 从表示根或基的单个对象的输入源开始读取对象图。 XamlObjectReader 随后从对象图生成 XAML 节点流。

定义了 XamlReader 子类的最突出的 .NET Framework XAML 服务是 XamlXmlReaderXamlXmlReader 直接通过流或文件路径或间接通过相关读取器类(如 TextReader)来加载文本文件,从而加载初始 XAML。 可以将 XamlReader 视为包含整个 XAML 输入源(在该输入源加载之后)。 但是,设计了 XamlReader 基本 API 以便读取器与单个 XAML 节点交互。 首次加载后,您遇到的第一个节点是 XAML 的根节点及其起始对象。

XAML 节点流概念

如果您通常更熟悉 DOM、树形式或用于访问基于 XML 的技术的基于查询的方法,则一种概念化 XAML 节点流的有用方式如下所示。 假设加载的 XAML 是 DOM 或树,其中每个可能节点都完全展开,随后线性呈现。 在这些节点中前进时,可能会遍历“进入”或“退出”与 DOM 相关的级别,但是 XAML 节点流不显式进行跟踪,因为这些级别概念与节点流无关。 节点流具有“当前”位置,但除非您自己将流的其他部分存储为引用,否则节点流中除当前节点位置之外的所有方面都会处于视图之外。

XAML 节点流概念具有显著优点:如果您遍历整个节点流,则可以确保处理整个 XAML 表示形式;您无需担心查询、DOM 操作或用于处理信息的某种其他非线性方法会遗漏完整 XAML 表示形式的某个部分。 因此,XAML 节点流表示形式适用于连接 XAML 读取器和 XAML 编写器,同时又适用于提供一个系统,您可在该系统中插入自己的进程,而该进程在 XAML 处理操作的读取与写入阶段之间起作用。 在许多情况下,XAML 节点流中节点的顺序已特意由 XAML 读取器针对节点在源文本、二进制文件或对象图中出现的顺序进行优化或重新排序。 此行为旨在强制实施一种 XAML 处理体系结构,借助该体系结构,XAML 编写器永远不会处于一种必须在节点流中“后退”的位置。 理想情况下,所有 XAML 写入操作都应能够基于架构上下文以及节点流的当前位置来执行。

基本的读取节点循环

用于检查 XAML 节点流的基本读取节点循环包含以下概念。 出于本主题中所述的节点循环的目的,我们假定您使用 XamlXmlReader 读取基于文本的、用户可读的 XAML 文件。 本节中的链接指向由 XamlXmlReader 实现的特定 XAML 节点循环 API。

  • 确保您不在 XAML 节点流的末尾(检查 IsEof 或使用 Read() 返回值)。 如果您在节点流的末尾,则没有当前节点,您应该退出。

  • 通过调用 NodeType 检查 XAML 节点流当前公开的节点类型。

  • 如果您具有直接连接的关联 XAML 对象编写器,则通常可在此时调用 WriteNode

  • 根据将哪个 XamlNodeType 报告为当前节点或当前记录,调用以下内容之一以获取有关节点内容的信息:

    • 对于 StartMemberEndMemberNodeType,调用 Member 以获取有关成员的 XamlMember 信息。 请注意,成员可能是 XamlDirective,因而可能不一定是前一个对象的常规类型定义的成员。 例如,应用于对象的 x:Name 显示为 XAML 成员,其中 IsDirective 为 true,并且该成员的 Name 为 Name,还包含指示此指令处于 XAML 语言 XAML 命名空间中的其他属性。

    • 对于 StartObjectEndObjectNodeType,调用 Type 以来获取有关对象的 XamlType 信息。

    • 对于 ValueNodeType,调用 Value。 仅当节点是成员的最简单值表达式或对象的初始化文本时,该节点才为值(但是,应注意本主题后面部分中介绍的类型转换行为)。

    • 对于 NamespaceDeclarationNodeType,调用 Namespace 以获取命名空间节点的命名空间信息。

  • 调用 Read 将 XAML 读取器推进到 XAML 节点流中的下一个节点并重复这些步骤。

由 .NET Framework XAML 服务 XAML 读取器提供的 XAML 节点流始终提供所有可能节点的完全深层遍历。 XAML 节点循环的典型流控制方法包括在 while (reader.Read()) 内定义正文以及在节点循环中每个节点的 NodeType 上切换。

如果节点流位于文件的末尾,则当前节点为 null。

使用读取器和编写器的最简单循环类似下面的示例。

XamlXmlReader xxr = new XamlXmlReader(new StringReader(xamlStringToLoad));
//where xamlStringToLoad is a string of well formed XAML
XamlObjectWriter xow = new XamlObjectWriter(xxr.SchemaContext);
while (xxr.Read()) {
  xow.WriteNode(xxr);
}

加载路径 XAML 节点循环的这个基本示例透明地连接 XAML 读取器和 XAML 编写器,与使用 XamlServices.Parse 没有什么不同。 但是,之后要扩展这个基本结构以应用于读取或编写方案。 一些可能的方案如下:

  • 开启 NodeType。 根据读取的节点类型执行不同的操作。

  • 不要在所有情况下都调用 WriteNode。 仅在某些 NodeType 情况下才调用 WriteNode

  • 在特定节点类型的逻辑中,分析该节点的具体信息并相应地执行操作。 例如,可以只编写来自特定 XAML 命名空间的对象,然后丢弃或推迟非来自该 XAML 命名空间的任何对象。 或者也可以丢弃或重新处理 XAML 系统不作为成员处理的一部分而支持的任何 XAML 指令。

  • 定义一个自定义 XamlObjectWriter,它重写 Write* 方法,从而有可能执行跳过 XAML 架构上下文的类型映射。

  • 构造 XamlXmlReader 以使用非默认 XAML 架构上下文,以便读取器和编写器都使用 XAML 行为中的自定义差异。

节点循环概念之外的 XAML 访问方法

除了 XAML 节点循环之外,可能还有其他处理 XAML 表示形式的方法。 例如,可能存在一个 XAML 读取器,它可以读取索引节点,尤其可以直接通过 x:Name、x:Uid 或通过其他标识符访问节点。 .NET Framework XAML 服务不提供完整实现,但通过服务和支持类型提供建议模式。 有关更多信息,请参见 IXamlIndexingReaderXamlNodeList

提示提示

Microsoft 还生产带外版本(称为 Microsoft XAML 工具包)。此带外版本仍处于其预发布阶段。不过,如果您愿意使用预发布组件,则 Microsoft XAML 工具包可为 XAML 工具和 XAML 的静态分析提供一些令人感兴趣的资源。Microsoft XAML 工具包包含 XAML DOM API、对 FxCop 分析的支持以及用于 Silverlight 的 XAML 架构上下文。有关更多信息,请参见 Microsoft XAML Toolkit

处理当前节点

使用 XAML 节点循环的大多数方案不仅仅读取节点。 大多数方案会处理当前节点,并将每个节点传递给 XamlWriter 实现,一次传递一个节点。

在典型的加载路径方案中,XamlXmlReader 会生成一个 XAML 节点流;将根据您的逻辑和 XAML 架构上下文来处理 XAML 节点;然后将这些节点传递给 XamlObjectWriter。 然后,将产生的对象图集成到应用程序或框架中。

在典型的保存路径方案中,XamlObjectReader 读取对象图,处理各个 XAML 节点,然后 XamlXmlWriter 以 XAML 文本文件的形式输出序列化的结果。 关键在于这两个路径和方案都涉及每次只处理一个 XAML 节点,并且可以采用由 XAML 类型系统和 .NET Framework XAML 服务 API 定义的标准化方式处理 XAML 节点。

框架和范围

XAML 节点循环采用线性方法遍历 XAML 节点流。 节点流遍历到对象、到包含其他对象的成员等。 通常,通过实现框架和堆栈概念在 XAML 节点流中跟踪范围非常有用。 当在节点流中积极调整该节点流时,尤其如此。 当您向下进入 XAML 节点结构(如果从 DOM 角度看该结构)时,作为节点循环逻辑的一部分实现的框架和堆栈支持可能会计算 StartObject(或 GetObject)和 EndObject 范围。

遍历并进入对象节点

当 XAML 读取器打开某个节点流时,该节点流中的第一个节点是根对象的起始对象节点。 根据定义,此对象始终是单个对象节点,没有对等项。 在任何实际的 XAML 示例中,根对象会定义为拥有包含多个对象的一个或多个属性,并且这些属性会拥有成员节点。 这些成员节点又拥有一个或多个对象节点,或者也可能终止于值节点。 根对象通常定义 XAML 名称范围,这些名称范围在语法上作为 XAML 文本标记中的特性进行分配,但是映射到 XAML 节点流表示形式中的 Namescope 节点类型。

考虑下面的 XAML 示例(此示例是任意 XAML,不受 .NET Framework 中现有类型的支持)。 假定在此对象模型中,FavorCollection 为 Favor 的 List<T>,Balloon 和 NoiseMaker 可赋值给 Favor,Balloon.Color 属性受 Color 对象支持(类似于 WPF 定义称为颜色名称的颜色的方式),并且 Color 支持类型转换器使用特性语法。

XAML 标记

得到的 XAML 节点流

<Party

Party 的 Namespace 节点

xmlns="PartyXamlNamespace">

Party 的 StartObject 节点

  <Party.Favors>

Party.Favors 的 StartMember 节点

隐式 FavorCollection 的 StartObject 节点

隐式 FavorCollection 项属性的 StartMember 节点。

    <Balloon

Balloon 的 StartObject 节点

      Color="Red"

Color 的 StartMember 节点

  特性值字符串 "Red" 的 Value 节点

Color 的 EndMember

      HasHelium="True"

HasHelium 的 StartMember 节点

  特性值字符串 "True" 的 Value 节点

HasHelium 的 EndMember

    >

Balloon 的 EndObject

    <NoiseMaker>Loudest</NoiseMaker>

NoiseMaker 的 StartObject 节点

_Initialization 的  StartMember 节点

    初始化值字符串 "Loudest" 的 Value 节点

_Initialization 的  EndMember 节点

NoiseMaker 的 EndObject

隐式 FavorCollection 项属性的 EndMember 节点。

隐式 FavorCollection 的 EndObject 节点

  </Party.Favors>

Favors 的 EndMember

</Party>

Party 的 EndObject

在 XAML 节点流中,可以依赖于以下行为:

  • 如果存在 Namespace 节点,则该节点会添加到该流中,恰好在使用 xmlns 声明 XAML 命名空间的 StartObject 之前。 再次查看列有 XAML 和示例节点流的上表。 注意 StartObject 和 Namespace 节点是如何相对于其在文本标记中的声明位置而发生转置的。 这是具有代表性的行为,其中命名空间节点始终出现在节点流中应用它们的节点的前面。 此设计的目的在于,命名空间信息对于对象编写器来说至关重要,因此必须在对象编写器尝试执行类型映射或者以其他方式处理对象之前了解此信息。 将 XAML 命名空间信息放在流中其应用程序范围的前面,这样更易于始终按其出现的顺序处理节点流。

  • 由于上述注意事项,因此在大多数实际的标记情况下从头遍历节点时,首先读取的是一个或多个 Namespace 节点,而不是根的 StartObject 节点。

  • StartObject 节点后面可以跟 StartMember、Value 或紧跟 EndObject。 它后面从不会紧跟另一个 StartObject。

  • StartMember 后面可以跟 StartObject、Value 或紧跟 EndMember。 它的后面可以跟 GetObject(对于值应来自父对象的现有值而不是来自将实例化新值的 StartObject 的成员)。 它的后面还可以跟 Namespace 节点,该节点应用于即将到来的 StartObject。 它后面从不会紧跟另一个 StartMember。

  • Value 节点表示值本身;没有“EndValue”。 它后面只能跟 EndMember。

    • 可能由构造使用的对象的 XAML 初始化文本不会产生对象-值结构, 而是会为名为 _Initialization 的成员创建一个专用的成员节点, 并且该成员节点包含初始化值字符串。 如果 _Initialization 存在,则它始终是第一个 StartMember。 _Initialization 在某些具有 XAML 语言 XAML 名称范围的 XAML 服务表示形式中可能是限定的,目的是表明该 _Initialization 不是在后备类型中定义的属性。

    • 成员-值组合表示该值的特性设置。 最终可能会在处理此值时涉及值转换器,并且值为纯字符串。 但是,在 XAML 对象编写器处理此节点流之前,不会对它进行计算。 XAML 对象编写器拥有必需的 XAML 架构上下文、类型系统映射以及值转换所需的其他支持。

  • EndMember 节点后面可以跟后续成员的 StartMember 节点,或者跟成员所有者的 EndObject 节点。

  • EndObject 节点后面可以跟 EndMember 节点。 它后面还可以跟 StartObject 节点,这适于多个对象属于集合中的对等项的情况。 或者,它的后面可以跟 Namespace 节点,该节点应用于即将到来的 StartObject。

    • 对于唯一的关闭整个节点流的情况,根的 EndObject 后面不跟任何内容;读取器现在已到达文件尾,Read 将返回 false。

值转换器和 XAML 节点流

值转换器是用于标记扩展、类型转换器(包括值序列化程序)或另一个专用类(通过 XAML 类型系统将该类报告为值转换器)的泛称。 在 XAML 节点流中,类型转换器用法与标记扩展用法有非常不同的表示形式。

XAML 节点流中的类型转换器

最终导致类型转换器用法的特性集会在 XAML 节点流中报告为成员的一个值。 XAML 节点流不会尝试生成类型转换器实例对象并向其传递值。 使用类型转换器的转换实现需要调用 XAML 架构上下文并将其用于类型映射。 即使确定了应该使用哪个类型转换器类来处理值,也间接需要 XAML 架构上下文。 在使用默认 XAML 架构上下文时,可以从 XAML 类型系统获取该信息。 如果在连接到 XAML 编写器之前需要 XAML 节点流级别的类型转换器类信息,则可以从所设置的成员的 XamlMember 信息获取该信息。 除此之外,类型转换器输入应在 XAML 节点流中保留为纯值,直至执行了需要类型映射系统和 XAML 架构上下文的剩余操作,例如由 XAML 对象编写器执行的对象创建。

例如,考虑以下类定义大纲及其 XAML 用法:

  public class BoardSizeConverter : TypeConverter {
    //converts from string to an int[2] by splitting on an "x" char
  }
  public class GameBoard {
    [TypeConverter(typeof(BoardSizeConverter))]
    public int[] BoardSize; //2x2 array, initialization not shown
  }
  <GameBoard BoardSize="8x8"/>

可以将此用法的 XAML 节点流的文本表示形式表示为以下内容:

StartObject,其中 XamlType 表示 GameBoard

StartMember,其中 XamlMember 表示 BoardSize

Value 节点,具有文本字符串“8x8”

EndMember,与 BoardSize 匹配

EndObject,与 GameBoard 匹配

请注意,在此节点流中没有任何类型转换器实例。 但是对于 BoardSize,可以通过对 XamlMember 调用 XamlMember.TypeConverter 来获取类型转换器信息。 如果您拥有有效的 XAML 架构上下文,则还可以通过从 ConverterInstance 获取实例来调用转换器方法。

XAML 节点流中的标记扩展

在 XAML 节点流中,标记扩展用法将报告为成员中的一个对象节点,其中该对象表示一个标记扩展实例。 因此,与类型转换器用法相比,标记扩展用法在节点流表示形式中的表示得更加明确,并且会携带更多信息。 XamlMember 信息无法告诉您有关标记扩展的任何内容,因为该用法是有语境的,并且在每个可能的标记情况下都有所不同;与类型转换器一样,对于每个类型或成员,它不是专用的也不是隐式的。

作为对象节点的标记扩展的节点流表示形式也是如此,即使在 XAML 文本标记中以特性形式使用了标记扩展用法(通常是这种情况)。 采用同样方式处理使用了显式对象元素形式的标记扩展用法。

在标记扩展对象节点中,可能存在该标记扩展的成员。 XAML 节点流表示形式保留该标记扩展的用法,而不管它是位置参数用法还是使用显式命名参数的用法。

对于位置参数用法,XAML 节点流包含一个 XAML 语言定义的属性 _PositionalParameters,用于记录用法。 此属性是具有 Object 约束的泛型 List<T>。 该约束是对象而不是字符串,因为无疑位置参数用法可以在其中包含嵌套的标记扩展用法。 若要从用法访问位置参数,可以循环访问该列表并对各个列表值使用索引器。

对于命名参数用法,每个命名参数表示为节点流中该名称的成员节点。 成员值不一定是字符串,因为有可能存在嵌套的标记扩展用法。

尚不调用来自标记扩展的 ProvideValue。 但在以下情况下也会调用它:您连接 XAML 读取器和 XAML 编写器,以便在节点流中检查到相应情况时对标记扩展节点调用 WriteEndObject。 出于这个原因,通常需要使用相同的 XAML 架构上下文,以供在加载路径上形成对象图时使用。 否则,来自任何标记扩展的 ProvideValue 可能会在此处引发异常,因为它没有可用的预期服务。

XAML 节点流中的 XAML 和 XML 语言定义的成员

由于 XAML 读取器的解释和转换,向 XAML 节点流中引入了某些成员,而不是通过显式 XamlMember 查找或构造。 通常,这些成员称为 XAML 指令。 在某些情况下,正是由读取 XAML 这一操作将指令引入 XAML 节点流。 换言之,原始输入 XAML 文本并未显式指定成员指令,但是 XAML 读取器会插入指令,以便满足结构 XAML 约定并在丢失 XAML 节点流中的信息之前报告该信息。

下面的列表说明了 XAML 读取器应引入指令 XAML 成员节点的所有情况,以及如何在 .NET Framework XAML 服务实现中标识该成员节点。

  • **对象节点的初始化文本:**此成员节点的名称为 _Initialization,它表示 XAML 指令,并在 XAML 语言 XAML 命名空间中定义。 可以从 Initialization 获取它的静态实体。

  • **标记扩展的位置参数:**此成员节点的名称为 _PositionalParameters,它在 XAML 语言 XAML 命名空间中定义。 它始终包含对象的泛型列表,其中每个对象都是位置参数,这些参数由输入 XAML 中提供的 , 分隔符字符预先分隔。 可以从 PositionalParameters 获取位置参数指令的静态实体。

  • **未知内容:**此成员节点的名称为 _UnknownContent。 严格来说,它是 XamlDirective,它在 XAML 语言 XAML 命名空间中定义。 对于 XAML 对象元素包含源 XAML 中的内容,但无法在当前可用的 XAML 架构上下文中确定任何内容属性的情况,此指令用作 sentinel。 通过检查名为 _UnknownContent 的成员,可以在 XAML 节点流中检测这种情况。 如果在加载路径 XAML 节点流中未采取任何其他措施,则在任何对象上遇到 _UnknownContent 成员时,会对所尝试的 WriteEndObject 引发默认 XamlObjectWriter。 默认的 XamlXmlWriter 不引发,并且将成员视为隐式。 可以从 UnknownContent 获取 _UnknownContent 的静态实体。

  • **集合的集合属性:**虽然用于 XAML 的集合类的后备 CLR 类型通常具有一个保存集合项的专用命名属性,但是在进行后备类型解析之前,该属性对于 XAML 类型系统是未知的。 XAML 节点流会引入一个 Items 占位符以作为集合 XAML 类型的成员。 在 .NET Framework XAML 服务实现中,节点流中的此指令/成员的名称是 _Items。 可以从 Items 获取此指令的常量。

    请注意,XAML 节点流可能会包含一个 Items 属性,它含有经证明不能基于后备类型解析和 XAML 架构上下文进行分析的项。 例如,

  • **XML 定义的成员:**XML 定义的 xml:base、xml:lang 和 xml:space 成员在 .NET Framework XAML 服务实现中分别报告为名为 base、lang 和 space 的 XAML 指令。 它们的命名空间为 XML 命名空间 http://www.w3.org/XML/1998/namespace。 可以从 XamlLanguage 获取它们中每一个的常量。

节点顺序

在某些情况下,XamlXmlReader 会更改 XAML 节点流中 XAML 节点的顺序,该顺序对应于节点在标记中进行查看或作为 XML 进行处理时出现的顺序。 这样做是为了对节点排序,以便 XamlObjectWriter 能够以仅向前的方式处理节点流。 在 .NET Framework XAML 服务中,XAML 读取器会对节点重新排序而不是将此任务留给 XAML 编写器,以便优化节点流的 XAML 对象编写器使用者的性能。

某些指令专门用于为从对象元素创建对象提供更多信息。 这些指令是:Initialization、PositionalParameters、TypeArguments、FactoryMethod、Arguments。 .NET Framework XAML 服务 XAML 读取器会尝试将这些指令作为前几个成员置于跟在对象的 StartObject 之后的节点流中(下一节说明这样做的原因)。

XamlObjectWriter 行为和节点顺序

传递给 XamlObjectWriter 的 StartObject 不一定是指示 XAML 对象编写器立即构造对象实例的信号。 XAML 包含一些语言功能,通过这些功能,可使用其他输入来初始化对象,而不是完全依赖于调用默认构造函数来生成初始对象,然后仅设置属性。 这些功能包括:XamlDeferLoadAttribute;初始化文本;x:TypeArguments;标记扩展的位置参数;工厂方法和关联 x:Arguments 节点 (XAML 2009)。 其中每种情况都会延迟实际对象构造,并且因为节点流会重新排序,所以 XAML 对象编写器可以依赖于以下行为:每当遇到不是明确作为该对象类型的构造指令的起始成员时,便实际构造实例。

GetObject

GetObject 表示 XAML 节点,其中 XAML 对象编写器应获取对象的包含属性值,而不是构造新对象。 在 XAML 节点流中遇到 GetObject 节点的典型情况是针对集合对象或字典对象(当包含属性在后备类型的对象模型中特意作为只读属性时)。 在这种情况下,集合或字典通常由所属类型的初始化逻辑来创建和初始化(通常为空)。

请参见

参考

XamlObjectReader

概念

XAML 服务

其他资源

.NET Framework XAML 服务的 XAML 命名空间