🗂

AvaloniaUIのTreeViewが遅い問題の解決法

に公開

AvaloniaUIのTreeViewは項目数が多くなると顕著に重くなる。それを解消しようともがいた経緯。

使用環境

Windows 11 : Home 22H2 + Visual Studio Community 2022(64bit) Version 17.12.3
Avalonia 11.2.3

TreeViewは項目数が増えると遅くなる

下記にあるようにTreeViewは項目数が多くなると顕著に動作が重くなる。

項目数が多い時にはTreeGridViewをつかうことが推奨されている。

https://docs.avaloniaui.net/docs/guides/development-guides/improving-performance

When you need to display a large amount of data in a DataGrid or a TreeView with many nodes, it is recommended to use the TreeDataGrid control. TreeDataGrid is built from scratch and provides better performance than the normal DataGrid. It supports virtualization and is particularly useful if you need a virtualized tree, as it has hierarchical data templates.

TreeGridViewを使ってTreeControlを実装してみた

以下でおこなったように、winformsのTreeControlのようなものをTreeGridViewを使って実装してみた
https://zenn.dev/ajkfds/articles/3fdeb4e93d00e9

TreeGridViewを使うにはStyleの設定が必要

App.xaml
<Application xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             x:Class="AjkAvaloniaLibs.App"
             RequestedThemeVariant="Default">
             <!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->

    <Application.Styles>
        <FluentTheme />
		<StyleInclude
			Source="avares://Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml"/>

	</Application.Styles>
</Application>

忘れると、エラーは出ないのに描画されない。

ViewModelはViewと必ず別にしないといけない。

Viewのコードビハインドに特に書くことがない場合に、ViewのコードビハインドにViewModelを書いて、Viewの中でDataContext = thisとするような
手抜きをすると、TreeGridViewは動作しなくなる。これをやると、エラーは出ないのに描画されない。

Styleの設定 : 行の高さを変える

Bindingがとても難解。
複雑なVisualTreeでいろんなTempleteが定義をされており、行の高さを変えるのがなかなか難しい。
Templete内で固定されている高さ要素があり、TreeGridViewのソースコードを見ながらいろいろ上書きする必要がありそう。

<TreeDataGrid.Styles>

    <Style Selector="TreeDataGridRow">
        <Setter Property="MinHeight" Value="0"/>
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="Height" Value="{Binding RowHeight, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
        <Setter Property="Margin" Value="0"/>
    </Style>

    
    <Style Selector="TextBlock">
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="MinHeight" Value="0"/>
    </Style>

    <Style Selector="Border#CellBorder > DockPanel > Border">
        <Setter Property="Height" Value="{Binding FontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
        <Setter Property="Width" Value="{Binding FontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
    </Style>

    <Style Selector="Border#CellBorder > DockPanel > Border > ToggleButton">
        <Setter Property="Height" Value="{Binding FontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
        <Setter Property="Width" Value="{Binding FontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" />
    </Style>

    <Style Selector="TreeDataGridExpanderCell">
        <Setter Property="MinHeight" Value="0"/>
    </Style>
    
    <!-- fix TreeDataGridExpanderCell template to eliminate CellBorder.DockPanel.Border.With/Height -->
    <Style Selector="TreeDataGridExpanderCell">
        <Setter Property="Template">
            <ControlTemplate>
                <Border x:Name="CellBorder"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        CornerRadius="{TemplateBinding CornerRadius}"
                        Padding="{TemplateBinding Indent, Converter={x:Static ctrl:IndentConverter.Instance}}"
                        >
                    <DockPanel>
                        <Border x:Name="ShiftBox"
                                DockPanel.Dock="Left"
                                Margin="4 0"
                                Width="{Binding FontSize, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=UserControl}}" Height="12">
                            <ToggleButton Theme="{StaticResource TreeDataGridExpandCollapseChevron}"
                                          Focusable="False"
                                          IsChecked="{TemplateBinding IsExpanded, Mode=TwoWay}"
                                          IsVisible="{TemplateBinding ShowExpander}" />
                        </Border>
                        <Decorator Name="PART_Content" />
                    </DockPanel>
                </Border>
            </ControlTemplate>
        </Setter>
    </Style>

    <!-- expand/collapse icon shape 変更 -->
    <Style Selector="ToggleButton:checked /template/ Path#ChevronPath">
        <Setter Property="Data" Value="{StaticResource TreeViewItemExpandedChevronPathData}" />
    </Style>
    <Style Selector="ToggleButton:unchecked /template/ Path#ChevronPath">
        <Setter Property="Data" Value="{StaticResource TreeViewItemCollapsedChevronPathData}" />
    </Style>

</TreeDataGrid.Styles>

</TreeDataGrid>

でもできなかった

なんとなく動いてみえたものの、Itemのプロパティを変更したときにViewが変更されない問題が解消できなかった。
https://github.com/AvaloniaUI/Avalonia.Controls.TreeDataGrid/issues/128

ListBoxを使って自前で実装する

しょうがないのでListBoxを使って自分で実装してみた。
NodesのUpdateをControlに伝達して、Viewをそのたびに更新する方式。

結果的には単純なコントロールの組み合わせで自分で組んでしまったほうが手っ取り早い感じがした。
https://github.com/ajkfds/AjkAvaloniaLibs/blob/main/AjkAvaloniaLibs/Controls/TreeControl.axaml
https://github.com/ajkfds/AjkAvaloniaLibs/blob/main/AjkAvaloniaLibs/Controls/TreeControl.axaml.cs
https://github.com/ajkfds/AjkAvaloniaLibs/blob/main/AjkAvaloniaLibs/Controls/TreeNode.cs

Discussion