- A+
(注:本文是《剖析WPF模板机制的内部实现》系列文章的最后一篇文章,查看上一篇文章请点这里)
上一篇文章我们讨论了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的类型,做一些准备工作。如HeaderedContentControl和HeaderedItemsControl会把ItemTemplate的值赋给HeaderTemplate,而ContentControl和ContentPresenter则会用它更新ContentTemplate。如果是element也是ItemsControl,这意味着一个ItemsControl的ItemTemplate里又嵌套了一个ItemsControl,这时就把父控件的ItemTemplate传递给子控件的ItemTemplate。
那么问题是ItemsControl.PrepareContainerForItemOverride()方法什么时候会被调用呢?查看引用发现,这个方法会被ItemsControl.PrepareItemContainer()调用,后者又会被ItemContainerGenerator.PrepareItemContainer()方法调用。
继续追踪发现,Panel类通过Generator属性调用了ItemContainerGenerator.PrepareItemContainer()方法。Panel.Generator属性是一个ItemContainerGenerator类型的只读属性,其支撑字段是_itemContainerGenerator。那么Panel的Generator属性又是从哪里得到的呢?秘密就在下面这个方法:
//*************Panel*************
private void ConnectToGenerator() { ItemsControl itemsOwner = ItemsControl.GetItemsOwner(this); if (itemsOwner == null) { // This can happen if IsItemsHost=true, but the panel is not nested in an ItemsControl throw new InvalidOperationException(SR.Get(SRID.Panel_ItemsControlNotFound)); } IItemContainerGenerator itemsOwnerGenerator = itemsOwner.ItemContainerGenerator; if (itemsOwnerGenerator != null) { _itemContainerGenerator = itemsOwnerGenerator.GetItemContainerGeneratorForPanel(this); if (_itemContainerGenerator != null) { _itemContainerGenerator.ItemsChanged += new ItemsChangedEventHandler(OnItemsChanged); ((IItemContainerGenerator)_itemContainerGenerator).RemoveAll(); } } }
可以看到,这个方法会先调用静态方法ItemsControl.GetItemsOwner()获得这个Panel所处的ItemsControl。这个方法的定义如下:
//****************ItemsControl*******************
/// <summary> /// Returns the ItemsControl for which element is an ItemsHost. /// More precisely, if element is marked by setting IsItemsHost="true" /// in the style for an ItemsControl, or if element is a panel created /// by the ItemsPresenter for an ItemsControl, return that ItemsControl. /// Otherwise, return null. /// </summary> public static ItemsControl GetItemsOwner(DependencyObject element) { ItemsControl container = null; Panel panel = element as Panel; if (panel != null && panel.IsItemsHost) { // see if element was generated for an ItemsPresenter ItemsPresenter ip = ItemsPresenter.FromPanel(panel); if (ip != null) { // if so use the element whose style begat the ItemsPresenter container = ip.Owner; } else { // otherwise use element's templated parent container = panel.TemplatedParent as ItemsControl; } } return container;
}
这个方法的注释已经说的很清楚了:在获取一个Panel所处的ItemsControl时,如果这个Panel的IsItemsHost属性非真则返回空值;不然,那么如果这个Panel的TemplateParent是ItemsPresenter,则返回其Owner,否则则直接返回这个Panel的TemplateParent。在知道自己所在的ItemsControl后,这个Panel就能调用这个ItemsControl的ItemContainerGenerator属性的GetItemContainerGeneratorForPanel()方法来获得一个正确的ItemContainerGenerator给其_itemContainerGenerator字段(Panel的Generator属性)赋值。
那么这里说的这个Panel是怎么出现在ItemsControl里的呢?要回答这个问题可以看一下ItemsControl的默认ItemsPanel模板:
<ItemsPanelTemplate x:Key="ItemsPanelTemplate1"> <StackPanel IsItemsHost="True"/> </ItemsPanelTemplate>
此外,我们前面在介绍ItemsPanelTemplate时也提到过,ItemsControl.ItemsPanel属性的默认值就是StackPanel,这里重贴一下代码:
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; }
我们知道可以用作ItemsControl的ItemsPanel模板的控件基本都是Panel类的子类。而ItemsControl的默认ItemsPanel模板就用了StackPanel(此外,查看源代码可以看到ListBox和ListView的默认ItemsPanel都是VirtualizingStackPanel,Menu类是WrapPanel,StatusBar类是DockPanel)。另外,要作为ItemDataTemplate生成的item container的容器,这个StackPanel的IsItemHost的值也必须为真。
结合第三篇文章的内容,这里我们按照从上至下的顺序梳理ItemsControl的模板应用机制:一个ItemsControl在应用模板时,首先会应用Template属性定义的模板生成自身的visual tree(这也是继承的Control类的模板机制),然后Template中的ItemsPresenter应用它的TemplateParent(即这个ItemsControl)的ItemsPanel模板生成一个visual tree,并把这个visual tree放置在这个ItemsPresenter的位置。ItemsPresenter只起一个占位符的作用,它不会定义自己的模板。特别的,要正确显示列表内容,ItemsPanel必须包含一个IsItemHost属性为真的面板。在ItemsPanel模板被应用时,这个面板会通过ConnectToGenerator()方法获得这个ItemsControl的ItemContainerGenrator,并调用其PrepareItemContainer()方法,并把这个ItemsContro的ItemTemplate模板装配到container相应的模板属性上,以供在呈现数据项(item)生成visual tree时使用。
(本文完结)
(写的有点乱,待完善)
(感谢阅读,欢迎批评指正,转载请注明出处!)