(注:本文是《剖析WPF模板機制的內部實現》系列文章的最後一篇文章,查看上一篇文章請點這裏)html
上一篇文章咱們討論了DataTemplate類型的兩個重要變量,ContentControl.ContentTemplate和ContentPresenter.ContentTemplate,這一篇將討論這個類型的另外一個重要變量ItemsControl.ItemTemplate。app
4.2)ItemsControl.ItemTemplateless
咱們都知道ItemsControl控件在WPF中的重要性,ItemsControl.ItemTemplate用的也很是多,那麼其在模板應用中的角色是什麼呢?要回答這個問題,咱們先看其定義:ide
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(); } }
能夠看到當ItemsControl.ItemTemplate改變時,會調用_itemContainerGenerator.Refresh()。這個方法的定義以下:ui
// 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事件,通知全部監聽者列表已經被重置。this
查找ItemsControl.ItemTemplate的引用會發現一個值得注意的方法ItemsControl.PrepareContainerForItemOverride:spa
//*********************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); } } }
這個方法共兩個參數,第一個參數element的做用是做爲第二個參數item的容器(container),這個item實際就是ItemsControl.ItemsSource(IEnumerable類型)列表的數據項。這個方法的主要工做是根據參數element的類型,作一些準備工做:如HeaderedContentControl和HeaderedItemsControl會把ItemTemplate的值賦給HeaderTemplate,而ContentControl和ContentPresenter則會用它更新ContentTemplate。若是是element也是ItemsControl,這意味着一個ItemsControl的ItemTemplate裏又嵌套了一個ItemsControl,這時就把父控件的ItemTemplate傳遞給子控件的ItemTemplate。code
如今關鍵的問題是這裏的參數element和item究竟是怎麼來的?要回答這個問題咱們須要搞清楚ItemsControl.PrepareContainerForItemOverride()方法是怎麼被調用的。查看引用能夠發現ItemsControl.PrepareItemContainer()方法調用了這個方法,其代碼以下:orm
//**************ItemsControl**************** /// /// Prepare the element to act as the ItemContainer for the corresponding item. /// void IGeneratorHost.PrepareItemContainer(DependencyObject container, object item) { // GroupItems are special - their information comes from a different place GroupItem groupItem = container as GroupItem; if (groupItem != null) { groupItem.PrepareItemContainer(item, this); return; } if (ShouldApplyItemContainerStyle(container, item)) { // apply the ItemContainer style (if any) ApplyItemContainerStyle(container, item); } // forward ItemTemplate, et al. PrepareContainerForItemOverride(container, item); .........此處省略**行代碼 }
能夠看到這個方法調首先會檢查這個container是否GroupItem類型,若是不是則調用PrepareContainerForItemOverride(),並把container做爲其第一參數。繼續追蹤這個方法的引用,咱們會發現:ItemContainerGenerator類的PrepareItemContainer()調用了這個方法:htm
//*****************ItemContainerGenerator********************
/// /// Prepare the given element to act as the container for the /// corresponding item. This includes applying the container style, /// forwarding information from the host control (ItemTemplate, etc.), /// and other small adjustments. /// /// /// This method must be called after the element has been added to the /// visual tree, so that resource references and inherited properties /// work correctly. /// /// The container to prepare. /// Normally this is the result of the previous call to GenerateNext. /// void IItemContainerGenerator.PrepareItemContainer(DependencyObject container) { object item = container.ReadLocalValue(ItemForItemContainerProperty); Host.PrepareItemContainer(container, item); }
這個方法的註釋講的比較清楚:這個方法的做用是對這個containter作一些預處理工做,包括應用樣式,「轉交」(forward)一些來自宿主控件(咱們這個例子是ItemsControl)的信息(例如ItemTemplate等)。爲了讓資源引用和依賴屬性繼承正常工做,這個container在被加入visual tree前必須調用這個方法。
這個方法的第一個語句告訴咱們數據項item能夠經過container讀取ItemForItemContainerProperty的值得到。這個屬性是附加屬性,ItemContainerGenerator有一個靜態方法LinkContainerToItem(),是專門用來爲每一個container設定(鏈接)這個屬性的:
//*****************ItemContainerGenerator********************
// establish the link from the container to the corresponding item internal static void LinkContainerToItem(DependencyObject container, object item) { // always set the ItemForItemContainer property container.ClearValue(ItemForItemContainerProperty); container.SetValue(ItemForItemContainerProperty, item); // for non-direct items, set the DataContext property if (container != item) { container.SetValue(FrameworkElement.DataContextProperty, item); } }
這個方法在ItemContainerGenerator類內部被屢次調用,其中關鍵的一次是在其內部類Generator的GenerateNext()方法。此外這個方法關於參數container的註釋也代表,這個container是經過調用GenerateNext()得到的。所以這個方法無疑是異常重要的,其代碼以下:
//****************ItemContainerGenerator******************* /// Generate UI for the next item or group public DependencyObject GenerateNext(bool stopAtRealized, out bool isNewlyRealized) { DependencyObject container = null; isNewlyRealized = false; while (container == null) { UnrealizedItemBlock uBlock = _cachedState.Block as UnrealizedItemBlock; IList items =_factory.ItemsInternal; int itemIndex = _cachedState.ItemIndex; int incr = (_direction == GeneratorDirection.Forward) ? +1 : -1; if (_cachedState.Block == _factory._itemMap) _done = true; // we've reached the end of the list if (uBlock == null && stopAtRealized) _done = true; if (!(0 <= itemIndex && itemIndex < items.Count)) _done = true; if (_done) { isNewlyRealized = false; return null; } object item = items[itemIndex]; if (uBlock != null) { // We don't have a realized container for this item. Try to use a recycled container // if possible, otherwise generate a new container. isNewlyRealized = true; CollectionViewGroup group = item as CollectionViewGroup; // DataGrid needs to generate DataGridRows for special items like NewItemPlaceHolder and when adding a new row. // Generate a new container for such cases. bool isNewItemPlaceHolderWhenGrouping = (_factory._generatesGroupItems && group == null); if (_factory._recyclableContainers.Count > 0 && !_factory.Host.IsItemItsOwnContainer(item) && !isNewItemPlaceHolderWhenGrouping) { container = _factory._recyclableContainers.Dequeue(); isNewlyRealized = false; } else { if (group == null || !_factory.IsGrouping) { // generate container for an item container = _factory.Host.GetContainerForItem(item); } else { // generate container for a group container = _factory.ContainerForGroup(group); } } // add the (item, container) to the current block if (container != null) { ItemContainerGenerator.LinkContainerToItem(container, item); _factory.Realize(uBlock, _cachedState.Offset, item, container); // set AlternationIndex on the container (and possibly others) _factory.SetAlternationIndex(_cachedState.Block, _cachedState.Offset, _direction); } } else { // return existing realized container isNewlyRealized = false; RealizedItemBlock rib = (RealizedItemBlock)_cachedState.Block; container = rib.ContainerAt(_cachedState.Offset); } // advance to the next item _cachedState.ItemIndex = itemIndex; if (_direction == GeneratorDirection.Forward) { _cachedState.Block.MoveForward(ref _cachedState, true); } else { _cachedState.Block.MoveBackward(ref _cachedState, true); } } return container; }
從代碼能夠看出,一個Generator在GenerateNext時,須要從_factory的ItemsInternal列表讀取一個item,而後再調用_factory字段的Host屬性的GetContainerForItem()方法來爲這個item生成一個container。那麼這個關鍵的_factory字段從何而來?事實上,_factory字段是ItemsContainerGenerator類型,另外ItemsContainerGenerator類內部有一個Generator類型的字段_generator,這個字段在建立對象時會將這個ItemsContainerGenerator自身做爲參數,傳給其_factory字段。
Generator.GenerateNext()方法一共被調用了兩次,都是在兩個被重載的ItemsContainerGenerator.GenerateNext()方法裏:
DependencyObject IItemContainerGenerator.GenerateNext() { bool isNewlyRealized; if (_generator == null) throw new InvalidOperationException(SR.Get(SRID.GenerationNotInProgress)); return _generator.GenerateNext(true, out isNewlyRealized); } DependencyObject IItemContainerGenerator.GenerateNext(out bool isNewlyRealized) { if (_generator == null) throw new InvalidOperationException(SR.Get(SRID.GenerationNotInProgress)); return _generator.GenerateNext(false, out isNewlyRealized); }
其中,第一個方法比較重要,它一共被調用了三次,其中兩次是在Panel類:一次在Panel.GenerateChildren(),一次在Panel.AddChildren()。兩者大同小異,咱們只看第一個就足夠了:
internal virtual void GenerateChildren() { // This method is typically called during layout, which suspends the dispatcher. // Firing an assert causes an exception "Dispatcher processing has been suspended, but messages are still being processed." // Besides, the asserted condition can actually arise in practice, and the // code responds harmlessly. IItemContainerGenerator generator = (IItemContainerGenerator)_itemContainerGenerator; if (generator != null) { using (generator.StartAt(new GeneratorPosition(-1, 0), GeneratorDirection.Forward)) { UIElement child; while ((child = generator.GenerateNext() as UIElement) != null) { _uiElementCollection.AddInternal(child); generator.PrepareItemContainer(child); } } } }
從代碼能夠看到,Panel會循環調用其_itemContainerGenerator字段(Generator屬性)的GenerateNext()方法直到沒法繼續。每次調用都會生成一個UIElement類型的child,這個child將被加入Panel的內部UI元素列表,並對其調用_itemContainerGenerator.PrepareItemContainer(child)方法。而這裏的這個child就是咱們前面提到的container。
至此,container和item的前因後果咱們算基本搞清楚了。
可是,這裏的問題是,Panel類的這個神祕的_itemContainerGenerator字段是從哪裏來的?一個Panel怎麼會和ItemContainerGenerator扯上關係?祕密就在下面這個方法:
//*************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******************* /// /// 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. /// 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的TemplateParent是怎麼和一個ItemsControl扯上關係的?咱們在第三篇文章介紹ItemsPanelTemplate時曾提到過,ItemsControl的默認Template裏的ItemsPresenter只起一個佔位符(placeholder)的做用,它的主要角色是接收ItemsControl的ItemsPanel模板,並在ItemsControl應用模板時應用這個模板。
咱們再能夠看一下ItemsControl的默認ItemsPanel模板:
<ItemsPanelTemplate x:Key="ItemsPanelTemplate1"> <StackPanel/> ItemsPanelTemplate>
(咱們前面提到過,ItemsControl類在註冊ItemsPanelTemplateProperty依賴屬性時,其默認值就是StackPanel。另外值得一提的時:ListBox和ListView的默認ItemsPanel都是VirtualizingStackPanel,Menu類是WrapPanel,StatusBar類是DockPanel)。
而咱們知道,要想讓這個ItemsPanel模板起做用,ItemsControl的Template內還必須包含一個ItemsPresenter:
<Style TargetType="{x:Type ItemsControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Border> <ItemsPresenter /> Border> ControlTemplate> Setter.Value> Setter> Style>
這時一個ItemsControl的Template模板裏的ItemsPresenter在應用這個ItemsControl的ItemsPanel模板時,會將模板裏面的Panel類控件的TemplateParent設定爲這個ItemsControl,同時將其IsItemsHost屬性標識爲true。
ItemsControl還有一種用法是忽略ItemsPanel,直接在其Template內指定一個"ItemsPanel",以下面的代碼:
<Style TargetType="{x:Type ItemsControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Border> <StackPanel IsItemsHost="True"/> Border> ControlTemplate> Setter.Value> Setter> Style>
這時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。固然具體過程要複雜的多。
至此,本文算所有寫完了。最後再強行總結一下WPF的模板機制:
1.FrameworkTemplate是全部模板類的基類,FrameworkElement類有一個FrameworkTemplate類型的TemplateInternal屬性,FrameworkElement.ApplyTemplate()將使用這個屬性的模板對象來生成visual tree,並將這個visual tree賦值給本身的TemplateChild屬性,從而在兩個Visual類對象之間創建起parent-child relationship;
2.FrameworkElement的TemplateInternal屬性是虛屬性,FrameworkElement子類能夠經過覆寫這個屬性來自定義模板。只有四個類Control、ContentPresenter、ItemsPresenter、Page覆寫了這個屬性,這意味着只有這4個類及其子類控件才能應用自定義的模板,它們也是WPF模板機制的實現基礎;
3.FrameworkTemplate類有三個子類:ControlTemplate、ItemsPanelTemplate和DataTemplate。WPF中這些模板類定義的變量不少,它們的內部實現也不盡相同,不過萬變不離其宗,全部模板類最終都要把本身傳遞到FrameworkElement.TemplateInternal屬性上,才能被應用,生成的visual tree才能被加載到總體的visual tree中。FrameworkElement.ApplyTemplate()方法是FrameworkElement及其子類模板應用的總入口。