WPF中NameScope的查找規(guī)則詳解
前言
我們?cè)?WPF 中使用綁定時(shí)可以使用 ElementName=Foo 這樣的寫法,并且還能夠真的在運(yùn)行時(shí)找到這個(gè)名稱對(duì)應(yīng)的對(duì)象,是因?yàn)?WPF 中提供了名稱范圍概念。
實(shí)現(xiàn) INameScope 接口可以定義一個(gè)名稱范圍。無論你使用 Name 屬性還是使用 x:Name 特性都可以在一個(gè)名稱范圍內(nèi)指定某個(gè)元素的名稱。綁定時(shí)就在此名稱范圍內(nèi)查找,于是可以找到你需要的對(duì)象。
XAML中的NameScope
首先來講講WPF的名稱管理機(jī)制NameScope,也即是名稱范圍。名稱范圍主要提供了兩種功能:記錄XAML名稱與界面元素實(shí)例之間的關(guān)聯(lián)關(guān)系;防止名稱沖突。可以說,第二種功能是第一種功能實(shí)現(xiàn)時(shí)所產(chǎn)生的副作用。而在XAML中引用某個(gè)名稱時(shí),WPF會(huì)自動(dòng)使用相應(yīng)的NameScope執(zhí)行對(duì)名稱的查找。
那么,WPF的名稱范圍是如何在XAML等程序組成中起作用的呢?如果一個(gè)元素在XAML中使用x:Name或Name屬性設(shè)置了名稱,那么WPF會(huì)為該屬性設(shè)置執(zhí)行一些額外的執(zhí)行邏輯,如在對(duì)應(yīng)的cs文件中自動(dòng)生成具有相同名稱的成員,并將它們注冊(cè)到相應(yīng)的名稱范圍中。如果在該范圍中多次使用了相同的名稱,那么WPF會(huì)拋出一個(gè)異常。在XAML中對(duì)某個(gè)元素進(jìn)行引用的時(shí)候,WPF會(huì)從該NameScope中尋找該名稱所對(duì)應(yīng)的界面元素以進(jìn)行操作。
當(dāng)然,用戶并不需要顯式地對(duì)名稱范圍進(jìn)行處理。默認(rèn)情況下,WPF會(huì)使用一定的機(jī)制保證該文件中的各個(gè)界面元素可以擁有合適的名稱范圍。在XAML中常常作為根元素的Page類及Window類都提供了對(duì)名稱范圍的支持。如果XAML中的根元素并不是這兩個(gè)類型,那么XAML處理器會(huì)在處理過程中為該文件隱式地添加一個(gè)Page元素作為新的根元素。通過這種方法,WPF可以保證XAML文件中對(duì)x:Name以及Name的使用可以將名稱正確地注冊(cè)進(jìn)相應(yīng)的名稱范圍中。
本文將介紹 WPF 中 NameScope 的查找規(guī)則。(額外的,資源 / 資源字典的查找方式與 NameScope 的方式是一樣的,所以本文分析過程同樣使用與資源的查找。)
INameScope
WPF 的 INameScope 接口只用來管理一個(gè)范圍之內(nèi)的名稱。它包含下面三個(gè)方法:
public interface INameScope { object FindName(string name); void RegisterName(string name, object scopedElement); void UnregisterName(string name); }
它的主要實(shí)現(xiàn)是 NameScope,包含了更多功能;而上面的接口是其本2222質(zhì)功能。
不過,NameScope 的實(shí)現(xiàn)帶來了一個(gè)重要的依賴項(xiàng)屬性 —— NameScope。下面是此屬性的代碼(經(jīng)過簡(jiǎn)化):
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)); }
同樣實(shí)現(xiàn)了此接口的還有 TemplateNameScope,此 NameScope 會(huì)被 FrameworkTemplate / FrameworkElementFactory / BamlRecordReader 設(shè)置到以上依賴屬性中。于是我們可以在模板范圍內(nèi)找到某個(gè)特定名稱對(duì)應(yīng)的元素。
除此之外,NameScope 的設(shè)置由 XAML 解析器在 WPF 項(xiàng)目編譯的時(shí)候自動(dòng)生成。
NameScope 的名稱注冊(cè)規(guī)則
如果你沒有在代碼中顯式去調(diào)用 RegisterName 這樣的方法,那么 NameScope 的創(chuàng)建以及名稱的注冊(cè)都由 XAML 解析器來完成。
XAML 解析器(BamlRecordReader)注冊(cè)名字的時(shí)候并沒有去爬可視化樹什么的,只是單純?cè)诮馕?XAML 的時(shí)候去調(diào)用代碼注冊(cè)這個(gè)名字而已。注冊(cè)由一個(gè) Stack 來完成,NameScopeStack。
設(shè)想以下這個(gè)例子(來自于 .NET Framework 代碼中的注釋):
<Window x:Name="myWindow"> ... <Style x:Name="myStyle"> ... <SolidColorBrush x:Name="myBrush"> </SolidColorBrush> </Style> </Window>
每當(dāng) XAML 解析器解析一層的時(shí)候,就會(huì)給 NameScopeStack 入棧,于是 Window 首先創(chuàng)建 NameScope 入棧。隨后解析到 Style 時(shí)又加一個(gè) NameScope 入棧,其他元素解析時(shí)不會(huì)創(chuàng)建 NameScope(包括 XAML 中的頂層元素 UserControl 等)。
這時(shí),myWindow 會(huì)被注冊(cè)到 Window 一層的 NameScope 中,myStyle 也會(huì)注冊(cè)到 Window 一層的 NameScope 中;而 myBrush 則會(huì)注冊(cè)到 Style 那一層的 NameScope 中。
Window 的 NameScope
- myWindow
- myStyle
Style 的 NameScope
- myBrush
NameScope 的名稱查找規(guī)則
在本文一開始貼出 NameScope 依賴項(xiàng)屬性的時(shí)候,你應(yīng)該注意到這只是一個(gè)普通的屬性,并沒有使用到什么可以用可視化樹繼承這樣的高級(jí)元數(shù)據(jù)。事實(shí)上也不應(yīng)該有這樣的高級(jí)元數(shù)據(jù),因?yàn)?NameScope 的抽象級(jí)別低于可視化樹或者邏輯樹。
但是,實(shí)際上 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; }
非常明顯,F(xiàn)indScope 是期望使用邏輯樹來查找名稱范圍的。
不過值得注意的是,當(dāng)一個(gè)元素沒有邏輯父級(jí)的時(shí)候,會(huì)試圖使用 Helper.FindMentor 來查找另一個(gè)對(duì)象。那這是什么方法,又試圖尋找什么對(duì)象呢?
Mentor 是名詞,意為 “導(dǎo)師,指導(dǎo)”。于是我們需要閱讀以下 Helper.FindMentor 方法的實(shí)現(xiàn)來了解其意圖:
提示:以下注釋中的 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,那么就返回這個(gè) FE 或者 FCE;如果到最終也沒有找到,則返回 null。
這是個(gè) virtual 屬性,基類 DependencyObject 中只返回 null,而子類重寫它時(shí),返回父級(jí)。Freezable, FrameworkElement, FrameworkContentElement 等重寫了這個(gè)屬性。
對(duì)于 FrameworkElement,重寫時(shí)只是單純的返回了一個(gè)內(nèi)部管理的字段而已:
internal override DependencyObject InheritanceContext { get { return InheritanceContextField.GetValue(this); } }
此字段在調(diào)用 DependencyObject.AddInheritanceContext 的時(shí)候會(huì)賦值。而對(duì)于可視化樹或邏輯樹的建立,此方法不會(huì)被調(diào)用,所以此屬性并不會(huì)對(duì)可視化樹或邏輯樹有影響。但是,F(xiàn)reezable, InputBinding, Visual3D, GridViewColumn, ViewBase, CollectionViewSource, ResourceDictionary, TriggerAction, TriggerBase 等會(huì)在屬性賦值的時(shí)候調(diào)用此方法。于是我們能夠在以上這些屬性的設(shè)置中找到名稱。
特別說明,只有那些重寫了 InheritanceContext 的類型才會(huì)在查找名稱的時(shí)候找得到 NameScope;只有以上這些調(diào)用了 DependencyObject.AddInheritanceContext 方法的屬性才會(huì)在賦值是能夠找得到 NameScope。
所以,我另一篇文章中所說的 ContextMenu 是找不到對(duì)應(yīng)的 NameScope 的。WPF 的 ElementName 在 ContextMenu 中無法綁定成功?試試使用 x:Reference!。此文中 ContextMenu 找到的 NameScope 是 null。
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
桌面浮動(dòng)窗口(類似惡意廣告)的實(shí)現(xiàn)詳解
本篇文章是對(duì)桌面浮動(dòng)窗口的實(shí)現(xiàn)方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06解決C#中取消方向鍵對(duì)控件焦點(diǎn)控制的實(shí)現(xiàn)方法
本篇文章是對(duì)C#中取消方向鍵對(duì)控件焦點(diǎn)控制的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05Unity打開淘寶app并跳轉(zhuǎn)到商品頁面功能的實(shí)現(xiàn)方法
這篇文章主要給大家介紹了關(guān)于如何利用Unity打開淘寶app并跳轉(zhuǎn)到商品頁面功能的相關(guān)資料,這個(gè)功能目前在網(wǎng)上找不到相關(guān)的解決方法,所以自己寫了出來,需要的朋友可以參考下2021-07-07C# Dynamic之:ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用(下)
本篇文章是對(duì)C#中ExpandoObject,DynamicObject,DynamicMetaOb的應(yīng)用進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05C#?CefSharp?根據(jù)輸入日期段自動(dòng)選擇日期的操作代碼
這篇文章主要介紹了C#?CefSharp?根據(jù)輸入日期段自動(dòng)選擇日期的操作代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2024-01-01