1. TemplatePart
TemplatePart(部件)是指ControlTemplate中的命名元素。控件邏輯預期這些部分存在於ControlTemplate中,並且使用protected DependencyObject GetTemplateChild(String childName)獲取它們后進行操作。
以AutoSuggestBox為例,它的ControlTemplate結構如下,可以看到AutoSuggestBox由四個TemplatePart組成,每個TemplatePart都可以在控件代碼中以編程方式訪問:

下圖顯示了AutoSuggestBox的TemplatePart:

2. 使用TemplatePart
上一篇文章構造了一個很基礎的控件HeaderedContentControl,這次通過擴展這個類做些試驗性質的功能來介紹模板化控件的進階知識。
新建一個名為ContentView的控件,繼承自HeaderedContentControl,它要實現的功能有兩個:
- 控件的Header默認Opacity=0.7,當鼠標移動到控件上時,設置Header的Opacity=1。
- 當Header為空時,隱藏用於顯示Header的HeaderContentPresenter。
雖然可以使用依賴屬性及TemplateBinding的方式實現這個需求,不過這次用TemplatePart的方式實現。很顯然,要實現這次的需求最直接的做法是獲取顯示Header的TemplatePart,然后用代碼對其進行操作。大致上分為兩步:添加TemplatePart名稱,在代碼中獲取這個部件並操作。
2.1 添加TemplatePart名稱
在ContentView的ControlTemplate中為ContentPresenter命名為HeaderContentPresenter:
<ContentPresenter x:Name="HeaderContentPresenter"
Foreground="{ThemeResource TextControlHeaderForeground}"
Margin="0,0,0,8"
FontWeight="Normal"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}" />
2.2 獲取TemplatePart
模板化控件在加載ControlTemplate后會調用OnApplyTemplate,可以在這個函數中調用protected DependencyObject GetTemplateChild(String childName)獲取模板中指定名字的部件。從返回值是DependencyObject可以看出,只要是DependencyObject 都能使用ControlTemplate獲取。
這段代碼演示了如何獲得顯示Header的ContentPresenter部件:
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_headerPart = GetTemplateChild(HeaderPartName) as FrameworkElement;
}
注意:不要在Loaded事件中嘗試調用GetTemplateChild,因為Loaded在OnApplyTemplate前調用,而且Loaded更容易被多次觸發。
由於Template可能多次加載,或者不能正確獲取TemplatePart,所以使用TemplatePart前應該先判斷是否為空;如果要訂閱事件,應該先取消訂閱。更完整的GetTemplateChild步驟應該是:
- 取消訂閱TemplatePart事件
- 將TemplatePart存儲到私有字段
- 訂閱TemplatePart事件
可以參考如下代碼:
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (_button != null)
{
_button.Click -= OnButtonClick;
}
_button = GetTemplateChild(PartButtonName) as ButtonBase;
if (_button != null)
{
_button.Click += OnButtonClick;
}
}
2.3 完整的代碼
[TemplatePart(Name = HeaderPartName, Type = typeof(FrameworkElement))]
public sealed class ContentView : HeaderedContentControl
{
public const string HeaderPartName = "HeaderContentPresenter";
public ContentView()
{
this.DefaultStyleKey = typeof(ContentView);
}
private FrameworkElement _headerPart;
private bool _isPointerEntered;
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
_headerPart = GetTemplateChild(HeaderPartName) as FrameworkElement;
UpdateHeaderVisual();
}
protected override void OnPointerEntered(PointerRoutedEventArgs e)
{
base.OnPointerEntered(e);
_isPointerEntered = true;
UpdateHeaderVisual();
}
protected override void OnPointerExited(PointerRoutedEventArgs e)
{
base.OnPointerExited(e);
_isPointerEntered = false;
UpdateHeaderVisual();
}
protected override void OnHeaderChanged(object oldValue, object newValue)
{
base.OnHeaderChanged(oldValue, newValue);
UpdateHeaderVisual();
}
private void UpdateHeaderVisual()
{
if (_headerPart == null)
return;
if (_isPointerEntered)
_headerPart.Opacity = 1;
else
_headerPart.Opacity = 0.7;
if (Header == null)
_headerPart.Visibility = Visibility.Collapsed;
else
_headerPart.Visibility = Visibility.Visible;
}
}
3. x:DeferLoadStrategy="Lazy"與GetTemplateChild
標記為x:DeferLoadStrategy="Lazy"的元素將延遲加載,即不會出現在VisualTree上,直到它被調用。
假設將ContentView中HeaderContentPresenter標記為x:DeferLoadStrategy="Lazy"並且在代碼中注釋_headerPart = GetTemplateChild(HeaderPartName) as FrameworkElement這句,運行時將看不到Header的內容,並且VisualTree如下所示:

只有代碼中執行了_headerPart = GetTemplateChild(HeaderPartName) as FrameworkElement這句后,VisualTree上才可以看到HeaderContentPresenter,如下所示:

出於性能方面的考慮,很多UWP原生控件都會包含x:DeferLoadStrategy="Lazy"。
4. TemplatePartAttribute協定
有時,為了表明控件期待在ControlTemplate存在某個特定部件,防止編輯ControlTemplate的開發人員刪除它,控件上會添加添加TemplatePartAttribute協定。上面的ContentView代碼中即包含這個協定:
[TemplatePart(Name = HeaderPartName, Type = typeof(FrameworkElement))]
這段代碼的意思是期待在ControlTemplate中存在名稱為 "HeaderContentPresenter",類型為FrameworkElement的部件。
TemplatePartAttribute在UWP中的作用好像被弱化了,不止在UWP原生控件中見不到TemplatePartAttribute,甚至在Blend中“部件”窗口也消失了。可能UWP更加建議使用VisualState。
注意:你可能會在別的地方看到部件的命名為“PART_”開頭,在WPF時代確實是這樣,到現在仍有很多人保留了這種習慣。新興的命名語法更加自然,不需要加上“PART_”開頭。不過既然Blend中沒有了“部件”窗口,用“PART_”標識部件也是個不錯的方法。
5. 原則
使用TemplatePart需要遵循以下原則:
- 盡可能減少TemplarePartAttribute協定。
- 在使用TemplatePart之前檢查其是否為Null。
- 如果ControlTemplate沒有遵循TemplatePartAttribute協定也不應該拋出異常,有可能ControlTemplate的作者是故意屏蔽某項功能。
