深入理解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型別)模板機制。這些內容將留待下一篇文章分解。
謝謝閱讀!轉載請註明出處!