1. 程式人生 > >精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)

精通 WPF UI Virtualization (提升 OEA 框架中 TreeGrid 控件的性能)

alt ans .html 自定義 sp1 fin cnblogs nts tex

原文:精通 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> <ItemsPresenter
SnapsToDevicePixels="{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 知識點有那麽幾個:

  1. WPF 中的 VirtualizingStackPanel 只支持一層數據的 UIV。(這一點好像在 WPF3.5 SP1 後有所改善?)
  2. WPF3.5 SP1 以前的 TreeView 是不支持 UIV的。而之後的 TreeView 在默認情況下 UIV 處於關閉狀態,需要手動打開。
  3. 實現 UIV 需要一個對應的 ScollViewer。
  4. ScollViewer 中的 CanContentScroll 屬性為 True 時,子對象才能實現 UIV。 該屬性為 True 時,ScollViewer 在 Measure 時會把當前的 ViewPort 大小傳給 Content 元素。否則,它會把 Infinite 傳給 Content。 同時,由子元素(也就是 VirtualizingStackPanel)需要實現 IScollInfo 並返回 Scroll 相關信息,而 ScollViewer 則只是一個簡單的視窗;這樣,子元素就可以在內部實現 UIV,並告知其對應的 ScrollOwner(ScrollViewer) 相關的拖動信息。

所以,上面的 xaml 主要有兩個錯誤:

  1. ScrollViewer.CanContentScroll 應該設置為 True。
  2. 應該把 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 控件的性能)