WPF中NameScope的查找规则详解

前言

我们在 WPF 中使用绑定时可以使用 ElementName=Foo 这样的写法,并且还能够真的在运行时找到这个名称对应的对象,是因为 WPF 中提供了名称范围概念。

实现 INameScope 接口可以定义一个名称范围。无论你使用 Name 属性还是使用 x:Name 特性都可以在一个名称范围内指定某个元素的名称。绑定时就在此名称范围内查找,于是可以找到你需要的对象。

XAML中的NameScope

首先来讲讲WPF的名称管理机制NameScope,也即是名称范围。名称范围主要提供了两种功能:记录XAML名称与界面元素实例之间的关联关系;防止名称冲突。可以说,第二种功能是第一种功能实现时所产生的副作用。而在XAML中引用某个名称时,WPF会自动使用相应的NameScope执行对名称的查找。

那么,WPF的名称范围是如何在XAML等程序组成中起作用的呢?如果一个元素在XAML中使用x:Name或Name属性设置了名称,那么WPF会为该属性设置执行一些额外的执行逻辑,如在对应的cs文件中自动生成具有相同名称的成员,并将它们注册到相应的名称范围中。如果在该范围中多次使用了相同的名称,那么WPF会抛出一个异常。在XAML中对某个元素进行引用的时候,WPF会从该NameScope中寻找该名称所对应的界面元素以进行操作。

当然,用户并不需要显式地对名称范围进行处理。默认情况下,WPF会使用一定的机制保证该文件中的各个界面元素可以拥有合适的名称范围。在XAML中常常作为根元素的Page类及Window类都提供了对名称范围的支持。如果XAML中的根元素并不是这两个类型,那么XAML处理器会在处理过程中为该文件隐式地添加一个Page元素作为新的根元素。通过这种方法,WPF可以保证XAML文件中对x:Name以及Name的使用可以将名称正确地注册进相应的名称范围中。

本文将介绍 WPF 中 NameScope 的查找规则。(额外的,资源 / 资源字典的查找方式与 NameScope 的方式是一样的,所以本文分析过程同样使用与资源的查找。)

INameScope

WPF 的 INameScope 接口只用来管理一个范围之内的名称。它包含下面三个方法:

public interface INameScope
{
 object FindName(string name);
 void RegisterName(string name, object scopedElement);
 void UnregisterName(string name);
}

它的主要实现是 NameScope,包含了更多功能;而上面的接口是其本2222质功能。

不过,NameScope 的实现带来了一个重要的依赖项属性 —— NameScope。下面是此属性的代码(经过简化):

public static readonly DependencyProperty NameScopeProperty
 = DependencyProperty.RegisterAttached("NameScope", typeof(INameScope), typeof(NameScope));

public static void SetNameScope(DependencyObject dependencyObject, INameScope value)
{
 if (dependencyObject == null) throw new ArgumentNullException(nameof(dependencyObject));
 dependencyObject.SetValue(NameScopeProperty, value);
}

public static INameScope GetNameScope(DependencyObject dependencyObject)
{
 if (dependencyObject == null) throw new ArgumentNullException(nameof(dependencyObject));
 return ((INameScope)dependencyObject.GetValue(NameScopeProperty));
}

同样实现了此接口的还有 TemplateNameScope,此 NameScope 会被 FrameworkTemplate / FrameworkElementFactory / BamlRecordReader 设置到以上依赖属性中。于是我们可以在模板范围内找到某个特定名称对应的元素。

除此之外,NameScope 的设置由 XAML 解析器在 WPF 项目编译的时候自动生成。

NameScope 的名称注册规则

如果你没有在代码中显式去调用 RegisterName 这样的方法,那么 NameScope 的创建以及名称的注册都由 XAML 解析器来完成。

XAML 解析器(BamlRecordReader)注册名字的时候并没有去爬可视化树什么的,只是单纯在解析 XAML 的时候去调用代码注册这个名字而已。注册由一个 Stack 来完成,NameScopeStack。

设想以下这个例子(来自于 .NET Framework 代码中的注释):

<Window x:Name="myWindow">
 ...
 <Style x:Name="myStyle">
 ...
 <SolidColorBrush x:Name="myBrush">
 </SolidColorBrush>
 </Style>
</Window>

每当 XAML 解析器解析一层的时候,就会给 NameScopeStack 入栈,于是 Window 首先创建 NameScope 入栈。随后解析到 Style 时又加一个 NameScope 入栈,其他元素解析时不会创建 NameScope(包括 XAML 中的顶层元素 UserControl 等)。

这时,myWindow 会被注册到 Window 一层的 NameScope 中,myStyle 也会注册到 Window 一层的 NameScope 中;而 myBrush 则会注册到 Style 那一层的 NameScope 中。

Window 的 NameScope

  • myWindow
  • myStyle

Style 的 NameScope

  • myBrush

NameScope 的名称查找规则

在本文一开始贴出 NameScope 依赖项属性的时候,你应该注意到这只是一个普通的属性,并没有使用到什么可以用可视化树继承这样的高级元数据。事实上也不应该有这样的高级元数据,因为 NameScope 的抽象级别低于可视化树或者逻辑树。

但是,实际上 NameScope 的查找却是依赖于逻辑树的 —— 这是 FrameworkElement 的功能:

internal static INameScope FindScope(DependencyObject d, out DependencyObject scopeOwner)
{
 while (d != null)
 {
  INameScope nameScope = NameScope.NameScopeFromObject(d);
  if (nameScope != null)
  {
   scopeOwner = d;
   return nameScope;
  }

  DependencyObject parent = LogicalTreeHelper.GetParent(d);

  d = (parent != null) ? parent : Helper.FindMentor(d.InheritanceContext);
 }

 scopeOwner = null;
 return null;
}

非常明显,FindScope 是期望使用逻辑树来查找名称范围的。

不过值得注意的是,当一个元素没有逻辑父级的时候,会试图使用 Helper.FindMentor 来查找另一个对象。那这是什么方法,又试图寻找什么对象呢?

Mentor 是名词,意为 “导师,指导”。于是我们需要阅读以下 Helper.FindMentor 方法的实现来了解其意图:

提示:以下注释中的 FE 代表 FrameworkElement,而 FCE 代表 FrameworkContentElement。

/// <summary>
///  This method finds the mentor by looking up the InheritanceContext
///  links starting from the given node until it finds an FE/FCE. This
///  mentor will be used to do a FindResource call while evaluating this
///  expression.
/// </summary>
/// <remarks>
///  This method is invoked by the ResourceReferenceExpression
///  and BindingExpression
/// </remarks>
internal static DependencyObject FindMentor(DependencyObject d)
{
 // Find the nearest FE/FCE InheritanceContext
 while (d != null)
 {
  FrameworkElement fe;
  FrameworkContentElement fce;
  Helper.DowncastToFEorFCE(d, out fe, out fce, false);

  if (fe != null)
  {
   return fe;
  }
  else if (fce != null)
  {
   return fce;
  }
  else
  {
   d = d.InheritanceContext;
  }
 }

 return null;
}

具体来说,是不断查找 InheritanceContext,如果找到了 FrameworkElement 或者 FrameworkContentElement,那么就返回这个 FE 或者 FCE;如果到最终也没有找到,则返回 null。

这是个 virtual 属性,基类 DependencyObject 中只返回 null,而子类重写它时,返回父级。Freezable, FrameworkElement, FrameworkContentElement 等重写了这个属性。

对于 FrameworkElement,重写时只是单纯的返回了一个内部管理的字段而已:

internal override DependencyObject InheritanceContext
{
 get { return InheritanceContextField.GetValue(this); }
}

此字段在调用 DependencyObject.AddInheritanceContext 的时候会赋值。而对于可视化树或逻辑树的建立,此方法不会被调用,所以此属性并不会对可视化树或逻辑树有影响。但是,Freezable, InputBinding, Visual3D, GridViewColumn, ViewBase, CollectionViewSource, ResourceDictionary, TriggerAction, TriggerBase 等会在属性赋值的时候调用此方法。于是我们能够在以上这些属性的设置中找到名称。

特别说明,只有那些重写了 InheritanceContext 的类型才会在查找名称的时候找得到 NameScope;只有以上这些调用了 DependencyObject.AddInheritanceContext 方法的属性才会在赋值是能够找得到 NameScope。

所以,我另一篇文章中所说的 ContextMenu 是找不到对应的 NameScope 的。WPF 的 ElementName 在 ContextMenu 中无法绑定成功?试试使用 x:Reference!。此文中 ContextMenu 找到的 NameScope 是 null。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • WPF绑定实例详解

    本文详细讲述了WPF绑定的用法,分享给大家供大家参考.具体用法分析如下: 1.WPF绑定使用的源属性必须是依赖项属性,这是因为依赖项属性具有内置的更改通知支持,元素绑定表达式使用了Xaml扩展标记,WPF绑定一个控件是使用Binding.ElementName,绑定非控件对象时使用Source,RelativeSource,DataContext属性(WPF特有,而非XAML),只能绑定对象的共有字段. 下边是部分Binding 属性名,完整列表参考 :http://msdn.microsoft

  • WPF的数据绑定详细介绍

    一.WPF数据绑定的概要 数据绑定:是应用程序 UI 与业务逻辑之间建立连接的过程. 如果绑定正确设置并且数据提供正确通知,则当数据的值发生更改时,绑定到数据的视觉元素会自动反映更改. 数据绑定可能还意味着如果视觉元素中数据的外部表现形式发生更改,则基础数据可以自动更新以反映更改. 例如:如果用户编辑 TextBox 元素中的值,则基础数据值会自动更新以反映该更改. 1. 数据绑定涉及到两个方面: 一个是绑定源,一个是绑定目标.绑定源即控件绑定所使用的源数据,绑定目标即数据显示的控件. 2. 对

  • 如何搭建新的WPF项目框架

    下面就WPF项目框架搭建步骤一步一步的分享给大家. 在WPF项目开发中最常用的开发模式无疑是MVVM模式,  MVVM模式开发的好处,在这里就不详细讨论, 还有 本文中所使用MVVMLight框架,为什么使用MVVM框架(1.框架较轻,2.学习成本低.3.适用大多数中小型项目,4.相对于微软的prism框架更容易上手)    下面开始 一步一步 搭建框架 第一步: 利用反射创建VM构造器 public class ViewModelFactory { private static Diction

  • WPF MVVM示例讲解

    在没给大家讲解wpf mwm示例之前先给大家简单说下MVVM理论知识: WPF技术的主要特点是数据驱动UI,所以在使用WPF技术开发的过程中是以数据为核心的,WPF提供了数据绑定机制,当数据发生变化时,WPF会自动发出通知去更新UI. 我们使用模式,一般是想达到高内聚低耦合.在WPF开发中,经典的编程模式是MVVM,是为WPF量身定做的模式,该模式充分利用了WPF的数据绑定机制,最大限度地降低了Xmal文件和CS文件的耦合度,也就是UI显示和逻辑代码的耦合度,如需要更换界面时,逻辑代码修改很少,

  • WPF中NameScope的查找规则详解

    前言 我们在 WPF 中使用绑定时可以使用 ElementName=Foo 这样的写法,并且还能够真的在运行时找到这个名称对应的对象,是因为 WPF 中提供了名称范围概念. 实现 INameScope 接口可以定义一个名称范围.无论你使用 Name 属性还是使用 x:Name 特性都可以在一个名称范围内指定某个元素的名称.绑定时就在此名称范围内查找,于是可以找到你需要的对象. XAML中的NameScope 首先来讲讲WPF的名称管理机制NameScope,也即是名称范围.名称范围主要提供了两种

  • 前端Vue中常用rules校验规则详解

    目录 1.是否合法IP地址 2.是否手机号码或者固话 3.是否固话 4.是否手机号码 5.是否身份证号码 6.是否邮箱 7.合法url 8.验证内容是否包含英文数字以及下划线 9.自动检验数值的范围 10.验证数字输入框最大数值 11.验证是否1-99之间 12.验证是否整数 13.验证是否整数,非必填 14. 验证是否是[0-1]的小数 15. 验证是否是[1-10]的小数,即不可以等于0 16.验证是否是[1-100]的小数,即不可以等于0 17.验证是否是[0-100]的小数 18.验证端

  • seajs中模块的解析规则详解和模块使用总结

    seajs github 模块标识已经说的相对清楚了.但并没有面面俱到,特别是当你需要手写 [模块ID]和[模块依赖]的时候,或者自己写自动化工具来做 transport 的时候(ps:spm貌似适应性不是很强也不易用,毕竟每个项目的目录结构可能相差很大,且不易改变.当然如果他的定位是包管理工具就别指望它来做你的项目的自动化构建工具了),ID的解析规则就需要了解透彻了.注意事项:1. 顶级标识始终相对 base 基础路径解析.2. 绝对路径和根路径始终相对当前页面解析.3. require 和

  • golang中defer的使用规则详解

    前言 在golang当中,defer代码块会在函数调用链表中增加一个函数调用.这个函数调用不是普通的函数调用,而是会在函数正常返回,也就是return之后添加一个函数调用.因此,defer通常用来释放函数内部变量. 为了更好的学习defer的行为,我们首先来看下面一段代码: func CopyFile(dstName, srcName string) (written int64, err error) { src, err := os.Open(srcName) if err != nil {

  • Angular.JS中指令的命名规则详解

    命名规范 同一个AngularJS指令,在js文件和html文件中有着不同的命名规范:在js文件中使用标准的小驼峰命名法,在html文件中使用"小写字母+连接符"的命名法.如下表所示 在js文件中 在html文件中 ngApp ng-app myDirective my-directive 处理机制 AngularJS之所以选择这样的命名方式,是因为html文件不区分大小写,而js文件则对大小写敏感(myDir和mydir在js文件中是不同的指令,但html看来是同一个指令),为了避免

  • Oracle中分组查询group by用法规则详解

    Oracle中group by用法 在select 语句中可以使用group by 子句将行划分成较小的组,一旦使用分组后select操作的对象变为各个分组后的数据,使用聚组函数返回的是每一个组的汇总信息. 使用having子句 限制返回的结果集.group by 子句可以将查询结果分组,并返回行的汇总信息Oracle 按照group by 子句中指定的表达式的值分组查询结果. 在带有group by 子句的查询语句中,在select 列表中指定的列要么是group by 子句中指定的列,要么包

  • 正则表达式中两个反斜杠的匹配规则详解

    关于正则表达式raw的\匹配规则 这是我在学习中获得到的一个例子,第一表达式中匹配到的是none.于是乎我就在思考,为什么会匹配不到,假设\t被转义成一个\t,那么也应该匹配到\tsanle,而不是none. 为了验证这个问题,我做了如下的实验: 那为什么一个会出现这样的结果呢,在正则表达式中,需要查找的字符串,会进行两次转义,先是传入的字符串进行第一层转换,例如:\\t --> \t .然后传到re解析器里进行第二层转换,\t -->tab键.而需要匹配的字符串\\\t -->两个反斜

  • C#中WPF ListView绑定数据的实例详解

    C#中WPF ListView绑定数据的实例详解 WPF中ListView用来显示数据十分方便, 我们可以将它分成几个列,每一个列用来显示一条数据,但是又是在一方之中. 那么怎样实现这样的效果的呢,这就要用绑定了. 我们先来看一看他的xmal代码 <ListView Name="receiveList" Grid.Row="0"> <ListView.View> <GridView> <GridView.Columns>

  • python flask中动态URL规则详解

    URL是可以添加变量部分的, 把类似的部分抽象出来, 比如: @app.route('/example/1/') @app.route('/example/2/') @app.route('/example/3/') def example(id): return 'example:{ }'.format(id) 可以抽象为: @app.route('/example/<id>/') def wxample(id): return 'example:{ }'.format(id) 尖括号中的内

  • Python-re中search()函数的用法详解(查找ip)

    1.首先来看一下search()和find()的区别 import re s1 = "2221155" #search 字符串第一次出现的位置 print(re.search("1",s1)) print(s1.find("1")) 它们的输出分别是: search(): 函数返回的是查找的字符在字符串中的位置 find(): 函数返回的也是在字符串中的位置 目前来看两者并没有什么区别,但是**search()**函数的高级操作还在后边哦! 2.

随机推荐