天天看點

WPF 中的 NameScope

原文: 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