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

13 篇文章 1 订阅

剖析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 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时都会调用这个方法,而我们知道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/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除了覆写了TemplateInternalTemplateCache属性,还新定义了一个ControlTemplate类型的Template属性:在这里插入图片描述

//*****************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()**方法里被修改过。这个方法的代码如下:

//*****************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.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 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.ItemsPanelItemsPresenter.TemplateGroupStyle.Panel是如何被装配到***FrameworkElement.ApplyTemplate()***这个模板应用的流水线上的也就清楚了。

4. DataTemplate

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

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

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

//***********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选择模板的优先级:

//***********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:

//*******************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.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 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:

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

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

是刘彦宏吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值