1. 程式人生 > 實用技巧 >深入理解WPF的模板機制

深入理解WPF的模板機制

在WPF中,Control類的Template屬性(ControlTemplate型別)定義著一個Control的外觀。每一個Control都有一個預設的Template,更換其Template可以徹底改變其外觀,這是WPF提供的靈活自定義控制元件外觀機制的實現基礎。下面我們通過簡單分析WPF原始碼探尋模板機制的內部實現(為了行文簡潔和便於理解,儘量只顯示必要的原始碼和註釋):

一、Control是如何按照自己的Template建立自己的外觀(Visual Tree)的:

1.Control和ControlTemplate的父類分別是FrameworkElement和FrameworkTemplate。雖然FrameworkElement沒有暴露Template屬性,不過其內部維持著一個TemplateCache屬性(FrameworkTemplate型別);

        //**********Framework.cs**********

        // 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 {} }

2.可見這個屬性是virtual型別,Control類正是override了Framework的TemplateCache,從而將其與Control的Template屬性連線在了一起;

//***********Control.cs***********

        // Internal Helper so the FrameworkElement could see the template cache
        internal override FrameworkTemplate TemplateCache
        {
            
get { return _templateCache; } set { _templateCache = (ControlTemplate) value; } } /// <summary> /// Template Property /// </summary> public ControlTemplate Template { get { return _templateCache; } set { SetValue(TemplateProperty, value); } }

3.Framework包含一個public的ApplyTemplate()方法。值得一提的是,這個方法的重要性無論如何強調都不為過:這裡是全部魔法發生的地方。顧名思義,呼叫這個方法,將“應用”模板,即根據模板定義的“藍圖”生成一個對應的Visual Tree(這裡也是在開發自定義控制元件時經常需要override的virtual方法OnApplyTemplate()被呼叫的地方)。

//***********Framework.cs***********
        
        /// <returns>Whether Visuals were added to the tree</returns>
        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;


            var dataField = StyleHelper.TemplateDataField;
            FrameworkTemplate template = TemplateInternal;

            // Create a VisualTree using the given template
            visualsCreated = template.ApplyTemplateContent(dataField, this);
            if (visualsCreated)
            {
                // 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();
            }

            OnPostApplyTemplate();

            return visualsCreated;
        }

4.可見上面的方法又呼叫了FrameworkElement的ApplyTemplateContent()方法,而這個方法又呼叫了輔助類StyleHelper的ApplyTemplateContent(),而這個方法又呼叫了FrameworkFactory的InstantiateTree()方法,這個方法是Visual Tree被最終生成的地方。整個呼叫過程簡化如下:

FrameworkElement.ApplyTemplate() -> FrameworkTemplate.ApplyTemplateContent() -> StyleHelper.ApplyTemplateContent() -> FrameworkFactory.InstantiateTree()

二、上面我們簡單探討了從Template到Visual Tree的魔法的實現過程。不過我們知道WPF有4種模板型別,除了Control的Template模板(ControlTemplate型別)和Framework的TemplateCache模板(FrameworkTemplate型別),還有ContentControl(ContentPresenter)的ContentTemplate和ItemsControl的ItemTemplate(DataTemplate型別),以及ItemsControl的ItemsPanel(ItemsPanelTemplate型別),那麼這些型別之間的有什麼關係呢?ContentControl的Template和ContentTemplate又有什麼區別和聯絡呢?都有ContentTemplate屬性,ContentControl和ContentPresenter又有什麼區別?DataTemplate又是如何從模板到Visual Tree的呢?

1.WPF四種模板型別的關係可以用下面的類繼承圖來概括:

從上圖可以看出,常用的三種模板都有一個共同的父類FrameworkTemplate。這個模板型別雖然是public屬性,但是我們在開發時基本不會直接用到。前面我們提到,Control類override了其父類Framework的TemplateCache屬性,從而將其Template和Framework的TemplateCache聯絡在了一起。Framework在呼叫ApplyTemplate()建立Visual Tree時所引用的模板就是子類的Template。

2.要回答ContentControl的Template和ContentTemplate的區別和聯絡,以及ContentControl和ContentPresenter的區別,我們需要搞清楚ContentControl的模板應用機制。

首先,ContentControl的Template繼承自Control,這意味著像其他Control一樣,我們可以通過自定義Style來更換這個Template,進而徹底改變其外觀。不過,與一般Control的Template不同是,ContentControl的Template裡面必須包含一個ContentPresenter,否則其Content將無法正常顯示。要理解這點我們必須先理解ContentPresenter類的模板應用機制:

ContentPresenter繼承自Framework,它首先定義了一個ContentTemplate屬性(DataTemplate型別),其原始碼如下:

//************ContentPresenter.cs**************

        /// <summary>
        ///     The DependencyProperty for the ContentTemplate property.
        ///     Flags:              None
        ///     Default Value:      null
        /// </summary>
        [CommonDependencyProperty]
        public static readonly DependencyProperty ContentTemplateProperty =
                ContentControl.ContentTemplateProperty.AddOwner(
                        typeof(ContentPresenter),
                        new FrameworkPropertyMetadata(
                                (DataTemplate)null,
                                FrameworkPropertyMetadataOptions.AffectsMeasure,
                                new PropertyChangedCallback(OnContentTemplateChanged)));

        /// <summary>
        ///     ContentTemplate is the template used to display the content of the control.
        /// </summary>
        public DataTemplate ContentTemplate
        {
            get { return (DataTemplate) GetValue(ContentControl.ContentTemplateProperty); }
            set { SetValue(ContentControl.ContentTemplateProperty, value); }
        }

        /// <summary>
        ///     Called when ContentTemplateProperty is invalidated on "d."
        /// </summary>
        private static void OnContentTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ContentPresenter ctrl = (ContentPresenter)d;
            ctrl._templateIsCurrent = false;
            ctrl.OnContentTemplateChanged((DataTemplate) e.OldValue, (DataTemplate) e.NewValue);
        }

        /// <summary>
        ///     This method is invoked when the ContentTemplate property changes.
        /// </summary>
        /// <param name="oldContentTemplate">The old value of the ContentTemplate property.</param>
        /// <param name="newContentTemplate">The new value of the ContentTemplate property.</param>
        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;
        }

從程式碼我們可以看到當ContentPresenter的ContentTemplate屬性被改變時,其Template屬性將被清空。這個屬性的定義如下:

//***********ContentPresenter.cs**************
        /// <summary>
        /// TemplateProperty
        /// </summary>
        internal static readonly DependencyProperty TemplateProperty =
                DependencyProperty.Register(
                        "Template",
                        typeof(DataTemplate),
                        typeof(ContentPresenter),
                        new FrameworkPropertyMetadata(
                                (DataTemplate) null,  // default value
                                FrameworkPropertyMetadataOptions.AffectsMeasure,
                                new PropertyChangedCallback(OnTemplateChanged)));


        /// <summary>
        /// Template Property
        /// </summary>
        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);
        }

        // Property invalidation callback invoked when TemplateProperty is invalidated
        private static void OnTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ContentPresenter c = (ContentPresenter) d;
            StyleHelper.UpdateTemplateCache(c, (FrameworkTemplate) e.OldValue, (FrameworkTemplate) e.NewValue, TemplateProperty);
        }

可以看到,當ContentPresenter的Template屬性改變時,會呼叫輔助類StyleHelper的UpdateTemplateCache()方法。這個方法會用當前的Template的值更新其父類的TemplateCache。這意味著當ContentPresenter的ContentTemplate屬性改變時,其Template進而其TemplateCache都將被重置為null。那麼ContentPresenter又是如何確保其父類Framework在呼叫ApplyTemplate()生成Visual Tree時其TemplateCache不為null的呢?其祕密在於ContentPresenter類override了OnPreApplyTemplate(),並在這個方法裡,呼叫了EnsureTemplate(),而後者接著又呼叫了ChooseTemplate()來根據一定的優先順序來選擇一個合適的DataTemplate,並用這個模板更新其Template屬性。Template被賦值後,會再次呼叫StyleHelper的UpdateTemplateCache()方法,並用新的Template(確定是非空的)來更新TemplateCache(Template的型別是DataTemplate,TemplateCache的型別是FrameworkTemplate,後者是前者的積累,當然可以賦值),從而保證Framework在呼叫ApplyTemplate()可以找到一個非空的FrameworkTemplate。

這裡有必要貼一下ChooseTemplate()方法的原始碼,以一探ContentPresenter選擇模板的優先順序:

//***********ContentPresenter.cs**************

        /// <summary>
        /// Return the template to use.  This may depend on the Content, or
        /// other properties.
        /// </summary>
        /// <remarks>
        /// 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.
        /// </remarks>
        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;
        }

可見,ContentPresenter在選擇Template(NOTE:這裡的Template是DataTemplate型別,與Control類的屬於ControlTemplate的Template不同,雖然二者父類都是FrameworkTemplate)時,會優先ContentTemplate,如果為空,則會嘗試ContentTemplateSelector,再次是通過遍歷資源樹根據DataType來定位合適的模板,如果都找不到,則使用WPF預設的模板。。。這裡不一一贅述。

到此,ContentControl的Template和ContentTemplate的區別和聯絡,以及ContentControl和ContentPresenter的區別也就一目瞭然了。

總結:本文簡單分析了WPF的模板應用機制,以及ContentControl和ContentPresenter的特殊模板應用機制。為了簡潔起見,本文暫未涉及ItemsControl的ItemTemplate(DataTemplate型別),以及ItemsControl的ItemsPanel(ItemsPanelTemplate型別)模板機制。這些內容將留待下一篇文章分解。

謝謝閱讀!轉載請註明出處!