• 剖析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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注释表明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*******************        /// 
            /// Gets or sets the template child of the FrameworkElement.
            /// 
            virtual internal UIElement TemplateChild
            {
                get
                {
                    return _templateChild;
                }
                set
                {
                    if (value != _templateChild)
                    {
                        RemoveVisualChild(_templateChild);
                        _templateChild = value;
                        AddVisualChild(value);
                    }
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    由于FrameworkTemplate.ApplyTemplateContent()不是虚方面,因此其子类无法覆写。查看这个方法的引用我们可以看到,这个方法只在FrameworkElement.ApplyTemplate()里被调用了一次,这意味着FrameworkElement的这个方法是FrameworkElement及其子类实现模板应用的唯一入口。这个方法的重要性无论如何强调都不为过,以后我们还会多次提到这个方法。因此有必要贴一下其代码:

    //***************FrameworkElement********************        /// 
            /// ApplyTemplate is called on every Measure
            /// 
            /// 
            /// Used by subclassers as a notification to delay fault-in their Visuals
            /// Used by application authors ensure an Elements Visual tree is completely built
            /// 
            /// Whether Visuals were added to the tree
            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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    方法的注释表明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 {}
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到二者的注释几乎都完全相同,也都是虚属性,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)));
    
    
            /// 
            /// Template Property
            /// 
            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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    可以看到,覆写后,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);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这意味着每次更新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;
            }
    
            /// 
            ///     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.)
            /// 
            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();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    可以看到如果一个ItemsPresenter的TemplatedParent能够转换为一个ItemsControl,则其_owner字段(Owner属性)将指向这个ItemsControl,并将这个ItemsControl的ItemContainerGenerator属性作为唯一参数传给紧接着被调用的UseGenerator()方法。那么现在的关键是这个ItemsPresenter的TemplatedParent是从哪里来的?要回答这个问题我们需要参考一下ItemsControl的默认Template,其Xaml代码大致如下:

    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    原来,ItemsControl根据Template模板生成自己的visual tree,在实例化ItemsPresenter时会刷新其TemplatedParent属性,将其指向自己。这个过程比较底层,我们只需要知道大致流程是这样就可以了。

    此外,从注释也可以看出这个方法非常重要,FrameworkElement.ApplyTemplate()将用到它。事实上ItemsPresnter类覆写了FrameworkElement.OnPreApplyTemplate()方法,并在这里调用了这个方法:

    //************ItemsPresenter**************
    
    ///  /// Called when the Template's tree is about to be generated ///  
    internal override void OnPreApplyTemplate() 
    { 
      base.OnPreApplyTemplate(); 
      AttachToOwner(); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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;
            }
    
            /// 
            ///     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.)
            /// 
            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();
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    是否似曾相识?这些代码和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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    我们这里只重点分析比较重要和有代表性的三个: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)));
    
     
    
            /// 
            ///     ContentTemplate is the template used to display the content of the control.
            /// 
            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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    首先可以注意到依赖属性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);
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    这里可以看到,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**************
    
            /// 
            /// Return the template to use.  This may depend on the Content, or
            /// other properties.
            /// 
            /// 
            /// 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.
            /// 
            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;
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

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

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

    //*******************DefaultSelector***********************
    
                /// 
                /// Override this method to return an app specific .
                /// 
                /// The data content
                /// The container in which the content is to be displayed
                /// a app specific template to apply.
                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;
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    至此,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));
    
            /// 
            ///     ItemTemplate is the template used to display each item.
            /// 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();
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    可以看到当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));
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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

    //*********************ItemsControl*************************       
     /// 
            /// Prepare the element to display the item.  This may involve
            /// applying styles, setting bindings, etc.
            /// 
            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);
                    }
                }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    这个方法共两个参数,第一个参数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及其子类模板应用的总入口。

  • 相关阅读:
    【STM32F407+CUBEMX+FreeRTOS+lwIP之UDP记录】
    酒店品牌纷纷冲击中高端,东呈集团能否“快人一步”?
    Python Selenium 执行 JavaScript
    Xcode调试内存最新理解
    LinkedList与链表
    3.4JavaScript网页编程——JS高级(可以不看)
    深分页Scroll
    技术解密Java Chassis 3超实用的可观测性
    D-Desthiobiotin Amine_D-脱硫生物素-胺相关的产品性质
    tensorflow中的slim函数集合
  • 原文地址:https://blog.csdn.net/kalvin_y_liu/article/details/134255598