精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)
本篇博客主要說明如何使用 UI Virtualization(以下簡稱為 UIV) 來提升 OEA 框架中 TreeGrid 控件的性能,同時,給出了一些學習 UIV 的資源。
問題
最近對 OEA 的 TreeGrid 控件進行了比較大的改造,並使用新的控件來替換了系統中所有的 DataGrid 控件。新的 TreeGrid 控件實現了很多新的功能,(之後會寫一篇文章說明),但是最後遺留了一個問題:由於使用它替換了原來的 DataGrid,而 DataGrid 默認是支持 UI Virtualization 的,當有些界面的數據量比較大時,沒有支持 UIV 的TreeGrid 控件就顯得有些力不從心了。為了解決這個問題,這兩天看了許多文章並學習了 WPF 中 UIV 的知識,在最後終於解決了,待寫下此文予以記錄。
先來看看實現 UIV 前:
518 條數據,生成了 18130 個 Visuals。
其實,在解決完後看來,問題主要出在 TreeGrid 的 Template 上,直接貼上來給大家看看:
<ScrollViewer Style="{StaticResource GridTreeViewScroll}" Background="{TemplateBinding Background}" Focusable="false" CanContentScroll="false" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"> <Grid> <ItemsPresenterSnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> <TextBlock Opacity="0.5" TextWrapping="Wrap" FontSize="36" Text="沒有數據" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" FontFamily="STCaiyun" RenderTransformOrigin="0.5,0.5" Foreground="#80000000"> <TextBlock.Visibility> <MultiBinding> <MultiBinding.Converter> <oeaModuleWPF:ItemsControlNoDataConverter/> </MultiBinding.Converter> <Binding Path="Data" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type oea:GridTreeView}}"/> <Binding Path="Items.Count" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type oea:GridTreeView}}"/> </MultiBinding> </TextBlock.Visibility> <TextBlock.RenderTransform> <TransformGroup> <ScaleTransform ScaleX="1.5"/> <SkewTransform AngleX="-30"/> <RotateTransform Angle="-30"/> </TransformGroup> </TextBlock.RenderTransform> </TextBlock> </Grid> </ScrollViewer>
其中,為了實現在列表沒有數據時,顯示 “沒有數據” 四個字,使用了一個 Grid 包含了一個 ItemsPresenter 以及一個 TextBlock。這段代碼看上去沒有什麽問題,所以搞了很久都沒有把 UIV 調試出來,最終只有在網上耐心學習了很我 UIV 的相關知識。
解決方案
其實,相關的 UIV 知識點有那麽幾個:
- WPF 中的 VirtualizingStackPanel 只支持一層數據的 UIV。(這一點好像在 WPF3.5 SP1 後有所改善?)
- WPF3.5 SP1 以前的 TreeView 是不支持 UIV的。而之後的 TreeView 在默認情況下 UIV 處於關閉狀態,需要手動打開。
- 實現 UIV 需要一個對應的 ScollViewer。
- ScollViewer 中的 CanContentScroll 屬性為 True 時,子對象才能實現 UIV。 該屬性為 True 時,ScollViewer 在 Measure 時會把當前的 ViewPort 大小傳給 Content 元素。否則,它會把 Infinite 傳給 Content。 同時,由子元素(也就是 VirtualizingStackPanel)需要實現 IScollInfo 並返回 Scroll 相關信息,而 ScollViewer 則只是一個簡單的視窗;這樣,子元素就可以在內部實現 UIV,並告知其對應的 ScrollOwner(ScrollViewer) 相關的拖動信息。
所以,上面的 xaml 主要有兩個錯誤:
- ScrollViewer.CanContentScroll 應該設置為 True。
- 應該把 VirtualizingStackPanel 作為 ScrollViewer 的內容元素(Content)。
修改為以下 xaml 即可:
<Grid> <ScrollViewer Style="{StaticResource GridTreeViewScroll}" Background="{TemplateBinding Background}" Focusable="false" CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}" HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"> <VirtualizingStackPanel IsItemsHost="True" SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" /> </ScrollViewer> <TextBlock Opacity="0.5" TextWrapping="Wrap" FontSize="36" Text="沒有數據">……</TextBlock> </Grid>
同時,註意打開 TreeView 的 UIV 支持:
public class GridTreeView : TreeView { static GridTreeView() { VirtualizingStackPanel.IsVirtualizingProperty.OverrideMetadata(typeof(GridTreeView), new FrameworkPropertyMetadata(true)); }
來看看優化後的結果:
Visuals 的數量由 1W8 降到了 3000,當行數更多時,也就保持初始生成 3000 個左右。拖動起來也明顯地感覺到流暢了許多。
大功告成!
相關資源
一篇通俗易懂的 UIV 概念文章:《UI Virtualization》,其中講到了 WPF 及 SilverLight 中的 UIV。(它還有後續的文章:《Data virtualization》,也很不錯)。
之前系統中用到的 DataGrid 控件,一旦數據被分組之後,性能異常低下。原因其實也和 UIV 有關:
目前 WPF 中的控件在 Group 分組後是不支持 UI Virtualization 的,原因是當 ScrollViewer.CanContentScroll 設置為 true 時,模式由 Scroll By Pixel 變為 Scroll by Item。而分組後的控件中每一個組 GroupItem 其實就是一個 Item,這時,如果繼續使用 Scroll by Item 模式,將會得到非常差的用戶體驗,所以 MS 決定不支持分組後的 UIV,ListBox 控件的默認模板中有一個 Trigger 當 IsGrouping 為 True 時,設置 ConContentScroll 為 False。相關的內容參見:《UI Virtualization》。其它與分組相關的 UIV 文章如下:
《WPF DataGrid Virtualization with Grouping》、《MSDN Sample Code:Grouping and Virtualization》、《Problem: ListView Virtualization》
《Virtualizing TreeViewItem》:其中的最佳答案說到幾個知識點:VirtualizingStackPanel 需要和 ScrollViewer 進行交互,同時,它只支持一層的 Virtualization。可以考慮變通地使用 ListBox/ListView 來實現假的 TreeView,這樣就可以實現整個列表的虛擬化。
《WPF - Virtualizing an ItemsControl》:文中指出,ItemsControl 默認不支持 UI Virtualization,原因是它的模板中沒有一個 ScrollViewer。
《Are there any tricks that will help me improve TreeView’s performance》:這個系列的文章一共3篇:《Part I》、《Part II》、《Part III》,最後一篇說明了在如何使用 ListBox 模擬一個 TreeView,這樣,由於 ListBox 本身支持 UIVirtualization,所以最後的 “TreeView” 也就支持了 UI Virtualization。類似的控件已經有人傳到了 CodeProject 上:《Virtualizing Tree View (VTreeView)》,其中還正好談到了上面的這系列文章,非常湊巧的是,它還談到了 CodeProject上被我們系統選擇來實現 TreeGrid 控件的資源:《A Versatile TreeView for WPF》。
更高級的自定義 UI Virtualization,可以先參考以下幾篇文章,很不錯:《Virtualizing WrapPanel》、《Implementing a virtualized panel in WPF (Avalon)》、《IScrollInfo in Avalon part I》、《IScrollInfo in Avalon part II》、《IScrollInfo in Avalon part III》。
MS 自己的相關資源:
《MSDN Control Performance》、《How to: Find a TreeViewItem in a TreeView》(如何在 UIV 的情況下找到控件)、《Changing selection in a virtualized TreeView》
精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)