為什么要重構
上兩個月主要做了一件事情,那就是把 OEA 框架中的 TreeGrid 控件,從結構上重新設計,並大量重構現有代碼。而花較大精力做這件事的原因,主要是因為:
- 業務中需要支持一系列新功能:整行編輯、上下箭頭鍵進行導航、合計行、鎖定列 等。
- 控件顯示性能較差,需要支持列虛擬化。
- 和 OEA 元數據系統耦合,希望獨立為單獨的控件程序集,提高復用性。
- 不支持 xaml 聲明的格式。原控件直接在后台用 OEA 代碼生成,本質上作為一個 WinForm 控件來用。
- 整個 TreeGrid 控件混合了三個控件代碼而成,包括:GridView、TreeView 以及自身的一些代碼,內容復雜,維護較難。
- OEA 的界面層十分依賴當前的 TreeGrid 控件的各項功能,特別是樹型實體的展現。但是,在 WPF 環境下,一直沒有找到比較好用的 TreeGrid。而我們的 TreeGrid,由於之前做得一直不徹底,代碼比較亂,經常出現 BUG,修改起來也非常費時。(我記得,之前開發的項目,花了太多時間在修正這個半成品控件的問題上了。還是 B/S 好啊,ExtJS 中就有很強大的 TreeGrid,十分省事。)
- 隨着對 WPF 技術了解得更深入,希望做一個完全獨立的 WPF 控件。(用了那么久 WPF,想留下點東西。:))
TreeGrid 重構設計
先看下歷史代碼結構:
圖 TreeGrid 歷史代碼結構
可以看出,主要包含三大塊:GridTreeView、ObjectTreeView、TreeGrid。當初為了實現樹型表格控件,所以我們在網絡上搜索了大量文章,以下兩篇是當時覺得最有用的:《CodeProject A Versatile TreeView for WPF_ Free source code and programming help》及《GridTreeView: Show Hierarchy Data with Details in Columns》。我們的前兩大塊,GridTreeView 及 ObjectTreeView 中的一些代碼分別來自這兩篇文章。然后最終由 TreeGird.cs 整合起來。
雖然這只是一個簡單的半成品,但是已經達到了讓界面上顯示樹型表格、並同時支持 OEA 中的 ListObjectView 控制器控制的兩個目的。但是,隨着框架的應用場景越來越多、使用越來越頻繁,它暴露出來的問題也就更多了。許多新的功能也不能支持,這個在前面已經列舉了許多。
另外,在使用 TreeGrid 時,其實開發人員還是希望同時擁有 樹 及 表格 的兩套 API。而老版本的表格卻只有 樹 節點操作的 API。而我們的表格 API,也應該象 WPF 原生的 System.Windows.Controls.DataGrid 控件接口類似。例如:表格由行組成、行由格子組成、可以通過數據找到對應的行、再通過行找到對應的格子等。這里,我分析了一下 DataGrid 中,認為一些比較重要的 API:
圖 DataGrid 重點API
九、十月私下的時間,都在思考、設計、編碼這玩意兒。經過N多天的努力……目前已經把所有代碼完成。TreeGrid 不再依賴 TreeView、GridView,而是直接從 ItemsControl 上繼承下來,自定義邏輯樹、可視樹結構,自定義繪制過程。代碼有點多,看下最終的效果:
圖 TreeGrid 現在的代碼
其相應的可視樹結構如下:
圖 OEA TreeGrid 可視樹結構圖
具體的設計,可以看之前寫的一篇文章:《OEA 中 WPF 樹型表格虛擬化設計方案》。
具體的效果其實還不錯,這是最近用 OEA 框架編寫的《個人計划管理工具》,已經可以通過樣式、模板來定制表格中的各種顯示了:
圖 基於 OEA 的個人計划管理工具中的表格示例圖
自定義控件相關知識
以下總結一下,本次控件設計中,覺得比較重要的幾個知識點:
- 控件邏輯與布局、渲染的分離。
在 WPF 中,界面最終的渲染效果,是由可視樹決定的。而每個可視樹元素的測量、布局等行為,則是依賴於元素本身的數據,通過元素本身的算法決定。
元素的邏輯行為與渲染是分離的:
在元素發生諸如點擊、拖動、選擇等邏輯行為時,其實只變更了它內部的狀態數據。同時,這些行為也可以調用 InvalidateMeasure 來標記該元素的狀態為需要重新測量。而查看該方法源碼,可以看到本質上也是修改元素的內部狀態屬性 MeasureDirty。
當界面線程執行完邏輯處理后,會調用布局系統進行布局。布局系統會檢測之前所有標記為需要重新測量的元素,並分別調用它們的 Measure 方法。然后,再按類似的邏輯來調用 Arrange 和 Render。
界面線程會在需要時不斷地調用 Measure,我們可以把自定義控件中很多重要的邏輯都可以在 MeasureOverride 中實現。例如,界面虛擬化代碼就是在 Measure 過程中編寫,先添加必要的可視樹元素,然后再對這些新生成的元素進行測量。通過添加一些 bool 類型的防止重入的字段,Measure 中可以做所有邏輯操作之后、渲染之前的控件構造、刷新、替換、狀態變更,並對最終確定的可視樹子元素進行測量。如:
if(this._needBuildVisualTree){
this._needBuildVisualTree = false;
this.BuildVisualTree();
}
理解以上過程,將有助於更好地進行自定義控件的設計。
- 元素與元素之間應該是松耦合的。
在查看 WPF 源碼時,可以經常看到一些代碼,在通過可視樹關系查找指定類型的元素后,再要對元素的可空性進行判斷。而經常做這些可空性檢測的原因是,WPF 控件的設計要求,各控件互相之間沒有必然的聯系。控件的設計者不會知道該控件會被上層開發人員把它放在哪個控件里。例如,ListBoxItem 並不一定要放在 ListBox 中才能顯示。所以,在開發自定義控件時,盡量不要把控件的可視樹關系要求得過於嚴格。當沒有指定的可視樹關系時,也不應該拋出異常。而是應該檢測,如果在有指定的元素的情況下,才表現出具體的行為,否則將沒有行為。
- 關於 OnApplyTemplate 與 Measure 的關系。
ApplyTemplate 是應用模板的意思,所以我們一般在 OnApplyTemplate 中查找應用模板后的指定的可視元素。那么,可以寫在別的地方嗎?
系統本身對 ApplyTemplate 方法的調用,其實是放在 Measure 過程中的。查看源碼,發現在 FrameworkElement.MeasureCore 方法實現中,第一行就是調用 ApplyTemplate。也就是說,如果還沒有應用模板的元素,會在第一次測量之初,會應用它對應的模板。而 ApplyTemplate 方法內部則會通過一個 bool 類型的狀態值來檢測是否已經應用過模板,以防止重入。
OnApplyTemplate 只會在 ApplyTemplate 方法第一次執行時被調用。我們經常會重寫控件的這個方法,在其中查找指定的可視樹元素。其本質,與在 MeasureOverride 方法中以防止重入的方式來編寫這些代碼是一致的。