眾所周知,在WPF框架中,Visual類是可以提供渲染(render)支援的最頂層的類,所有視覺化元素(包括UIElement、FrameworkElment、Control等)都直接或間接繼承自Visual類。一個WPF應用的使用者介面上的所有視覺化元素一起組成了一個視覺化樹(visual tree),任何一個顯示在使用者介面上的元素都在且必須在這個樹中。通常一個視覺化元素都是由眾多視覺化元素組合而成,一個控制元件的所有視覺化元素一起又組成了一個區域性的visual tree,當然這個區域性的visual tree也是整體visual tree的一部分。一個視覺化元素可能是由應用直接建立(要麼通過Xaml,要麼通過背後的程式碼),也可能是從模板間接生成。前者比較容易理解,這裡我們主要討論後者,即WPF的模板機制,方法是通過簡單分析WPF的原始碼。由於內容較多,為了便於閱讀,將分成一系列共5篇文章來敘述。本文是這一系列的第一篇,主要討論FrameworkTemplate類和FrameworkElement的模板應用框架。
一、從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)的基類,其重要性不言而喻。瀏覽其程式碼會發現一個引人注意的方法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()方法。由於這個方法的程式碼較多,這裡為了簡潔就不貼了。簡而言之,這個方法的流程有三個分支:1)如果一個FrameworkTemplate的_templateRoot欄位(FrameworkElementFactory型別)不為空,則呼叫其_templateRoot.InstantiateTree()方法來生成visual tree;2)否則,如果這個FrameworkTemplate的HasXamlNodeContent屬性為真,則呼叫其LoadContent()方法生成visual tree;3)如果二者均不滿足,則最終呼叫其BuildVisualTree()來生成visual tree。這些方法都比較複雜,它們的主要工作是例項化給定模板以生成visual tree。因為我們只關心模板框架和模板應用的流程,所以不妨忽略這些細節。
由於FrameworkTemplate.ApplyTemplateContent()不是虛方面,因此其子類無法覆寫。用程式碼工具我們可以看到,這個方法只在FrameworkElement.ApplyTemplate()裡被呼叫了一次,這意味著這個方法是WPF視覺化元素實現模板應用的唯一入口,其重要性無論如何強調都不為過,以後我們還會多次提到這個方法。其程式碼如下:
//***************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進行佈局的兩個重要步驟。這個方法的程式碼並不複雜,它先是呼叫虛方法OnPreApplyTemplate();然後如果TemplateInternal非空,則呼叫其ApplyTemplateContent()方法生成相應的visual tree,並呼叫虛方法OnApplyTemplate()(這個虛方法在開發自定義控制元件時經常需要重寫,此時visual tree已經生成並可以訪問了);最後呼叫虛方法OnPostApplyTemplate()。
注意上面程式碼有一個語句:
FrameworkTemplate template = TemplateInternal;
這說明FrameworkElement實際是根據其屬性TemplateInternal的值來生成visual tree的。那麼這個TemplateInternal又是從哪裡來的呢?事實上,這個屬性與另一個屬性TemplateCache是有密切關係的,二者都是FrameworkTemplate型別,它們的定義如下:
//***************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的子類可以通過覆寫它們來實現多型性,提供自定義的模板。另外,利用工具我們可以看到只有4個子類重寫了TemplateInternal屬性:Control、ContentPresenter、ItemsPresenter、Page,這意味著只有這4個類及其子類呼叫ApplyTemplate()才有意義。
現在問題是:FrameworkElement的子類具體是如何通過覆寫虛屬性TemplateInternal來自定義模板的?FrameworkTemplate的三個子類的變數有哪些?它們在這個過程中的角色又有何不同?
為了便於理解,下面我們將按照三個模板子類,分成四篇文章來討論(由於DataTemplate的內容較多,被分成了兩篇文章)。
(本文是系列文章《剖析WPF模板機制的內部實現》的第一篇,檢視下一篇點這裡)
(原創文章,歡迎批評指正,轉載請註明出處,謝謝!)