(注:本文是《剖析WPF模板机制的内部实现》系列文章的第三篇,查看上一篇文章请点这里)
3. ItemsPanelTemplate
上一篇文章我们讨论了ControlTemplate模板类,在这一篇我们将讨论ItemsPanelTemplate模板类。
ItemsPanelTemplate类型的变量主要有:ItemsControl.ItemsPanel,ItemsPresenter.Template,GroupStyle.Panel,DataGridRow.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(); }
从依赖属性ItemsPanelProperty注册的第一个参数可知ItemsControl.ItemsPanel默认用的是一个StackPanel控件。此外,其回调函数调用了ItemContainerGenerator.OnPanelChanged(),这个方法只有一个可执行语句:
//**************ItemContainerGenerator****************
internal void OnPanelChanged() { if (PanelChanged != null) PanelChanged(this, EventArgs.Empty); }
这个语句检查一个ItemContainerGenerator的PanelChanged事件是否被注册,如果有注册则调用事件处理函数。用代码工具查看,只有ItemsPresenter类注册了这个事件:
//*******************ItemsPresenter********************** void UseGenerator(ItemContainerGenerator generator) { if (generator == _generator) return; if (_generator != null) _generator.PanelChanged -= new EventHandler(OnPanelChanged); _generator = generator; if (_generator != null) _generator.PanelChanged += new EventHandler(OnPanelChanged); }
上面代码的意思概括就是,当一个ItemsControl的ItemsPanel属性改变时,会触发其ItemContainerGenerator属性的PanelChanged事件,而一个ItemsPresenter用自己的OnPanelChanged()方法注册了这个事件。
问题是这个ItemsPresenter是从哪里来的?又是如何与这个ItemsControl联系在一起的?要回答这个问题我们可以查看一下UseGenerator()的引用情况。这个方法一共被调用过两次,其中一次是在ItemsPresenter.AttachToOwner()。另外注意到,ItemsControl.ItemsPanel属性的唯一一次被引用,也是在这个方法。因此这个方法一个ItemsControl和其ItemsPresenter建立连接的关键所在。其代码如下:
//************ItemsPresenter**************
// initialize (called during measure, from ApplyTemplate) void AttachToOwner() { DependencyObject templatedParent = this.TemplatedParent; ItemsControl owner = templatedParent as ItemsControl; ItemContainerGenerator generator; if (owner != null) { // top-level presenter - get information from ItemsControl generator = owner.ItemContainerGenerator; } else { // subgroup presenter - get information from GroupItem GroupItem parentGI = templatedParent as GroupItem; ItemsPresenter parentIP = FromGroupItem(parentGI); if (parentIP != null) owner = parentIP.Owner; generator = (parentGI != null) ? parentGI.Generator : null; } _owner = owner; UseGenerator(generator); // create the panel, based either on ItemsControl.ItemsPanel or GroupStyle.Panel ItemsPanelTemplate template = null; GroupStyle groupStyle = (_generator != null) ? _generator.GroupStyle : null; if (groupStyle != null) { // If GroupStyle.Panel is set then we dont honor ItemsControl.IsVirtualizing template = groupStyle.Panel; if (template == null) { // create default Panels if (VirtualizingPanel.GetIsVirtualizingWhenGrouping(owner)) { template = GroupStyle.DefaultVirtualizingStackPanel; } else { template = GroupStyle.DefaultStackPanel; } } } else { // Its a leaf-level ItemsPresenter, therefore pick ItemsControl.ItemsPanel template = (_owner != null) ? _owner.ItemsPanel : null; } Template = template; }
可以看到如果一个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(); }
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.ItemsPanel、ItemsPresenter.Template和GroupStyle.Panel是如何被装配到FrameworkElement.ApplyTemplate()这个模板应用的流水线上的也就清楚了。
下一篇文章开始我们将讨论DataTemplate类。