原文: WPF 中的 NameScope
版權聲明:本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。歡迎轉載、使用、重新釋出,但務必保留文章署名呂毅(包含連結:http://blog.csdn.net/wpwalter/),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。如有任何疑問,請與我聯系([email protected])。 https://blog.csdn.net/WPwalter/article/details/83473818
我們在 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
,包含了更多功能;而上面的接口是其本質功能。
不過,
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