WPF 中的 NameScope
我們在 WPF 中使用繫結時可以使用ElementName=Foo
這樣的寫法,並且還能夠真的在執行時找到這個名稱對應的物件,是因為 WPF 中提供了名稱範圍概念。
實現INameScope
介面可以定義一個名稱範圍。無論你使用Name
屬性還是使用x: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 的。ofollow,noindex" target="_blank">WPF 的 ElementName 在 ContextMenu 中無法繫結成功?試試使用 x:Reference!
。此文中ContextMenu
找到的 NameScope 是null
。