剖析WPF模板机制的内部实现
众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElement 、FrameworkElment 、Control 等)都直接或间接继承自Visual 类。一个WPF应用的用户界面上的所有可视化元素一起组成了一个可视化树(visual tree),任何一个显示在用户界面上的元素都在且必须在这个树中。通常一个可视化元素都是由众多可视化元素组合而成,一个控件的所有可视化元素一起又组成了一个局部的visual tree,当然这个局部的visual tree也是整体visual tree的一部分。一个可视化元素可能是由应用直接创建(要么通过Xaml,要么通过背后的代码),也可能是从模板间接生成。前者比较容易理解,这里我们主要讨论后者,即WPF的模板机制,方法是通过简单分析WPF的源代码。由于内容较多,为了便于阅读,将分成一系列共5篇文章来叙述。本文是这一系列的第一篇,重点讨论FrameworkTemplate类和FrameworkElement模板应用机制,这也是WPF模板机制的框架。
1. 从FrameworkTemplate到visual tree
我们知道尽管WPF中模板众多,但是它们的类型无外乎四个,这四个类的继承关系如下图所示:
可见开发中常用的三个模板类都以FrameworkTemplate为基类。问题是,除了继承关系,这些模板类的子类与基类还有什么关系?三个子类之间有什么关系?这些模板类在WPF模板机制中的各自角色是什么?WPF究竟是如何从模板生成visual tree的?
要回答这些问题,最佳途径是从分析模板基类FrameworkTemplate着手。
FrameworkTemplate是抽象类,其定义代码比较多,为了简明,这里就不贴完整代码了,我们只看比较关键的地方。首先,注意到这个类的注释只有一句话:A generic class that allow instantiation of a tree of Framework[Content]Elements,意思是这个类是允许实例化一个Framework元素树(也即visual tree)的基类(generic class),其重要性不言而喻。浏览其代码会发现一个值得注意的方法ApplyTemplateContent():
csharp
****************FrameworkTemplate****************** //
// This method
// Creates the VisualTree
//
internal bool ApplyTemplateContent(
UncommonField<HybridDictionary[]> templateDataField,
FrameworkElement container)
{
ValidateTemplatedParent(container);
bool visualsCreated = StyleHelper.ApplyTemplateContent(templateDataField, container,
_templateRoot, _lastChildIndex,
ChildIndexFromChildName, this);
return visualsCreated;
}
注释表明FrameworkTemplate生成VisualTree用的就是这个方法。其中最重要的是第二句,它把具体应用模板内容的工作交给了辅助类StyleHelper.ApplyTemplateContent()方法。这个方法的注释是:Instantiate the content of the template (either from FEFs or from Baml).This is done for every element to which this template is attached。其意思是说,每一个带有模板的元素要实例化模板的内容(无论是来自FEF还是来自Baml),都必须调用这个方法。而查看对StyleHelper.ApplyTemplateContent()方法的引用,会发现它只被引用了一次。而这唯一一次引用就是在FrameworkTemplate.ApplyTemplateContent()方法里。这也表明这个方法是FrameworkTemplate生成visual tree的唯一入口。
由于StyleHelper.ApplyTemplateContent()方法的代码较多,这里为了简洁就不贴了。简而言之,这个方法会视具体情况选择合适的方法来实例化一个FrameworkTemplate ,用其生成一个visual tree 。生成的visual tree最终都会被传递到FrameworkElement.TemplateChild属性上,而这个属性的setter又会调用Visaul.AddVisualChild()方法。后者的主要目的建立两个visual之间的父子关系(parent-child relationship),以方便以后进行布局(layout)。至此,一切准备就绪,生成的visual tree已经可视化了。
csharp
*****************FrameworkElement******************* /// <summary>
/// Gets or sets the template child of the FrameworkElement.
/// </summary>
virtual internal UIElement TemplateChild
{
get
{
return _templateChild;
}
set
{
if (value != _templateChild)
{
RemoveVisualChild(_templateChild);
_templateChild = value;
AddVisualChild(value);
}
}
}
由于FrameworkTemplate.ApplyTemplateContent()不是虚方面,因此其子类无法覆写。查看这个方法的引用我们可以看到,这个方法只在FrameworkElement.ApplyTemplate()里被调用了一次,这意味着FrameworkElement的这个方法是FrameworkElement及其子类实现模板应用的唯一入口。这个方法的重要性无论如何强调都不为过,以后我们还会多次提到这个方法。因此有必要贴一下其代码:
csharp
//***************FrameworkElement******************** /// <summary>
/// ApplyTemplate is called on every Measure
/// </summary>
/// <remarks>
/// Used by subclassers as a notification to delay fault-in their Visuals
/// Used by application authors ensure an Elements Visual tree is completely built
/// </remarks>
/// <returns>Whether Visuals were added to the tree</returns>
public bool ApplyTemplate()
{
// Notify the ContentPresenter/ItemsPresenter that we are about to generate the
// template tree and allow them to choose the right template to be applied.
OnPreApplyTemplate();
bool visualsCreated = false;
UncommonField<HybridDictionary[]> dataField = StyleHelper.TemplateDataField;
FrameworkTemplate template = TemplateInternal;
// The Template may change in OnApplyTemplate so we'll retry in this case.
// We dont want to get stuck in a loop doing this, so limit the number of
// template changes before we bail out.
int retryCount = 2;
for (int i = 0; template != null && i < retryCount; i++)
{
// VisualTree application never clears existing trees. Trees
// will be conditionally cleared on Template invalidation
if (!HasTemplateGeneratedSubTree)
{
// Create a VisualTree using the given template
visualsCreated = template.ApplyTemplateContent(dataField, this);
if (visualsCreated)
{
// This VisualTree was created via a Template
HasTemplateGeneratedSubTree = true;
// We may have had trigger actions that had to wait until the
// template subtree has been created. Invoke them now.
StyleHelper.InvokeDeferredActions(this, template);
// Notify sub-classes when the template tree has been created
OnApplyTemplate();
}
if (template != TemplateInternal)
{
template = TemplateInternal;
continue;
}
}
break;
}
OnPostApplyTemplate();
return visualsCreated;
}
方法的注释表明FrameworkElement 在每次measure 时都会调用这个方法,而我们知道measure 和arrange 是UIElement 进行布局的两个主要步骤。如果FrameworkElement 元素在布局其HasTemplateGeneratedSubTree 属性为false,那么就将调用FrameworkTemplate.ApplyTemplateContent()重新应用模板,生成visual tree。
这个方法的代码并不复杂,它先是调用虚方法OnPreApplyTemplate ();然后如果HasTemplateGeneratedSubTree 为false且TemplateInternal非空,则调用TemplateInternal 的ApplyTemplateContent ()方法生成相应的visual tree ,并调用虚方法OnApplyTemplate()(这个虚方法在开发自定义控件时经常需要重写,此时visual tree已经生成并可以访问了);最后调用虚方法***OnPostApplyTemplate()***完成收尾工作。
从上面的分析可以看到,FrameworkElement能生成什么样的visual tree,或者说生成的visual tree的结构,完全取决于其TemplateInternal。给这个属性一个什么样的模板,就会生成一个什么样的visual tree。换句话说,FrameworkElement的visual tree的模板完全是由TemplateInternal唯一提供的。那么这个神奇的TemplateInternal属性又是怎如何定义的呢?事实上,除了这个属性FrameworkElement还定义了一个FrameworkTemplate类型的属性TemplateCache。这两个属性的定义都很简单,代码如下:
csharp
//***************FrameworkElement********************
// Internal helper so the FrameworkElement could see the
// ControlTemplate/DataTemplate set on the
// Control/Page/PageFunction/ContentPresenter
internal virtual FrameworkTemplate TemplateInternal
{
get { return null; }
}
// Internal helper so the FrameworkElement could see the
// ControlTemplate/DataTemplate set on the
// Control/Page/PageFunction/ContentPresenter
internal virtual FrameworkTemplate TemplateCache
{
get { return null; }
set {}
}
可以看到二者的注释几乎都完全相同,也都是虚属性,FrameworkElement的子类可以通过覆写它们来实现多态性,提供自定义的模板。它们的自定义模板完全决定了它们的visual tree。事实上,利用工具我们可以看到只有4个FrameworkElement子类重写了TemplateInternal属性:Control、ContentPresenter、ItemsPresenter、Page,这意味着只有这4个类及其子类调用ApplyTemplate()才有意义。
现在问题是:FrameworkElement的子类具体是如何通过覆写虚属性TemplateInternal来自定义模板的呢?FrameworkTemplate的三个子类的变量有哪些?它们在这个过程中的角色又有何不同?
为了便于理解,下面我们将按照三个模板子类,分成四篇文章来讨论(由于DataTemplate的内容较多,被分成了两篇文章)。
2. ControlTemplate
ControlTemplate 类是最简单的FrameworkTemplate 子类,而最常见的ControlTemplate 类型变量是Control.Template属性。
上一篇我们提到,FrameworkElement子类要想生成自己的visual tree,就必须自定义一个模板给TemplateInternal属性。一个FrameworkElement子类元素的visual tree完全取决其提供给TemplateInternal属性的实际模板。
我们将看到,作为FrameworkElement的子类,Control除了覆写了TemplateInternal 和TemplateCache 属性,还新定义了一个ControlTemplate 类型的Template 属性:
csharp
//*****************Control********************
public static readonly DependencyProperty TemplateProperty =
DependencyProperty.Register(
"Template",
typeof(ControlTemplate),
typeof(Control),
new FrameworkPropertyMetadata(
(ControlTemplate) null, // default value
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnTemplateChanged)));
/// <summary>
/// Template Property
/// </summary>
public ControlTemplate Template
{
get { return _templateCache; }
set { SetValue(TemplateProperty, value); }
}
// Internal Helper so the FrameworkElement could see this property
internal override FrameworkTemplate TemplateInternal
{
get { return Template; }
}
// Internal Helper so the FrameworkElement could see the template cache
internal override FrameworkTemplate TemplateCache
{
get { return _templateCache; }
set { _templateCache = (ControlTemplate) value; }
}
// Internal helper so FrameworkElement could see call the template changed virtual
internal override void OnTemplateChangedInternal(FrameworkTemplate oldTemplate, FrameworkTemplate newTemplate)
{
OnTemplateChanged((ControlTemplate)oldTemplate, (ControlTemplate)newTemplate);
}
// Property invalidation callback invoked when TemplateProperty is invalidated
private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Control c = (Control) d;
StyleHelper.UpdateTemplateCache(c, (FrameworkTemplate) e.OldValue, (FrameworkTemplate) e.NewValue, TemplateProperty);
}
可以看到,覆写后,TemplateInternal 属性返回的就是Template ,而Template 属性返回的是**_templateCache字段,这个字段又是 TemplateCache属性的支撑字段,而TemplateCache属性只在StyleHelper.UpdateTemplateCache()**方法里被修改过。这个方法的代码如下:
csharp
//*****************StyleHelper*********************
internal static void UpdateTemplateCache(
FrameworkElement fe,
FrameworkTemplate oldTemplate,
FrameworkTemplate newTemplate,
DependencyProperty templateProperty)
{
DependencyObject d = fe;// Update the template cache
fe.TemplateCache = newTemplate;
// Do template property invalidations. Note that some of the invalidations may be callouts
// that could turn around and query the template property on this node. Hence it is essential
// to update the template cache before we do this operation.
StyleHelper.DoTemplateInvalidations(fe, oldTemplate);
// Now look for triggers that might want their EnterActions or ExitActions
// to run immediately.
StyleHelper.ExecuteOnApplyEnterExitActions(fe, null, newTemplate);
}
这意味着每次更新Control.Template 都会相应更新***_templateCache***,从而FrameworkElement.ApplyTemplate()读到的TemplateInternal 的值也就是Control.Template 的值。就这样,Control 类在无法覆写ApplyTemplate ()方法的情况下,实现了模板应用的多态性。值得一提的是,我们后面将看到,这种模式已经成了FrameworkElement 子类对虚属性TemplateInternal 实现多态性的固定模式。另外,前面我们提到只有4个FrameworkElement 的子类覆写了TemplateInternal属性:***Control、ContentPresenter、ItemsPresenter、Page,***因此可以期望在后面三种类里面也能找到类似的TemplateInternal多态性实现机制。
其他ControlTemplate类型的变量还有Page.Template,DataGrid.RowValidationErrorTemplate等。它们的模板机制与Control.Template大同小异,这里就不一一赘述了。下一篇文章我们将讨论ItemsPanelTemplate类。
3. ItemsPanelTemplate
上一篇文章我们讨论了ControlTemplate模板类,在这一篇我们将讨论ItemsPanelTemplate模板类。
temsPanelTemplate类型的变量主要有:ItemsControl.ItemsPanel ,ItemsPresenter.Template ,GroupStyle.Panel ,DataGridRow.ItemsPanel 等。这里重点讨论前两者,同时顺带提一下第三者。首先,ItemsControl.ItemsPanel属性定义如下:
csharp
//***************ItemsControl*****************
public static readonly DependencyProperty ItemsPanelProperty
= DependencyProperty.Register("ItemsPanel", typeof(ItemsPanelTemplate), typeof(ItemsControl),
new FrameworkPropertyMetadata(GetDefaultItemsPanelTemplate(),
OnItemsPanelChanged));
private static ItemsPanelTemplate GetDefaultItemsPanelTemplate()
{
ItemsPanelTemplate template = new ItemsPanelTemplate(new FrameworkElementFactory(typeof(StackPanel)));
template.Seal();
return template;
}
/// <summary>
/// ItemsPanel is the panel that controls the layout of items.
/// (More precisely, the panel that controls layout is created
/// from the template given by ItemsPanel.)
/// </summary>
public ItemsPanelTemplate ItemsPanel
{
get { return (ItemsPanelTemplate) GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsControl) d).OnItemsPanelChanged((ItemsPanelTemplate) e.OldValue, (ItemsPanelTemplate) e.NewValue);
}
protected virtual void OnItemsPanelChanged(ItemsPanelTemplate oldItemsPanel, ItemsPanelTemplate newItemsPanel)
{
ItemContainerGenerator.OnPanelChanged();
}
可以看到如果一个ItemsPresenter的TemplatedParent能够转换为一个ItemsControl,则其_owner字段(Owner属性)将指向这个ItemsControl,并将这个ItemsControl的ItemContainerGenerator属性作为唯一参数传给紧接着被调用的UseGenerator()方法。那么现在的关键是这个ItemsPresenter的TemplatedParent是从哪里来的?要回答这个问题我们需要参考一下ItemsControl的默认Template,其Xaml代码大致如下:
css
<Style x:Key="ItemsControlStyle1" TargetType="{x:Type ItemsControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<Border>
<ItemsPresenter/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
原来,ItemsControl根据Template模板生成自己的visual tree,在实例化ItemsPresenter时会刷新其TemplatedParent属性,将其指向自己。这个过程比较底层,我们只需要知道大致流程是这样就可以了。
此外,从注释也可以看出这个方法非常重要,FrameworkElement.ApplyTemplate()将用到它。事实上ItemsPresnter类覆写了FrameworkElement.OnPreApplyTemplate()方法,并在这里调用了这个方法:
csharp
//************ItemsPresenter**************
/// <summary> /// Called when the Template's tree is about to be generated /// </summary>
internal override void OnPreApplyTemplate()
{
base.OnPreApplyTemplate();
AttachToOwner();
}
ItemsPanelTemplate类型的变量主要有:ItemsControl.ItemsPanel ,ItemsPresenter.Template ,GroupStyle.Panel ,DataGridRow.ItemsPanel 等。这里重点讨论前两者,同时顺带提一下第三者。首先,ItemsControl.ItemsPanel属性定义如下:
csharp
//***************ItemsControl*****************
public static readonly DependencyProperty ItemsPanelProperty
= DependencyProperty.Register("ItemsPanel", typeof(ItemsPanelTemplate), typeof(ItemsControl),
new FrameworkPropertyMetadata(GetDefaultItemsPanelTemplate(),
OnItemsPanelChanged));
private static ItemsPanelTemplate GetDefaultItemsPanelTemplate()
{
ItemsPanelTemplate template = new ItemsPanelTemplate(new FrameworkElementFactory(typeof(StackPanel)));
template.Seal();
return template;
}
/// <summary>
/// ItemsPanel is the panel that controls the layout of items.
/// (More precisely, the panel that controls layout is created
/// from the template given by ItemsPanel.)
/// </summary>
public ItemsPanelTemplate ItemsPanel
{
get { return (ItemsPanelTemplate) GetValue(ItemsPanelProperty); }
set { SetValue(ItemsPanelProperty, value); }
}
private static void OnItemsPanelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsControl) d).OnItemsPanelChanged((ItemsPanelTemplate) e.OldValue, (ItemsPanelTemplate) e.NewValue);
}
protected virtual void OnItemsPanelChanged(ItemsPanelTemplate oldItemsPanel, ItemsPanelTemplate newItemsPanel)
{
ItemContainerGenerator.OnPanelChanged();
}
ItemsPresenter.AttachToOwner()方法的另一个重要工作是根据字段_generator的GroupStyle属性是否为空,来为Template属性选择模板。其中最关键的是倒数第二个语句:
template = (_owner != null) ? _owner.ItemsPanel : null;
这意味着,如果一个ItemsPresenter的TemplateParent是一个ItemsControl,而且不是用的groupStyle,这个ItemsPresenter的Template将被指向这个ItemsControl的ItemsPanel。这样ItemsControl.ItemsPanel就和ItemsPresenter.Template联系在了一起。
那么这个Template的作用是什么呢?事实上,ItemsPresenter继承自FrameworkElement,并覆写了TemplateInternal和TemplateCache属性。以下是相关代码:
csharp
//************ItemsPresenter**************
// Internal Helper so the FrameworkElement could see this property
internal override FrameworkTemplate TemplateInternal
{
get { return Template; }
}
// Internal Helper so the FrameworkElement could see the template cache
internal override FrameworkTemplate TemplateCache
{
get { return _templateCache; }
set { _templateCache = (ItemsPanelTemplate)value; }
}
internal static readonly DependencyProperty TemplateProperty =
DependencyProperty.Register(
"Template",
typeof(ItemsPanelTemplate),
typeof(ItemsPresenter),
new FrameworkPropertyMetadata(
(ItemsPanelTemplate) null, // default value
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnTemplateChanged)));
private ItemsPanelTemplate Template
{
get { return _templateCache; }
set { SetValue(TemplateProperty, value); }
}
// Internal helper so FrameworkElement could see call the template changed virtual
internal override void OnTemplateChangedInternal(FrameworkTemplate oldTemplate, FrameworkTemplate newTemplate)
{
OnTemplateChanged((ItemsPanelTemplate)oldTemplate, (ItemsPanelTemplate)newTemplate);
}
private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ItemsPresenter ip = (ItemsPresenter) d;
StyleHelper.UpdateTemplateCache(ip, (FrameworkTemplate) e.OldValue, (FrameworkTemplate) e.NewValue, TemplateProperty);
}
是否似曾相识?这些代码和Control 类几乎完全一样,除了Template 属性的类型从ControlTemplate 变成了ItemsPanelTemplate 。正如前面提到的,这是FrameworkElement 的子类对FrameworkElement.TemplateInternal 属性实现多态性的一种常用模式。这种模式的主要目的是提供一个通过修改Template属性来改变FrameworkElement.TemplateInternal属性值的机制。
由于流程比较复杂,我们这里概括一下:一个ItemsControl应用模板时,会实例化Template中的ItemsPresenter,并将其_templateParent字段指向这个ItemsControl。而在ApplyTemplate时,ItemsPresenter覆写了FrameworkElement.OnPreApplyTemplate()以调用AttachToOwner(),将_templateParent.ItemsPanel属性(或GroupStyle.Panel,如果设定了GroupStyle)的值赋给Template,从而实现TemplateInternal属性的多态性。
这里我们可以看到,ItemsPresenter的Template属性(ItemsPanelTemplate)实际是用的其TemplateParent属性(ItemsControl类型)的ItemsPanel属性(ItemsPanelTemplate)的值。也就是说,我们放在ItemsControl的Template里的ItemsPresenter是没有自己的模板的,它用的是这个ItemsControl的ItemsPanel模板。此时,这个ItemsPresenter只起到一个占位符(placeholder)的作用。在实际应用模板时,它将用这个ItemsControl的ItemsPanel模板来生成自己的visual tree。由于它自身没有模板,因此它的visual tree完全是ItemsPanel模板实例化的结果。它的作用就是一个占位符,即指定在Template的哪个位置放置ItemsControl的ItemsPanel模板生成的visual tree。我们下一篇文章将看到ContentPresenter与之类似,也是起到一个占位符的作用。这也是我们一般很少单独使用ItemsPresenter和ContentPresenter原因了。它们一般都会被放在Template里面,起到一个占位符的作用。
至此,ItemsPanelTemplate 类型的三个重要变量:ItemsControl.ItemsPanel 、ItemsPresenter.Template 和GroupStyle.Panel是如何被装配到***FrameworkElement.ApplyTemplate()***这个模板应用的流水线上的也就清楚了。
4. DataTemplate
上一篇文章我们讨论了ItemsPanelTemplate类,这一篇和下一篇将讨论DataTemplate类。
DataTemplate类型的变量比较多,主要有:
csharp
ComboBox.SelectionBoxItemTemplate
ContentControl.ContentTemplate
ContentPresenter.ContentTemplate
ContentPresenter.Template
DataGrid.RowHeaderTemplate
DataGridColumn.HeaderTemplate
DataGridRow.HeaderTemplate
DataGridRow.DetailsTemplate
DataGridTemplateColumn.CellTemplate
DataGridTemplateColumn.CellEditingTemplate
GridView.ColumnHeaderTemplate
GridViewColumn.HeaderTemplate
GridViewColumn.CellTemplate
GridViewHeaderRowPresenter.ColumnHeaderTemplate
GroupStyle.HeaderTemplate
HeaderedContentControl.HeaderTemplate
HeaderedItemsControl.HeaderTemplate
HierarchicalDataTemplate.ItemTemplate
ItemsControl.ItemTemplate
TabControl.SelectedContentTemplate
TabControl.ContentTemplate
我们这里只重点分析比较重要和有代表性的三个:ContentControl.ContentTemplate ,ContentPresenter.ContentTemplate 和ItemsControl.ItemTemplate 。由于内容较多,本篇文章只分析前两个,ItemsControl.ItemTemplate留待下一篇文章讨论。
4.1)ContentControl.ContentTemplate和ContentPresenter.ContentTemplate
ContentControl和ContentPresenter的父类是不相同的,分别是Control和FrameworkElement。ContentControl无疑继承了Control.Template属性和模板选择机制。那么ContentControl.ContentTemplate属性和其继承的Template属性究竟有什么关系?ContentControl和ContentPresenter的ContentTemplate属性在模板应用的角色是什么,二者又有什么联系?
要回答这些问题,我们先看ContentPresenter.ContentTemplate的定义:
csharp
//************ContentPresenter.cs**************
public static readonly DependencyProperty ContentTemplateProperty =
ContentControl.ContentTemplateProperty.AddOwner(
typeof(ContentPresenter),
new FrameworkPropertyMetadata(
(DataTemplate)null,
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnContentTemplateChanged)));
/// <summary>
/// ContentTemplate is the template used to display the content of the control.
/// </summary>
public DataTemplate ContentTemplate
{
get { return (DataTemplate) GetValue(ContentControl.ContentTemplateProperty); }
set { SetValue(ContentControl.ContentTemplateProperty, value); }
}
private static void OnContentTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ContentPresenter ctrl = (ContentPresenter)d;
ctrl._templateIsCurrent = false;
ctrl.OnContentTemplateChanged((DataTemplate) e.OldValue, (DataTemplate) e.NewValue);
}
protected virtual void OnContentTemplateChanged(DataTemplate oldContentTemplate, DataTemplate newContentTemplate)
{
Helper.CheckTemplateAndTemplateSelector("Content", ContentTemplateProperty, ContentTemplateSelectorProperty, this);
// if ContentTemplate is really changing, remove the old template
this.Template = null;
}
首先可以注意到依赖属性ContentTemplateProperty 的注册没有使用DependencyProperty.Register() ,而是用的ContentControl.ContentTemplateProperty.AddOwner() ,此外ContentTemplate 的读写也是用的ContentControl.ContentTemplateProperty 。这意味着如果一个ContentPresenter 处在一个ContentControlContent的visual tree上,那么其ContentTemplateProperty属性将继承这个ContentControl的ContentTemplateProperty的值。这就是WPF中依赖属性的继承。利用同样的方法,ContentPresenter 还继承了ContentControl.ContentProperty 属性。而我们还知道,就像ItemsControl 的默认Template会包含一个ItemsPresenter控件(参见上一篇文章),ContentControl 的默认Template模板也包含一个ContentPresenter控件。这意味着当ContentControl在应用模板生成visual tree时,将创建一个ContentPresenter控件,并把自己的ContentTemplate和Content属性的值传递给它的ContentPresenter控件,进而触发其调用自己的ApplyTemplate。ContentControl的模板应用就是这样一个大概可以分为两个步骤的级联过程,这与上一篇文章提到的,ItemsControl先应用自己的Template,然后这个Template中的ItemsPresenter再应用这个ItemsControl的ItemsPanel模板步骤类似。这里,ContentPresenter和ItemsPresenter没有自己的模板,应用的都是父控件(ContentControl和ItemsControl)的模板,它们都起到占位符的作用。
ContentControl的模板应用机制大致就这样了,不过为了搞清楚这个级联过程的第二个步骤,我们有必要进一步剖析一下ContentPresenter的模板应用机制。
首先,从回调函数可以看出,一旦ContentPresenter.ContentTemplate属性被改变,无论这种任何变化,ContentPresenter.Template属性都将被清空。这个属性的定义如下:
csharp
//***********ContentPresenter.cs**************
internal static readonly DependencyProperty TemplateProperty =
DependencyProperty.Register(
"Template",
typeof(DataTemplate),
typeof(ContentPresenter),
new FrameworkPropertyMetadata(
(DataTemplate) null, // default value
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnTemplateChanged)));
private DataTemplate Template
{
get { return _templateCache; }
set { SetValue(TemplateProperty, value); }
}
// Internal helper so FrameworkElement could see call the template changed virtual
internal override void OnTemplateChangedInternal(FrameworkTemplate oldTemplate, FrameworkTemplate newTemplate)
{
OnTemplateChanged((DataTemplate)oldTemplate, (DataTemplate)newTemplate);
}
private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ContentPresenter c = (ContentPresenter) d;
StyleHelper.UpdateTemplateCache(c, (FrameworkTemplate) e.OldValue, (FrameworkTemplate) e.NewValue, TemplateProperty);
}
这里可以看到,ContentPresenter.Template与Control.Template、ItemsPresenter.Template的定义如出一辙,都以_templateCache字段作为支撑字段,只不过Template的类型这次被换成了DataTemplate。不过与ItemsPresenter相同,ContentPresenter类也覆写了FrameworkElement.OnPreApplyTemplate()方法,自定义了一个模板选择机制。这个方法先是调用EnsureTemplate(),而后者接着又调用了ChooseTemplate()来根据一定的优先顺序来选择一个合适的DataTemplate,并用这个确定是非空的模板更新其Template属性进而_templateCache字段,从而保证Framework在调用ApplyTemplate()时TemplateInternal是非空的。
这里有必要贴一下ContentPresenter.ChooseTemplate()方法的源代码,看一下ContentPresenter选择模板的优先级:
csharp
//***********ContentPresenter**************
/// <summary>
/// Return the template to use. This may depend on the Content, or
/// other properties.
/// </summary>
/// <remarks>
/// The base class implements the following rules:
/// (a) If ContentTemplate is set, use it.
/// (b) If ContentTemplateSelector is set, call its
/// SelectTemplate method. If the result is not null, use it.
/// (c) Look for a DataTemplate whose DataType matches the
/// Content among the resources known to the ContentPresenter
/// (including application, theme, and system resources).
/// If one is found, use it.
/// (d) If the type of Content is "common", use a standard template.
/// The common types are String, XmlNode, UIElement.
/// (e) Otherwise, use a default template that essentially converts
/// Content to a string and displays it in a TextBlock.
/// Derived classes can override these rules and implement their own.
/// </remarks>
protected virtual DataTemplate ChooseTemplate()
{
DataTemplate template = null;
object content = Content;
// ContentTemplate has first stab
template = ContentTemplate;
// no ContentTemplate set, try ContentTemplateSelector
if (template == null)
{
if (ContentTemplateSelector != null)
{
template = ContentTemplateSelector.SelectTemplate(content, this);
}
}
// if that failed, try the default TemplateSelector
if (template == null)
{
template = DefaultTemplateSelector.SelectTemplate(content, this);
}
return template;
}
可以看出,ContentPresenter 在选择Template时,会优先选择ContentTemplate,如果为空,则会尝试调用ContentTemplateSelector.SelectTemplate()(DataTemplateSelector类型),如果再失败,会尝试调用其DefaultTemplateSelector.SelectTemplate()方法。
静态属性DefaultTemplateSelector是DefaultSelector类型 ,后者又继承自DataTemplateSelector类。DefaultSelector在覆写DataTemplateSelector.SelectTemplate()方法时引入了一套复杂的模板选择规则,以确保最终可以返回一个有效的DataTemplate:
csharp
//*******************DefaultSelector***********************
/// <summary>
/// Override this method to return an app specific <seealso cref="Template"/>.
/// </summary>
/// <param name="item">The data content</param>
/// <param name="container">The container in which the content is to be displayed</param>
/// <returns>a app specific template to apply.</returns>
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
DataTemplate template = null;
// Lookup template for typeof(Content) in resource dictionaries.
if (item != null)
{
template = (DataTemplate)FrameworkElement.FindTemplateResourceInternal(container, item, typeof(DataTemplate));
}
// default templates for well known types:
if (template == null)
{
TypeConverter tc = null;
string s;
if ((s = item as string) != null)
template = ((ContentPresenter)container).SelectTemplateForString(s);
else if (item is UIElement)
template = UIElementContentTemplate;
else if (SystemXmlHelper.IsXmlNode(item))
template = ((ContentPresenter)container).SelectTemplateForXML();
else if (item is Inline)
template = DefaultContentTemplate;
else if (item != null &&
(tc = TypeDescriptor.GetConverter(ReflectionHelper.GetReflectionType(item))) != null &&
tc.CanConvertTo(typeof(UIElement)))
template = UIElementContentTemplate;
else
template = DefaultContentTemplate;
}
return template;
}
}
至此,ContentPresenter在模板应用中的角色也一目了然了。
至此,两个重要的DataTemplate类型ContentControl.ContentTemplate 和ContentPresenter.ContentTemplate就介绍了完毕,下一篇文章将介绍DataTemplate类型的另一个重要变量ItemsControl.ItemTemplate。
上一篇文章我们讨论了DataTemplate类型的两个重要变量,ContentControl.ContentTemplate和ContentPresenter.ContentTemplate,这一篇将讨论这个类型的另一个重要变量ItemsControl.ItemTemplate。
4.2)ItemsControl.ItemTemplate
我们都知道ItemsControl控件在WPF中的重要性,ItemsControl.ItemTemplate用的也非常多,那么其在模板应用中的角色是什么呢?要回答这个问题,我们先看其定义:
csharp
public static readonly DependencyProperty ItemTemplateProperty =
DependencyProperty.Register(
"ItemTemplate",
typeof(DataTemplate),
typeof(ItemsControl),
new FrameworkPropertyMetadata(
(DataTemplate) null,
OnItemTemplateChanged));
/// <summary>
/// ItemTemplate is the template used to display each item.
/// </summary>public DataTemplate ItemTemplate
{
get { return (DataTemplate) GetValue(ItemTemplateProperty); }
set { SetValue(ItemTemplateProperty, value); }
}
private static void OnItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((ItemsControl) d).OnItemTemplateChanged((DataTemplate) e.OldValue, (DataTemplate) e.NewValue);
}
protected virtual void OnItemTemplateChanged(DataTemplate oldItemTemplate, DataTemplate newItemTemplate)
{
CheckTemplateSource();
if (_itemContainerGenerator != null)
{
_itemContainerGenerator.Refresh();
}
}
可以看到当ItemsControl.ItemTemplate改变时,会调用_itemContainerGenerator.Refresh()。这个方法的定义如下:
csharp
// regenerate everything
internal void Refresh()
{
OnRefresh();
}
// Called when the items collection is refreshed
void OnRefresh()
{
((IItemContainerGenerator)this).RemoveAll();
// tell layout what happened
if (ItemsChanged != null)
{
GeneratorPosition position = new GeneratorPosition(0, 0);
ItemsChanged(this, new ItemsChangedEventArgs(NotifyCollectionChangedAction.Reset, position, 0, 0));
}
}
可见这个方法调用OnRefresh(),后者的主要工作是清空已经生成的元素,并触发ItemsChanged事件,通知所有监听者列表已经被重置。
查找ItemsControl.ItemTemplate的引用会发现一个值得注意的方法ItemsControl.PrepareContainerForItemOverride:
csharp
//*********************ItemsControl*************************
/// <summary>
/// Prepare the element to display the item. This may involve
/// applying styles, setting bindings, etc.
/// </summary>
protected virtual void PrepareContainerForItemOverride(DependencyObject element, object item)
{
// Each type of "ItemContainer" element may require its own initialization.
// We use explicit polymorphism via internal methods for this.
//
// Another way would be to define an interface IGeneratedItemContainer with
// corresponding virtual "core" methods. Base classes (ContentControl,
// ItemsControl, ContentPresenter) would implement the interface
// and forward the work to subclasses via the "core" methods.
//
// While this is better from an OO point of view, and extends to
// 3rd-party elements used as containers, it exposes more public API.
// Management considers this undesirable, hence the following rather
// inelegant code.
HeaderedContentControl hcc;
ContentControl cc;
ContentPresenter cp;
ItemsControl ic;
HeaderedItemsControl hic;
if ((hcc = element as HeaderedContentControl) != null)
{
hcc.PrepareHeaderedContentControl(item, ItemTemplate, ItemTemplateSelector, ItemStringFormat);
}
else if ((cc = element as ContentControl) != null)
{
cc.PrepareContentControl(item, ItemTemplate, ItemTemplateSelector, ItemStringFormat);
}
else if ((cp = element as ContentPresenter) != null)
{
cp.PrepareContentPresenter(item, ItemTemplate, ItemTemplateSelector, ItemStringFormat);
}
else if ((hic = element as HeaderedItemsControl) != null)
{
hic.PrepareHeaderedItemsControl(item, this);
}
else if ((ic = element as ItemsControl) != null)
{
if (ic != this)
{
ic.PrepareItemsControl(item, this);
}
}
}
这个方法共两个参数,第一个参数element的作用是作为第二个参数item的容器(container),这个item实际就是ItemsControl.ItemsSource(IEnumerable类型)列表的数据项。这个方法的主要工作是根据参数element的类型,做一些准备工作:如HeaderedContentControl和HeaderedItemsControl会把ItemTemplate的值赋给HeaderTemplate,而ContentControl和ContentPresenter则会用它更新ContentTemplate。如果是element也是ItemsControl,这意味着一个ItemsControl的ItemTemplate里又嵌套了一个ItemsControl,这时就把父控件的ItemTemplate传递给子控件的ItemTemplate。
现在关键的问题是这里的参数element和item到底是怎么来的?要回答这个问题我们需要搞清楚ItemsControl.PrepareContainerForItemOverride()方法是怎么被调用的。查看引用可以发现ItemsControl.PrepareItemContainer()方法调用了这个方法,其代码如下:
这时ItemsPanel模板的设置将被直接忽略。不过,这时一定要将这个Panel的IsItemsHost设定为True,否则ItemsControl将找不到一个合适的ItemsPanel来显示列表项。
最后,结合第三篇文章的内容,我们再按照从上至下的顺序从整体上梳理一下ItemsControl的模板应用机制:一个ItemsControl在应用模板时,首先会应用Template模板(ControlTemplate类型)生成自身的visual tree(Control类的模板机制),然后Template模板中的ItemsPresenter应用其TemplateParent(即这个ItemsControl)的ItemsPanel模板(ItemsPanelTemplate类型)生成一个visual tree,并把这个visual tree放置在这个ItemsPresenter的位置(ItemsPresenter这时起到占位符的作用)。在ItemsPanel模板被应用时,这个面板的TemplateParent会被指向这个ItemsControl,同时其IsItemsHost属性被标识为true。ItemsControl的ItemContainerGeneror在遍历自己的ItemsInternal列表并为每个列表项(item)生成一个container,并将ItemsControl的ItemTemplate模板"转交"(forward)给这个container,这样这个container就可以应用模板,为与自己对应的数据项(item)生成一个由这个ItemTemplate定义的visual tree。当然具体过程要复杂的多。
5. 最后再强行总结一下WPF的模板机制:
1.FrameworkTemplate 是所有模板类的基类,FrameworkElement 类有一个FrameworkTemplate 类型的TemplateInternal 属性,FrameworkElement.ApplyTemplate()将使用这个属性的模板对象来生成visual tree ,并将这个visual tree赋值给自己的TemplateChild 属性,从而在两个Visual类对象之间建立起parent-child relationship;
2.FrameworkElement 的TemplateInternal 属性是虚属性,FrameworkElement 子类可以通过覆写这个属性来自定义模板。只有四个类Control 、ContentPresenter 、ItemsPresenter 、Page覆写了这个属性,这意味着只有这4个类及其子类控件才能应用自定义的模板,它们也是WPF模板机制的实现基础;
3.FrameworkTemplate 类有三个子类:ControlTemplate 、ItemsPanelTemplate 和DataTemplate 。WPF中这些模板类定义的变量很多,它们的内部实现也不尽相同,不过万变不离其宗,所有模板类最终都要把自己传递到FrameworkElement.TemplateInternal属性上,才能被应用,生成的visual tree才能被加载到整体的visual tree中。***FrameworkElement.ApplyTemplate()***方法是FrameworkElement及其子类模板应用的总入口。