1. 程式人生 > 實用技巧 >WPF原始碼分析系列一:剖析WPF模板機制的內部實現(三)

WPF原始碼分析系列一:剖析WPF模板機制的內部實現(三)

(注:本文是《剖析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);
        }

        private void OnPanelChanged(object sender, EventArgs e)
        {
            // something has changed that affects the ItemsPresenter.
            // Re-measure.  This will recalculate everything from scratch.
            InvalidateMeasure();

            //
            // If we're under a ScrollViewer then its ScrollContentPresenter needs to
            // be updated to work with the new panel.
            //
            ScrollViewer parent = Parent as ScrollViewer;
            if (parent != null)
            {
                // If our logical parent is a ScrollViewer then the visual parent is a ScrollContentPresenter.
                ScrollContentPresenter scp = VisualTreeHelper.GetParent(this) as ScrollContentPresenter;
 
                if (scp != null)
                {
                    scp.HookupScrollingComponents();
                }
            }
        }

這些程式碼的意思簡而言之就是,當一個ItemsControl的ItemsPanel屬性改變時,會觸發其ItemContainerGenerator屬性的PanelChanged事件,而一個ItemsPresenter註冊了用自己的OnPanelChanged()方法註冊了這個事件。這個方法的一個工作是呼叫InvalidateMeasure()方法,將這個ItemsPresenter的measurement狀態標記為失效(Invalidated),從而進入佇列等待下一次佈局更新時重新measure,而我們前面提到過,FrameworkElement及其子類控制元件每一次measure時都會呼叫FrameworkElement.ApplyTemplate()方法。

問題是這個ItemsPresenter是從哪裡來的?是如何與ItemsControl聯絡在一起的?要回答這個問題就必須回到上面兩個方法的第一個方法UseGenerator()。這個方法一共被呼叫過兩次,其中一次是在ItemsPresenter.AttachToOwner()

另外,ItemsControl.ItemsPanel屬性也只有一處引用,也是在這個方法。事實上,這個方法是一個ItemsControl和其ItemsPresenter建立連線的關鍵地方。其程式碼如下:

//************ItemsPresenter.cs**************

// 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欄位將指向這個ItemsControl,而且方法
UseGenerator()的入參generator就是這個ItemsControl的ItemContainerGenerator。那麼一個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,並覆寫了TemplateInternalTemplateCache屬性。以下是相關程式碼:

//************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屬性的多型性。

至此,ItemsPanelTemplate型別的三個重要變數:ItemsControl.ItemsPanel、ItemsPresenter.Template和GroupStyle.Panel是如何被裝配到FrameworkElement.ApplyTemplate()這個模板應用的流水線上的也就清楚了。

下一篇文章開始我們將討論DataTemplate類。