眾所周知,在WPF框架中,Visual類是可以提供渲染(render)支持的最頂層的類,所有可視化元素(包括UIElement、FrameworkElment、Control等)都直接或間接繼承自Visual類。一個WPF應用的用戶界面上的所有可視化元素一起組成了一個可視化樹(visual tree),任何一個顯示在用戶界面上的元素都在且必須在這個樹中。通常一個可視化元素都是由眾多可視化元素組合而成,一個控件的所有可視化元素一起又組成了一個局部的visual tree,當然這個局部的visual tree也是整體visual tree的一部分。一個可視化元素可能是由應用直接創建(要么通過Xaml,要么通過背后的代碼),也可能是從模板間接生成。前者比較容易理解,這里我們主要討論后者,即WPF的模板機制,方法是通過簡單分析WPF的源代碼。由於內容較多,為了便於閱讀,將分成一系列共5篇文章來敘述。本文是這一系列的第一篇,重點討論FrameworkTemplate類和FrameworkElement模板應用機制,這也是WPF模板機制的框架。
一、從FrameworkTemplate到visual tree
我們知道盡管WPF中模板眾多,但是它們的類型無外乎四個,這四個類的繼承關系如下圖所示:
可見開發中常用的三個模板類都以FrameworkTemplate為基類。問題是,除了繼承關系,這些模板類的子類與基類還有什么關系?三個子類之間有什么關系?這些模板類在WPF模板機制中的各自角色是什么?WPF究竟是如何從模板生成visual tree的?
要回答這些問題,最佳途徑是從分析模板基類FrameworkTemplate着手。
FrameworkTemplate是抽象類,其定義代碼比較多,為了簡明,這里就不貼完整代碼了,我們只看比較關鍵的地方。首先,注意到這個類的注釋只有一句話:A generic class that allow instantiation of a tree of Framework[Content]Elements,意思是這個類是允許實例化一個Framework元素樹(也即visual tree)的基類(generic class),其重要性不言而喻。瀏覽其代碼會發現一個值得注意的方法ApplyTemplateContent():
****************FrameworkTemplate******************
// // This method // Creates the VisualTree // internal bool ApplyTemplateContent( UncommonField<HybridDictionary[]> templateDataField, FrameworkElement container) { ValidateTemplatedParent(container); bool visualsCreated = StyleHelper.ApplyTemplateContent(templateDataField, container, _templateRoot, _lastChildIndex, ChildIndexFromChildName, this); return visualsCreated; }
這是刪除了打印調試信息后的代碼,簡單到只有三個語句。注釋表明FrameworkTemplate生成VisualTree用的就是這個方法。其中最重要的是第二句,它把具體應用模板內容的工作交給了輔助類StyleHelper.ApplyTemplateContent()方法。這個方法的注釋是:Instantiate the content of the template (either from FEFs or from Baml).This is done for every element to which this template is attached。其意思是說,每一個帶有模板的元素要實例化模板的內容(無論是來自FEF還是來自Baml),都必須調用這個方法。而查看對StyleHelper.ApplyTemplateContent()方法的引用,會發現它只被引用了一次。而這唯一一次引用就是在FrameworkTemplate.ApplyTemplateContent()方法里。這也表明這個方法是FrameworkTemplate生成visual tree的唯一入口。
由於StyleHelper.ApplyTemplateContent()方法的代碼較多,這里為了簡潔就不貼了。簡而言之,這個方法會視具體情況選擇合適的方法來實例化一個FrameworkTemplate,用其生成一個visual tree。生成的visual tree最終都會被傳遞到FrameworkElement.TemplateChild屬性上,而這個屬性的setter又會調用Visaul.AddVisualChild()方法。后者的主要目的建立兩個visual之間的父子關系(parent-child relationship),以方便以后進行布局(layout)。至此,一切准備就緒,生成的visual tree已經可視化了。
*****************FrameworkElement*******************
/// <summary> /// Gets or sets the template child of the FrameworkElement. /// </summary> virtual internal UIElement TemplateChild { get { return _templateChild; } set { if (value != _templateChild) { RemoveVisualChild(_templateChild); _templateChild = value; AddVisualChild(value); } } }
由於FrameworkTemplate.ApplyTemplateContent()不是虛方面,因此其子類無法覆寫。查看這個方法的引用我們可以看到,這個方法只在FrameworkElement.ApplyTemplate()里被調用了一次,這意味着FrameworkElement的這個方法是FrameworkElement及其子類實現模板應用的唯一入口。這個方法的重要性無論如何強調都不為過,以后我們還會多次提到這個方法。因此有必要貼一下其代碼:
//***************FrameworkElement********************
/// <summary> /// ApplyTemplate is called on every Measure /// </summary> /// <remarks> /// Used by subclassers as a notification to delay fault-in their Visuals /// Used by application authors ensure an Elements Visual tree is completely built /// </remarks> /// <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; UncommonField<HybridDictionary[]> dataField = StyleHelper.TemplateDataField; FrameworkTemplate template = TemplateInternal; // The Template may change in OnApplyTemplate so we'll retry in this case. // We dont want to get stuck in a loop doing this, so limit the number of // template changes before we bail out. int retryCount = 2; for (int i = 0; template != null && i < retryCount; i++) { // VisualTree application never clears existing trees. Trees // will be conditionally cleared on Template invalidation if (!HasTemplateGeneratedSubTree) { // Create a VisualTree using the given template visualsCreated = template.ApplyTemplateContent(dataField, this); if (visualsCreated) { // This VisualTree was created via a Template HasTemplateGeneratedSubTree = true; // 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(); } if (template != TemplateInternal) { template = TemplateInternal; continue; } } break; } OnPostApplyTemplate(); return visualsCreated; }
方法的注釋表明FrameworkElement在每次measure時都會調用這個方法,而我們知道measure和arrange是UIElement進行布局的兩個主要步驟。如果FrameworkElement元素在布局其HasTemplateGeneratedSubTree屬性為false,那么就將調用FrameworkTemplate.ApplyTemplateContent()重新應用模板,生成visual tree。
這個方法的代碼並不復雜,它先是調用虛方法OnPreApplyTemplate();然后如果HasTemplateGeneratedSubTree為false且TemplateInternal非空,則調用TemplateInternal的ApplyTemplateContent()方法生成相應的visual tree,並調用虛方法OnApplyTemplate()(這個虛方法在開發自定義控件時經常需要重寫,此時visual tree已經生成並可以訪問了);最后調用虛方法OnPostApplyTemplate()完成收尾工作。
從上面的分析可以看到,FrameworkElement能生成什么樣的visual tree,或者說生成的visual tree的結構,完全取決於其TemplateInternal。給這個屬性一個什么樣的模板,就會生成一個什么樣的visual tree。換句話說,FrameworkElement的visual tree的模板完全是由TemplateInternal唯一提供的。那么這個神奇的TemplateInternal屬性又是怎如何定義的呢?事實上,除了這個屬性FrameworkElement還定義了一個FrameworkTemplate類型的屬性TemplateCache。這兩個屬性的定義都很簡單,代碼如下:
//***************FrameworkElement********************
// Internal helper so the FrameworkElement could see the // ControlTemplate/DataTemplate set on the // Control/Page/PageFunction/ContentPresenter internal virtual FrameworkTemplate TemplateInternal { get { return null; } } // 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 {} }
可以看到二者的注釋幾乎都完全相同,也都是虛屬性,FrameworkElement的子類可以通過覆寫它們來實現多態性,提供自定義的模板。它們的自定義模板完全決定了它們的visual tree。事實上,利用工具我們可以看到只有4個FrameworkElement子類重寫了TemplateInternal屬性:Control、ContentPresenter、ItemsPresenter、Page,這意味着只有這4個類及其子類調用ApplyTemplate()才有意義。
現在問題是:FrameworkElement的子類具體是如何通過覆寫虛屬性TemplateInternal來自定義模板的呢?FrameworkTemplate的三個子類的變量有哪些?它們在這個過程中的角色又有何不同?
為了便於理解,下面我們將按照三個模板子類,分成四篇文章來討論(由於DataTemplate的內容較多,被分成了兩篇文章)。
(本文是系列文章《剖析WPF模板機制的內部實現》的第一篇,查看下一篇點這里)
(原創文章,歡迎批評指正,轉載請注明出處,謝謝!)