剖析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及其子类模板应用的总入口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/189091.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Maya 2024 for Mac(3D建模软件)

Maya 2024是一款三维计算机图形软件&#xff0c;具有强大的建模、动画、渲染、特效等功能&#xff0c;广泛应用于影视、游戏、广告等行业。以下是Maya 2024软件的主要功能介绍&#xff1a; 建模&#xff1a;Maya 2024具有强大的建模工具&#xff0c;包括多边形建模、曲面建模、…

Leetcode100120. 找出强数对的最大异或值 I

Every day a Leetcode 题目来源&#xff1a;100120. 找出强数对的最大异或值 I 解法1&#xff1a;模拟 枚举 2 遍数组 nums 的元素&#xff0c;更新最大异或值。 代码&#xff1a; /** lc appleetcode.cn id100120 langcpp** [100120] 找出强数对的最大异或值 I*/// lc c…

【大模型】大语言模型语料下载

文章目录 概述Hugging Faceobs操作git-lfs例子RedPajama-Data-1TSlimPajama-627B/git clone续传 数据格式参考资料 概述 大模型训练中语料是非常重要的&#xff0c;目前公网上有各种各样的语料可以供下载&#xff0c;但是不可能每个用户、每次训练任务都通过公网去拉取语料&am…

MYSQL操作详解

一)计算机的基本结构 但是实际上&#xff0c;更多的是这种情况: 二)MYSQL中的数据类型: 一)数值类型: 数据类型内存大小(字节)说明bit(M)M指定位数,默认为1单个二进制位值&#xff0c;或者为0或者为1&#xff0c;主要用于开/关标志tinyint1字节1个字节的整数值&#xff0c;支持…

快速走进通信世界 --- 基础知识扫盲

想不到吧&#xff0c;家人们&#xff0c;博主好久没来更新文章了&#xff0c;而且这次更新的是关于通信工程的文章。博主确实以前一直更新关于编程的文章&#xff0c;只不过最近在学习一些新的知识&#xff0c;以后有机会了我还是会继续更新一些编程技术文章的。不过每一门技术…

时序预测 | MATLAB实现WOA-CNN-BiGRU-Attention时间序列预测(SE注意力机制)

时序预测 | MATLAB实现WOA-CNN-BiGRU-Attention时间序列预测&#xff08;SE注意力机制&#xff09; 目录 时序预测 | MATLAB实现WOA-CNN-BiGRU-Attention时间序列预测&#xff08;SE注意力机制&#xff09;预测效果基本描述模型描述程序设计参考资料 预测效果 基本描述 1.MATLA…

数字政府!3DCAT实时云渲染助推上海湾区数字孪生平台

数字孪生&#xff0c;是一种利用物理模型、传感器数据、运行历史等信息&#xff0c;在虚拟空间中构建实体对象或系统的精确映射&#xff0c;从而实现对其全生命周期的仿真、优化和管理的技术。数字孪生可以应用于各个领域&#xff0c;如工业制造、智慧城市、医疗健康、教育培训…

idea使用git删除本地提交(未推送)

1、找到reset head 2、打开弹窗&#xff0c;在HEAD后面输入^ 结果为HEAD^ 注释&#xff1a; Reset Type 有三种&#xff1a; Mixed&#xff08;默认方式&#xff09;&#xff0c;保留本地源码&#xff0c;回退 commit 和 index 信息&#xff0c;最常用的方式Soft 回退到某个版本…

Android选项卡TabHost

选项卡主要由TabHost(标签&#xff0c;主人)&#xff0c;TabWidget(微件)和FrameLayout3个组件组成&#xff0c;用于实现一个多标签页的用户界面。 1. TabHost在XML文件中添加&#xff1a; XML布局文件中添加选项卡时必须使用系统id来为各组件指定id属性。 <TabHostandro…

1366 - Incorrect string value: ‘\xE5\xB9\xBF\xE5\x85\xB0...‘ for column编码错误

1366 - Incorrect string value: ‘\xE5\xB9\xBF\xE5\x85\xB0…’ for column ‘campus_name’ at row 1 > 查询时间: 0s 原因是数据库创建的时候使用的默认编码latin1&#xff0c;导致表和字段的编码格式都是这种编码&#xff0c;显然这种编码不支持中文。 自己修改了数据库…

MySQL表的增删改查(进阶)

目录 数据库约束 约束的定义 约束类型 null约束 unique:唯一约束 default:默认值约束 primary key:主键约束(重要) foreign key:外键约束(描述两个表之间的关联) 表的设计 一般思路 三大范式 一对一 一对多 ​编辑 多对多 ​编辑 新增 查询 聚合查询 聚合函…

leetcode(力扣) 51. N 皇后 (回溯,纸老虎题)

文章目录 题目描述思路分析对于问题1对于问题2 完整代码 题目描述 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数…

OpenCV C++ 图像处理实战 ——《多二维码识别》

OpenCV C++ 图像处理实战 ——《多二维码识别》 一、结果演示二、zxing库配置2.1下载编译三、多二维码识别3.1 Method one3.1.1 源码3.2 Method two3.2.1 源码四、源码测试图像下载总结一、结果演示 </

nginx https 如何将部分路径转移到 http

nginx https 如何将部分路径转移到 http 我有一个自己的网站&#xff0c;默认是走的 https&#xff0c;其中有一个路径需要走 http。 实现 在 nginx 的配置文件 https 中添加这个路径&#xff0c;并添加一个 rewrite 的指令。 比如我需要将 tools/iphone 的路径转成 http&am…

Jvm虚拟机

问题&#xff1a; 计算机能识别的语言是二进制&#xff0c;Java文件是程序员编写的&#xff0c;如何能够在计算机上运行&#xff1f; 以及Java为什么可以实现跨平台&#xff1f; 一Java的jdk中有jvm虚拟机 可以将文件转换为字节码文件 使得它可以在各种平台上运行&#xff0c;这…

网网络安全基础之php开发 文件读取、写入功能的实现

前言 续之前的系列&#xff0c;这里php开发的文件操作的内容读取以及文本写入的部分 文件读取代码的实现 css代码 本系列的php博客都是这个css&#xff0c;名字都是index.css /* css样式初始化 */ * {font-family: Poppins, sans-serif;margin: 0;padding: 0;box-sizing: …

HDRP图形入门:HDRP渲染管线depth翻转

新项目开坑HDRP渲染管线&#xff0c;花了些时间把项目开发框架和图形工作流更新到最新版本&#xff0c;其间发现HDRP中深度信息和buildin渲染管线翻转了。 以前的buildin渲染管线&#xff0c;距离摄像机越近depth->0&#xff0c;越远depth->1&#xff0c;这也很好理…

酷开科技持续推动智能投影行业创新发展

近年来&#xff0c;投影仪逐渐成为年轻人追捧的家居时尚单品。据国际数据公司&#xff08;IDC&#xff09;报告显示&#xff0c;2022年中国投影机市场总出货量505万台&#xff0c;超80%为家用投影仪。相比于电视&#xff0c;投影仪外观小巧、屏幕大小可调节&#xff0c;无论是卧…

Oracle获取执行计划的6种方法

一、什么是执行计划&#xff1f; 执行计划是一条查询语句在Oracle中的执行过程或访问路径的描述。 执行计划描述了SQL引擎为执行SQL语句进行的操作&#xff0c;分析SQL语句相关的性能问题或仅仅质疑查询优化器的决定时&#xff0c;必须知道执行计划&#xff1b;所以执行计划常用…