WPF 名称范围
更新:2007 年 11 月
名称范围既是一种概念,也是用于存储对象的 XAML 定义名称及其实例等效项之间的关系的编程对象。加载 WPF 应用程序的页面时,即在 XAML 托管代码中创建了名称范围。作为编程对象的名称范围由 INameScope 接口定义,并且还由实际类 NameScope 实现。
本主题包括下列各节。
- 加载的 XAML 应用程序中的名称范围
- 样式和模板中的名称范围
- 名称范围和名称相关的 API
- 相关主题
加载的 XAML 应用程序中的名称范围
处理 XAML 页时,即对该页的根元素创建了名称范围。该页中指定的每个名称都会添加到相关的名称范围中。作为常见根元素(例如 Page 和 Window)的元素总是控制名称范围。如果在标记中某个元素(例如 FrameworkElement 或 FrameworkContentElement)是页的根元素,则 XAML 处理器会隐式添加一个 Page 根元素,以便 Page 可以提供一个名称范围。即使最初在 XAML 中没有定义 Name 或 x:Name 属性,也会创建一个名称范围。
如果试图在任意名称范围中两次使用同一个名称,则会引发异常。对于具有代码隐藏并且是已编译的应用程序的一部分的 XAML,在创建页的已生成类时会引发该异常。
将元素添加到已分析的元素树
若要在初始的加载和处理之后向元素树添加任何元素,都必须对定义名称范围的类调用相应的 RegisterName 的实现。否则,无法通过 FindName 等方法按名称引用添加的对象。仅设置 Name 属性(或 x:Name 属性)不会将该名称注册到任何名称范围中。将命名的元素添加到具有名称范围的元素树中也不会将此名称注册到名称范围中。尽管名称范围可以嵌套,但通常您应该将名称注册到根元素上存在的名称范围中,这样您的名称范围位置便可与在等效的加载 XAML 页中可能已创建的名称范围并列。应用程序开发人员最常用的方案是使用 RegisterName 将名称注册到当前根元素的名称范围中。RegisterName 是查找将作为动画运行的演示图板的一种重要方案的一部分。有关更多信息,请参见演示图板概述。如果您对同一逻辑树中的非根元素的元素调用 RegisterName,则该名称仍然会注册到最靠近根元素的元素,就好像对该根元素调用了 RegisterName 一样。
代码中的名称范围
对于以编程方式创建(而不是来自加载的 XAML)的应用程序,若要支持名称范围,根元素必须实现 INameScope、或者必须是 FrameworkElement 或 FrameworkContentElement 派生类。
此外,对于不是由 XAML 处理器加载和处理的任何元素,默认情况下不会创建或初始化该对象的名称范围。必须为随后要向其中注册名称的任何元素显式创建新的名称范围。若要为元素创建名称范围,可调用静态 SetNameScope 方法。将该元素指定为 dependencyObject 参数,并且将新的 NameScope 构造函数调用指定为 value 参数。
如果作为 SetNameScope 的 dependencyObject 提供的对象不是 INameScope 实现,也不是 FrameworkElement 或 FrameworkContentElement,那么,对任何子元素调用 RegisterName 均无效。如果您无法显式创建新的名称范围,则调用 RegisterName 将引发异常。
有关在代码中使用名称范围 API 的示例,请参见如何:定义名称范围。
样式和模板中的名称范围
WPF 中的样式和模板提供了以简单的方法重新使用和重新应用内容的功能,但样式和模板可能还包括具有在模板级别指定的名称的元素。同一个模板可能在某个页中被多次重复使用。因此,样式和模板都定义其自己的名称范围,而与样式或模板所应用到的包含页无关。
请看下面的示例:
<Page
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://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>
此处,同一个模板被应用到两个不同的按钮。如果模板没有独立的名称范围,则该模板中用到的 TheBorder 名称会导致名称冲突。模板的每个实例化都有其自已的名称范围,因此在本示例中每个实例化模板的名称范围都只包含一个名称。
样式也具有其自己的名称范围,因此大多数情况下演示图板的某些部分会分配有特殊的名称。即使模板重新定义为控件自定义项的一部分,这些名称也会启用以具有此名称的元素为目标的控件特定行为。
由于名称范围是独立的,因此在模板中查找命名的元素比在页中查找非模板化的元素更具有挑战性。首先需要确定所应用的模板,方法是获取该模板所应用到的控件的 Template 属性值。然后,调用 FindName 的模板版本,将模板所应用到的控件作为第二个参数进行传递。
如果您是控件作者,您要生成一个约定,即所应用的模板中的特定命名元素是控件本身所定义的行为的目标,则可以在控件实现代码中使用 GetTemplateChild 方法。由于 GetTemplateChild 方法是受保护的,因此只有控件作者才可以访问它。
如果您正在使用模板,并且需要用到该模板所应用到的名称范围,则获取 TemplatedParent,然后调用其中的 FindName。举一个使用模板的例子:您正在编写事件处理程序实现,其中该事件将从所应用的模板中的元素被引发。
名称范围和名称相关的 API
FrameworkElement 具有 FindName、RegisterName 和 UnregisterName 方法。如果您对其调用这些方法的元素拥有名称范围,则这些元素方法仅调入名称范围方法。否则,会查看其父元素是否具有名称范围,此进程会以递归方式继续执行,直到找到一个名称范围为止(由于 XAML 处理器行为的原因,可以保证在根元素有名称范围)。FrameworkContentElement 具有类似的行为,所不同的是 FrameworkContentElement 从不会有名称范围。在 FrameworkContentElement 上存在上述方法,因此这些调用最终可以转发到 FrameworkElement 父元素。
SetNameScope 用于将新的名称范围映射到现有的对象。您可以多次调用 SetNameScope 以重置或清除名称范围,但这种用法并不常见。此外,通常不通过代码使用 GetNameScope。
名称范围实现
下面的类直接实现 INameScope:
ResourceDictionary 不使用名称范围,而是使用键,原因是它是一种字典哈希表实现。ResourceDictionary 之所以实现 INameScope,其唯一的原因是它可以对用户代码引发异常,从而有助于澄清真正的名称范围和 ResourceDictionary 处理键的方式之间的区别,此外,还可以保证名称范围不会被父元素特别应用到 ResourceDictionary。
FrameworkTemplate 和 Style 通过显式接口定义实现 INameScope。这些显式实现允许这些名称范围在通过 INameScope 接口进行访问时表现为常规行为,这是名称范围与 WPF 内部进程进行通信的方式。但是,这些显式接口定义不是 FrameworkTemplate 和 Style 的常规 API 图面的一部分,原因是几乎不需要对 FrameworkTemplate 和 Style 直接调用 INameScope 方法。
下面的类都定义自己的名称范围,方法是使用 System.Windows.NameScope 帮助器类并通过 NameScope 附加属性连接到相应的名称范围实现: