WPF XAML 名称范围
XAML 名称范围是关于标识 XAML 中定义的对象的一个概念。 XAML 名称范围中的名称可用于在对象树的对象 XAML 定义名称和其实例等效项之间建立关系。 通常,在加载 XAML 应用程序的各个 XAML 页面根时,会在 WPF 托管代码中创建 XAML 名称范围。 作为编程对象的 XAML 名称范围由 INameScope 接口定义,且由实际类 NameScope 实现。
加载的 XAML 应用程序中的名称范围
从更广泛的编程或计算机科学来说,编程概念通常包括可用于访问对象的唯一标识符或名称的原则。 对于使用标识符或名称的系统,名称范围会定义边界,在该边界中,进程或技术会搜索是否请求了具有该名称的对象或者是否执行了标识名称唯一性。 这些一般原则适用于 XAML 名称范围。 在 WPF 中,当 XAML 页面加载时,会在该页面的根元素上创建 XAML 名称范围。 在 XAML 页面的页面根位置处指定的每个名称会添加到相关的 XAML 名称范围中。
在 WPF XAML 中,作为公共根元素的元素(例如 Page 和 Window)总是控制一个 XAML 名称范围。 如果元素(如 FrameworkElement 或 FrameworkContentElement)是标记中页面的根元素,则 XAML 处理器会隐式添加 Page 根,以便 Page 可以提供一个有效的 XAML 名称范围。
注意
即使在 XAML 标记中的任何元素上未定义 Name
或 x:Name
属性,WPF 生成操作也会为 XAML 产品创建 XAML 名称范围。
如果尝试在任何 XAML 名称范围中两次使用相同的名称,将引发异常。 对于具有代码隐藏且作为已编译应用程序的一部分的 WPF XAML,在初始标记编译期间为页面创建生成类时,WPF 生成操作在生成时会引发异常。 对于不由任何生成操作进行标记编译的 XAML,加载 XAML 时可能会引发与 XAML 名称范围问题相关的异常。 XAML 设计器在设计时也可能会出现 XAML 名称范围问题。
将对象添加到运行时对象树
分析 XAML 时即意味着创建并定义 WPF XAML 名称范围的时刻。 如果在分析生成对象树的 XAML 之后的时间点将对象添加到对象树,新对象上的 Name
或 x:Name
值不会在 XAML 名称范围中自动更新信息。 若要在加载 XAML 后将对象的名称添加到 WPF XAML 命名范围中,则必须在定义 XAML 名称范围的对象(通常为 XAML 页面根)上调用 RegisterName 的适当实现。 如果未注册该名称,则不能通过 FindName 等方法使用名称引用已添加的对象,且无法将该名称用于动画定位。
对于应用程序开发人员来说最常见的方案是使用 RegisterName 将名称注册到页面当前根的 XAML 名称范围中。 RegisterName 是情节提要(针对动画目标对象)的重要场景的一部分。 有关详细信息,请参阅情节提要概述。
如果对并非定义 XAML 名称范围的对象调用 RegisterName,则该名称仍会注册到调用对象所在的 XAML 名称范围,就像在 XAML 名称范围中调用 RegisterName 定义对象一样。
代码中的 XAML 名称范围
可以通过代码创建然后使用 XAML 名称范围。 创建 XAML 名称范围所涉及的 API 和概念都是相同的,甚至是使用纯代码,因为 WPF 的 XAML 处理器在处理 XAML 本身就会使用这些 API 和概念。 这些概念和 API 存在的主要目的是为了能够在对象树中按名称查找对象(此对象树通常在 XAML 中完全或部分定义)。
对于以编程方式而不是通过加载的 XAML 创建的应用程序,定义 XAML 名称范围的对象必须实现 INameScope,或者为 FrameworkElement 或 FrameworkContentElement 派生类,以便支持在其实例上创建 XAML 名称范围。
此外,对于任何不由 XAML 处理器加载和处理的元素,默认情况下不会创建或初始化对象的 XAML 名称范围。 必须为随后要将名称注册到其中的任何对象显式创建新的 XAML 名称范围。 要创建 XAML 名称范围,调用 SetNameScope 静态方法。 指定对象将其作为 dependencyObject
参数,并将新的 NameScope 构造函数作为 value
参数调用。
如果提供的对象作为 SetNameScope 的 dependencyObject
不是 INameScope 实现,FrameworkElement 或 FrameworkContentElement,在任何子元素上调用 RegisterName 都将不起作用。 如果未能成功显式创建新的 XAML 名称范围,则对 RegisterName 的调用将引发异常。
有关以代码方式使用 XAML 名称范围 API 的示例,请参阅定义名称范围。
样式和模板中的 XAML 名称范围
通过 WPF 中的样式和模板,可以直接重复使用和重复应用内容。 但是,样式和模板可能还包含具有模板级别定义的 XAML 名称的元素。 此相同模板可在一个页面中多次使用。 出于此原因,样式和模板皆定义其自身的 XAML 名称范围,与在应用样式或模板的对象树中的位置无关。
请考虑以下示例:
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
>
<Page.Resources>
<ControlTemplate x:Key="MyButtonTemplate" TargetType="{x:Type Button}">
<Border BorderBrush="Red" Name="TheBorder" BorderThickness="2">
<ContentPresenter/>
</Border>
</ControlTemplate>
</Page.Resources>
<StackPanel>
<Button Template="{StaticResource MyButtonTemplate}">My first button</Button>
<Button Template="{StaticResource MyButtonTemplate}">My second button</Button>
</StackPanel>
</Page>
此处,同一模板应用到两个不同的按钮。 如果模板不具有离散的 XAML 名称范围,则模板中使用的 TheBorder
名称会导致 XAML 名称范围中的名称冲突。 模板的每个实例都具有其自己的 XAML 名称范围,因此在本例中,每个实例化模板的 XAML 名称范围仅包含一个名称。
样式也定义其自身的 XAML 名称范围,因此情节提要的各部分均可分配有特定的名称。 即使在控件自定义过程中重新定义模板,这些名称也可实现控件特定行为,定位具有该名称的元素。
由于这些分开的 XAML 名称范围,在模板中查找命名元素比在页面中查找非模板命名元素难度更大。 首先需通过获取控件(应用了模板)的 Template 属性值来确定已应用的模板。 然后,调用 FindName 的模板版本,将应用了模板的控件作为第二个参数传递。
如果你是控件作者且要生成一个约定,在该约定中,已应用模板中的特定命名元素是控件本身所定义行为的目标,则可通过控件实现代码使用 GetTemplateChild 方法。 GetTemplateChild 方法受到保护,因此只有控件作者有权访问它。
如果在模板内使用,并且需要进入应用了模板的 XAML 名称范围,则会获取 TemplatedParent 的值,随后在那里调用 FindName。 从模板内入手的一个示例是编写事件将从已应用模板中的元素引发的事件处理程序实现。
XAML 名称范围和与名称相关的 API
FrameworkElement 具有 FindNameRegisterName 和 UnregisterName 方法。 如果在其上调用这些方法的对象拥有 XAML 名称范围,则这些方法会调入相关 XAML 名称范围的方法。 否则,将检查父元素以查看其是否拥有 XAML 名称范围,此过程以递归方式持续发生,直到找到 XAML 名称范围(由于 XAML 处理器行为,根处必定存在一个 XAML 名称范围)。 FrameworkContentElement 具有类似的行为,例外是没有 FrameworkContentElement 曾拥有一个 XAML 名称范围。 该方法存在于 FrameworkContentElement,因此最终可将调用转接给 FrameworkElement 父元素。
SetNameScope 用于将新的 XAML 名称范围映射到现有对象。 可多次调用 SetNameScope 来重置或清除 XAML 名称范围,但这不是常见用法。 此外,GetNameScope 通常不从代码中使用。
XAML 名称范围实现
下面的类直接实现 INameScope:
ResourceDictionary 不使用 XAML 名称或名称范围;相反,它使用键,因为它是一个字典实现。 ResourceDictionary 实现 INameScope 的唯一原因是它可以引发用户代码异常,帮助区分真正的 XAML 名称范围与如何 ResourceDictionary 处理键之间的区别,并确保 XAML 名称范围不是由父元素应用到 ResourceDictionary。
FrameworkTemplate 和 Style 通过显式接口定义实现 INameScope。 通过 INameScope 接口访问 XAML 名称范围时,显式实现可使这些 XAML 名称范围按惯例行为,这也是 XAML 名称范围通过 WPF 内部进程进行通信的方式。 但显式接口定义不是 FrameworkTemplate 和 Style 常规 API 表面的一部分,因为很少需要在 FrameworkTemplate 和 Style 上直接调用 INameScope 方法,而改为使用其他 API,如 GetTemplateChild。
通过使用 System.Windows.NameScope 帮助程序类并通过 NameScope.NameScope 附加属性连接到其 XAML 名称范围实现,以下类定义了它们自己的 XAML 名称范围: