XAML 节点流结构和概念

.NET XAML 服务中实现的 XAML 读取器和 XAML 编写器基于 XAML 节点流的设计概念。 XAML 节点流是一组 XAML 节点的概念化。 在此概念化中,XAML 处理器逐个演练 XAML 中节点关系的结构。 在任何时候,打开的 XAML 节点流中只存在一条当前记录或当前位置,API 的许多方面仅报告该位置提供的信息。 XAML 节点流中的当前节点可描述为对象、成员或值。 通过将 XAML 视为 XAML 节点流,XAML 读取器可以与 XAML 编写器通信,并允许程序在加载路径或涉及 XAML 的保存路径操作期间查看、交互或更改 XAML 节点流的内容。 XAML 读取器和编写器 API 设计和 XAML 节点流概念类似于以前的相关读取器和编写器设计和概念,例如 XML 文档对象模型(DOM)和 XmlReaderXmlWriter 类。 本主题讨论 XAML 节点流概念,并介绍如何在 XAML 节点级别编写与 XAML 表示形式交互的例程。

将 XAML 加载到 XAML 读取器中

XamlReader 类不声明将初始 XAML 加载到 XAML 读取器的特定技术。 相反,派生类声明并实现加载技术,包括 XAML 的输入源的一般特性和约束。 例如,XamlObjectReader 从表示根或基的单个对象的输入源开始读取对象图。 然后,XamlObjectReader 从对象图生成 XAML 节点流。

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

XAML 节点流概念

如果更熟悉 DOM、树隐喻或基于查询的方法来访问基于 XML 的技术,则概念化 XAML 节点流的方法如下。 假设加载的 XAML 是 DOM 或树,其中每个可能的节点都一直扩展,然后以线性方式呈现。 在浏览节点时,可能会遍历与 DOM 相关的级别“in”或“out”,但 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,调用以下任一项以获取有关节点内容的信息:

  • 调用 Read 将 XAML 读取器前进到 XAML 节点流中的下一个节点,然后再次重复这些步骤。

.NET 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 指令,作为成员处理的一部分。

  • 定义替代 Write* 方法的自定义 XamlObjectWriter,可能执行绕过 XAML 架构上下文的类型映射。

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

访问超出节点循环概念的 XAML

除了作为 XAML 节点循环外,还有一些使用 XAML 表示形式的其他方法。 例如,可能有一个 XAML 读取器可以读取索引节点,或者通过 x:Namex:Uid或其他标识符直接访问节点。 .NET XAML 服务不提供完整的实现,但通过服务和支持类型提供建议的模式。 有关详细信息,请参阅 IXamlIndexingReaderXamlNodeList

使用当前节点

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

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

在典型的保存路径方案中,XamlObjectReader 读取对象图、处理单个 XAML 节点,XamlXmlWriter 将序列化结果输出为 XAML 文本文件。 关键是路径和方案都涉及一次只处理一个 XAML 节点,并且 XAML 节点可用于采用由 XAML 类型系统和 the.NET XAML 服务 API 定义的标准化方式进行处理。

帧和范围

XAML 节点循环以线性方式遍历 XAML 节点流。 节点流将遍历到对象、包含其他对象的成员等中。 通过实现帧和堆栈概念来跟踪 XAML 节点流中的范围通常很有用。 如果在处于节点流中时主动调整节点流,则尤其如此。 作为节点循环逻辑的一部分实现的帧和堆栈支持可以计算 StartObject(或 GetObject)和 EndObject 范围,前提是从 DOM 的角度来看,该结构会进入 XAML 节点结构。

遍历和输入对象节点

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

请考虑以下 XAML 示例(这是任意 XAML,不受 .NET 中的现有类型支持)。 假设在此对象模型中,FavorCollectionList<T>FavorBalloonNoiseMaker 可分配给 FavorBalloon.Color 属性由类似于 WPF 定义颜色为已知颜色名称的方式的 Color 对象提供支持,Color 支持属性语法的类型转换器。

XAML 标记 生成的 XAML 节点流
<Party PartyNamespace 节点
xmlns="PartyXamlNamespace"> PartyStartObject 节点
<Party.Favors> Party.FavorsStartMember 节点
隐式 FavorCollectionStartObject 节点
隐式 FavorCollection 项属性的 StartMember 节点。
<Balloon BalloonStartObject 节点
Color="Red" ColorStartMember 节点

属性值字符串 "Red"Value 节点

ColorEndMember
HasHelium="True" HasHeliumStartMember 节点

属性值字符串 "True"Value 节点

HasHeliumEndMember
> BalloonEndObject
<NoiseMaker>Loudest</NoiseMaker> NoiseMakerStartObject 节点

_InitializationStartMember 节点

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

_InitializationEndMember 节点

NoiseMakerEndObject
隐式 FavorCollection 项属性的 EndMember 节点。
隐式 FavorCollectionEndObject 节点
</Party.Favors> FavorsEndMember
</Party> PartyEndObject

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

  • 如果存在 Namespace 节点,则会在声明具有 xmlns的 XAML 命名空间的 StartObject 之前立即将其添加到流中。 再次查看上表,其中包含 XAML 和示例节点流。 请注意 StartObjectNamespace 节点在文本标记中如何转置与其声明位置。 这是命名空间节点始终出现在节点流中应用到的节点之前的行为。 此设计的目的是命名空间信息对对象编写器至关重要,并且必须知道对象编写器尝试执行类型映射或其他处理对象之前。 将 XAML 命名空间信息置于流中的应用程序范围之前,使始终按所呈现的顺序处理节点流变得更简单。

  • 由于上述考虑,在从头遍历节点而不是根 StartObject 时,大多数实际标记事例中首先读取的一个或多个 Namespace 节点。

  • StartObject 节点后跟 StartMemberValue或直接 EndObject。 它永远不会立即跟随另一个 StartObject

  • StartMember 后跟 StartObjectValue或直接 EndMember。 它后跟 GetObject,对于值应来自父对象的现有值而不是实例化新值的 StartObject 的成员。 它也可以后跟一个 Namespace 节点,该节点适用于即将推出的 StartObject。 它永远不会立即跟随另一个 StartMember

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

    • 构造可能使用的对象的 XAML 初始化文本不会导致 Object-Value 结构。 而是创建名为 _Initialization 的成员的专用成员节点。 并且该成员节点包含初始化值字符串。 如果存在,_Initialization 始终是第一个 StartMember_Initialization 可能在某些 XAML 服务表示形式中使用 XAML 语言 XAML 名称范围进行限定,以阐明 _Initialization 不是支持类型中定义的属性。

    • Member-Value 组合表示值的属性设置。 最终,处理此值时可能涉及值转换器,并且该值是纯字符串。 但是,在 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 节点流的文本表示形式可以如下所示:

表示 GameBoardXamlTypeStartObject

表示 BoardSizeXamlMemberStartMember

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

EndMember 匹配 BoardSize

EndObject 匹配 GameBoard

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

XAML 节点流中的标记扩展

标记扩展用法在 XAML 节点流中报告为成员中的对象节点,其中该对象表示标记扩展实例。 因此,与类型转换器用法相比,在节点流表示形式中更显式地呈现标记扩展用法,并且包含更多信息。 XamlMember 信息无法告知你有关标记扩展的任何信息,因为每个可能标记情况下的使用都是情况性的,并且会有所不同;它不是专用的,也不是每个类型或成员的隐式类型,与类型转换器的情况一样。

标记扩展作为对象节点的节点流表示形式是这种情况,即使标记扩展用法是在 XAML 文本标记中以属性形式进行的(通常是这种情况)。 对使用显式对象元素窗体的标记扩展用法的处理方式相同。

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

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

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

尚未从标记扩展调用 ProvideValue。 但是,如果连接 XAML 读取器和 XAML 编写器,以便在节点流中检查标记扩展节点上时调用 WriteEndObject,则会调用它。 因此,通常需要与用于在加载路径上形成对象图相同的 XAML 架构上下文。 否则,从任何标记扩展 ProvideValue 可能会在此处引发异常,因为它没有可用的预期服务。

XAML 和 XML Language-Defined XAML 节点流中的成员

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

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

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

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

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

  • 集合的集合属性: 尽管用于 XAML 的集合类的后盾 CLR 类型通常具有一个专用的命名属性来保存集合项,但在支持类型解析之前,该属性对 XAML 类型系统未知。 相反,XAML 节点流将 Items 占位符作为集合 XAML 类型的成员引入。 在 .NET XAML 服务实现中,节点流中此指令或成员的名称 _Items。 可以从 Items获取此指令的常量。

    请注意,XAML 节点流可能包含一个 Items 属性,这些属性根据支持类型解析和 XAML 架构上下文而无法分析。 例如

  • XML 定义的成员: XML 定义的 xml:basexml:langxml:space 成员报告为 .NET XAML Services 实现中名为 baselangspace 的 XAML 指令。 这些命名空间是 XML 命名空间 http://www.w3.org/XML/1998/namespace。 可以从 XamlLanguage获取其中每个常量。

节点顺序

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

某些指令旨在提供有关从对象元素创建对象的详细信息。 这些指令包括:InitializationPositionalParametersTypeArgumentsFactoryMethodArguments。 由于下一部分中介绍的原因,.NET XAML 服务 XAML 读取器尝试将这些指令作为节点流中的第一个成员放在 StartObject之后。

XamlObjectWriter 行为和节点顺序

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

GetObject

GetObject 表示 XAML 节点,而不是构造新对象,XAML 对象编写器应改为获取对象的包含属性的值。 在 XAML 节点流中遇到 GetObject 节点的典型情况是集合对象或字典对象,当包含属性在后盾类型的对象模型中故意只读时。 在此方案中,集合或字典通常由拥有类型的初始化逻辑创建和初始化(通常为空)。

另请参阅