当前位置: 首页 > news >正文

剖析WPF模板机制的内部实现

剖析WPF模板机制的内部实现

众所周知,在WPF框架中,Visual类是可以提供渲染(render)支持的最顶层的类,所有可视化元素(包括UIElementFrameworkElmentControl等)都直接或间接继承自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():

****************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已经可视化了。

*****************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及其子类实现模板应用的唯一入口。这个方法的重要性无论如何强调都不为过,以后我们还会多次提到这个方法。因此有必要贴一下其代码:

//***************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 invalidationif (!HasTemplateGeneratedSubTree){// Create a VisualTree using the given templatevisualsCreated = template.ApplyTemplateContent(dataField, this);if (visualsCreated){// This VisualTree was created via a TemplateHasTemplateGeneratedSubTree =  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 createdOnApplyTemplate();}if (template != TemplateInternal){template = TemplateInternal;continue;}}break;}OnPostApplyTemplate();return visualsCreated;}

方法的注释表明FrameworkElement在每次measure时都会调用这个方法,而我们知道measurearrangeUIElement进行布局的两个主要步骤。如果FrameworkElement元素在布局其HasTemplateGeneratedSubTree属性为false,那么就将调用FrameworkTemplate.ApplyTemplateContent()重新应用模板,生成visual tree

这个方法的代码并不复杂,它先是调用虚方法OnPreApplyTemplate();然后如果HasTemplateGeneratedSubTree为false且TemplateInternal非空,则调用TemplateInternalApplyTemplateContent()方法生成相应的visual tree,并调用虚方法OnApplyTemplate()(这个虚方法在开发自定义控件时经常需要重写,此时visual tree已经生成并可以访问了);最后调用虚方法***OnPostApplyTemplate()***完成收尾工作。

从上面的分析可以看到,FrameworkElement能生成什么样的visual tree,或者说生成的visual tree的结构,完全取决于其TemplateInternal。给这个属性一个什么样的模板,就会生成一个什么样的visual tree。换句话说,FrameworkElement的visual tree的模板完全是由TemplateInternal唯一提供的。那么这个神奇的TemplateInternal属性又是怎如何定义的呢?事实上,除了这个属性FrameworkElement还定义了一个FrameworkTemplate类型的属性TemplateCache。这两个属性的定义都很简单,代码如下:

//***************FrameworkElement********************// Internal helper so the FrameworkElement could see the// ControlTemplate/DataTemplate set on the// Control/Page/PageFunction/ContentPresenterinternal virtual FrameworkTemplate TemplateInternal{get { return null; }}// Internal helper so the FrameworkElement could see the// ControlTemplate/DataTemplate set on the// Control/Page/PageFunction/ContentPresenterinternal 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除了覆写了TemplateInternalTemplateCache属性,还新定义了一个ControlTemplate类型的Template属性:在这里插入图片描述

//*****************Control********************       public static readonly DependencyProperty TemplateProperty =DependencyProperty.Register("Template",typeof(ControlTemplate),typeof(Control),new FrameworkPropertyMetadata((ControlTemplate) null,  // default valueFrameworkPropertyMetadataOptions.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 propertyinternal override FrameworkTemplate TemplateInternal{get { return Template; }}// Internal Helper so the FrameworkElement could see the template cacheinternal override FrameworkTemplate TemplateCache{get { return _templateCache; }set { _templateCache = (ControlTemplate) value; }}// Internal helper so FrameworkElement could see call the template changed virtualinternal override void OnTemplateChangedInternal(FrameworkTemplate oldTemplate, FrameworkTemplate newTemplate){OnTemplateChanged((ControlTemplate)oldTemplate, (ControlTemplate)newTemplate);}// Property invalidation callback invoked when TemplateProperty is invalidatedprivate 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()**方法里被修改过。这个方法的代码如下:

//*****************StyleHelper*********************internal static void UpdateTemplateCache(FrameworkElement fe,FrameworkTemplate oldTemplate,FrameworkTemplate newTemplate,DependencyProperty templateProperty){DependencyObject d = fe;// Update the template cachefe.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.ItemsPanelItemsPresenter.TemplateGroupStyle.PanelDataGridRow.ItemsPanel等。这里重点讨论前两者,同时顺带提一下第三者。首先,ItemsControl.ItemsPanel属性定义如下:

//***************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代码大致如下:

<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()方法,并在这里调用了这个方法:

//************ItemsPresenter**************/// <summary> /// Called when the Template's tree is about to be generated /// </summary> 
internal override void OnPreApplyTemplate() 
{ base.OnPreApplyTemplate(); AttachToOwner(); 
}

ItemsPanelTemplate类型的变量主要有:ItemsControl.ItemsPanelItemsPresenter.TemplateGroupStyle.PanelDataGridRow.ItemsPanel等。这里重点讨论前两者,同时顺带提一下第三者。首先,ItemsControl.ItemsPanel属性定义如下:

//***************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属性。以下是相关代码:

//************ItemsPresenter**************// Internal Helper so the FrameworkElement could see this propertyinternal override FrameworkTemplate TemplateInternal{get { return Template; }}// Internal Helper so the FrameworkElement could see the template cacheinternal 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 valueFrameworkPropertyMetadataOptions.AffectsMeasure,new PropertyChangedCallback(OnTemplateChanged)));private ItemsPanelTemplate Template{get {  return _templateCache; }set { SetValue(TemplateProperty, value); }}// Internal helper so FrameworkElement could see call the template changed virtualinternal 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.ItemsPanelItemsPresenter.TemplateGroupStyle.Panel是如何被装配到***FrameworkElement.ApplyTemplate()***这个模板应用的流水线上的也就清楚了。

4. DataTemplate

上一篇文章我们讨论了ItemsPanelTemplate类,这一篇和下一篇将讨论DataTemplate类。

DataTemplate类型的变量比较多,主要有:

ComboBox.SelectionBoxItemTemplateContentControl.ContentTemplateContentPresenter.ContentTemplateContentPresenter.TemplateDataGrid.RowHeaderTemplateDataGridColumn.HeaderTemplateDataGridRow.HeaderTemplateDataGridRow.DetailsTemplateDataGridTemplateColumn.CellTemplateDataGridTemplateColumn.CellEditingTemplateGridView.ColumnHeaderTemplateGridViewColumn.HeaderTemplateGridViewColumn.CellTemplateGridViewHeaderRowPresenter.ColumnHeaderTemplateGroupStyle.HeaderTemplateHeaderedContentControl.HeaderTemplateHeaderedItemsControl.HeaderTemplateHierarchicalDataTemplate.ItemTemplateItemsControl.ItemTemplateTabControl.SelectedContentTemplateTabControl.ContentTemplate

我们这里只重点分析比较重要和有代表性的三个:ContentControl.ContentTemplateContentPresenter.ContentTemplateItemsControl.ItemTemplate。由于内容较多,本篇文章只分析前两个,ItemsControl.ItemTemplate留待下一篇文章讨论。

4.1)ContentControl.ContentTemplate和ContentPresenter.ContentTemplate

ContentControl和ContentPresenter的父类是不相同的,分别是Control和FrameworkElement。ContentControl无疑继承了Control.Template属性和模板选择机制。那么ContentControl.ContentTemplate属性和其继承的Template属性究竟有什么关系?ContentControl和ContentPresenter的ContentTemplate属性在模板应用的角色是什么,二者又有什么联系?

要回答这些问题,我们先看ContentPresenter.ContentTemplate的定义:

//************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 templatethis.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属性都将被清空。这个属性的定义如下:

//***********ContentPresenter.cs**************internal static readonly DependencyProperty TemplateProperty =DependencyProperty.Register("Template",typeof(DataTemplate),typeof(ContentPresenter),new FrameworkPropertyMetadata((DataTemplate) null,  // default valueFrameworkPropertyMetadataOptions.AffectsMeasure,new PropertyChangedCallback(OnTemplateChanged)));private DataTemplate Template{get {  return _templateCache; }set { SetValue(TemplateProperty, value); }}// Internal helper so FrameworkElement could see call the template changed virtualinternal 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选择模板的优先级:

//***********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 stabtemplate = ContentTemplate;// no ContentTemplate set, try ContentTemplateSelectorif (template == null){if (ContentTemplateSelector != null){template = ContentTemplateSelector.SelectTemplate(content, this);}}// if that failed, try the default TemplateSelectorif (template == null){template = DefaultTemplateSelector.SelectTemplate(content, this);}return template;}

可以看出,ContentPresenter在选择Template时,会优先选择ContentTemplate,如果为空,则会尝试调用ContentTemplateSelector.SelectTemplate()(DataTemplateSelector类型),如果再失败,会尝试调用其DefaultTemplateSelector.SelectTemplate()方法。

静态属性DefaultTemplateSelector是DefaultSelector类型 ,后者又继承自DataTemplateSelector类。DefaultSelector在覆写DataTemplateSelector.SelectTemplate()方法时引入了一套复杂的模板选择规则,以确保最终可以返回一个有效的DataTemplate:

//*******************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;elsetemplate = DefaultContentTemplate;}return template;}}

至此,ContentPresenter在模板应用中的角色也一目了然了。

至此,两个重要的DataTemplate类型ContentControl.ContentTemplateContentPresenter.ContentTemplate就介绍了完毕,下一篇文章将介绍DataTemplate类型的另一个重要变量ItemsControl.ItemTemplate。

上一篇文章我们讨论了DataTemplate类型的两个重要变量,ContentControl.ContentTemplate和ContentPresenter.ContentTemplate,这一篇将讨论这个类型的另一个重要变量ItemsControl.ItemTemplate。

4.2)ItemsControl.ItemTemplate

我们都知道ItemsControl控件在WPF中的重要性,ItemsControl.ItemTemplate用的也非常多,那么其在模板应用中的角色是什么呢?要回答这个问题,我们先看其定义:

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()。这个方法的定义如下:

// regenerate everythinginternal void Refresh(){OnRefresh();}// Called when the items collection is refreshedvoid OnRefresh(){((IItemContainerGenerator)this).RemoveAll();// tell layout what happenedif (ItemsChanged != null){GeneratorPosition position = new GeneratorPosition(0, 0);ItemsChanged(this, new ItemsChangedEventArgs(NotifyCollectionChangedAction.Reset, position, 0, 0));}}

可见这个方法调用OnRefresh(),后者的主要工作是清空已经生成的元素,并触发ItemsChanged事件,通知所有监听者列表已经被重置。

查找ItemsControl.ItemTemplate的引用会发现一个值得注意的方法ItemsControl.PrepareContainerForItemOverride:

//*********************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.FrameworkElementTemplateInternal属性是虚属性,FrameworkElement子类可以通过覆写这个属性来自定义模板。只有四个类ControlContentPresenterItemsPresenterPage覆写了这个属性,这意味着只有这4个类及其子类控件才能应用自定义的模板,它们也是WPF模板机制的实现基础;

3.FrameworkTemplate类有三个子类:ControlTemplateItemsPanelTemplateDataTemplate。WPF中这些模板类定义的变量很多,它们的内部实现也不尽相同,不过万变不离其宗,所有模板类最终都要把自己传递到FrameworkElement.TemplateInternal属性上,才能被应用,生成的visual tree才能被加载到整体的visual tree中。***FrameworkElement.ApplyTemplate()***方法是FrameworkElement及其子类模板应用的总入口。

相关文章:

  • 深度学习(生成式模型)——Classifier Guidance Diffusion
  • 说话人识别声纹识别CAM++,ECAPA-TDNN等算法
  • 月销破30万辆后,比亚迪整了波大的
  • 【Windows】Windows系统常用命令大全
  • 快速实现一个企业级域名 SSL 证书有效期监控巡检系统
  • STM32-EXTI中断
  • 社区街道治安智慧监管方案,AI算法赋能城市基层精细化治理
  • 2011年09月06日 Go生态洞察:Go语言的反射法则
  • 贰[2],QT异常处理
  • 2022美亚杯团队赛
  • uni-app 、Spring Boot 、ant Design 打造的一款跨平台包含小说(仿真翻页、段落听书)、短视频、壁纸等功能含完备后台管理的移动应用
  • 【面经】ES中分片是什么?副本是什么?
  • blender动画制作软件拓扑全流程
  • TensorFlow学习笔记--(2)张量的常用运算函数
  • debian 已安装命令找不到 解决方法
  • 分享的文章《人生如棋》
  • [nginx文档翻译系列] 控制nginx
  • [译]CSS 居中(Center)方法大合集
  • CSS魔法堂:Absolute Positioning就这个样
  • Java新版本的开发已正式进入轨道,版本号18.3
  • js作用域和this的理解
  • Shadow DOM 内部构造及如何构建独立组件
  • Terraform入门 - 1. 安装Terraform
  • vue的全局变量和全局拦截请求器
  • Vue组件定义
  • 得到一个数组中任意X个元素的所有组合 即C(n,m)
  • 近期前端发展计划
  • 看域名解析域名安全对SEO的影响
  • 浅谈web中前端模板引擎的使用
  • 使用Maven插件构建SpringBoot项目,生成Docker镜像push到DockerHub上
  • ​secrets --- 生成管理密码的安全随机数​
  • #周末课堂# 【Linux + JVM + Mysql高级性能优化班】(火热报名中~~~)
  • (70min)字节暑假实习二面(已挂)
  • (BFS)hdoj2377-Bus Pass
  • (附源码)spring boot车辆管理系统 毕业设计 031034
  • (附源码)ssm高校实验室 毕业设计 800008
  • (四)docker:为mysql和java jar运行环境创建同一网络,容器互联
  • (转) Android中ViewStub组件使用
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (转)利用ant在Mac 下自动化打包签名Android程序
  • (轉貼) 蒼井そら挑戰筋肉擂台 (Misc)
  • .net mvc actionresult 返回字符串_.NET架构师知识普及
  • .net wcf memory gates checking failed
  • .net 怎么循环得到数组里的值_关于js数组
  • .NET6 开发一个检查某些状态持续多长时间的类
  • @angular/cli项目构建--Dynamic.Form
  • @DataRedisTest测试redis从未如此丝滑
  • @javax.ws.rs Webservice注解
  • @Mapper作用
  • @transaction 提交事务_【读源码】剖析TCCTransaction事务提交实现细节
  • @zabbix数据库历史与趋势数据占用优化(mysql存储查询)
  • [ 常用工具篇 ] POC-bomber 漏洞检测工具安装及使用详解
  • [ 蓝桥杯Web真题 ]-Markdown 文档解析
  • [2019.3.5]BZOJ1934 [Shoi2007]Vote 善意的投票
  • [Angular] 笔记 9:list/detail 页面以及@Output