AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.27k stars 2.19k forks source link

TreeView is very slow with many loaded objects #6580

Open SCLDGit opened 3 years ago

SCLDGit commented 3 years ago

Describe the bug With many objects loaded into the tree, scrolling and general application responsiveness are negatively affected

To Reproduce Steps to reproduce the behavior:

Load many objects with hierarchy into a TreeView. Expand all expandable nodes. Testing here with 5000 entries.

Expected behavior TreeView should be able to handle many entries (possibly via virtualization?)

Desktop (please complete the following information):

Tested on Windows 10 and RHEL 8

Additional context Tree objects look like this:

    public interface ITreeItem
    {
        public int          DatabaseId { get; set; }
        public bool         IsExpanded { get; set; }
        public bool         IsChecked  { get; set; }
        public string       Name       { get; set; }
        public List<string> Tags       { get; set; }
    }
    public class TreeEndpoint : ReactiveObject, ITreeItem
    {
        public            int          DatabaseId { get; set; }
        [Reactive] public bool         IsExpanded { get; set; }
        [Reactive] public bool         IsChecked  { get; set; }
        [Reactive] public string       Name       { get; set; }
        public            List<string> Tags       { get; set; }
    }
    public class TreeGroup : ReactiveObject, ITreeItem
    {
        public            int          DatabaseId { get; set; }
        [Reactive] public bool         IsExpanded { get; set; }
        [Reactive] public bool         IsChecked  { get; set; }
        [Reactive] public string       Name       { get; set; }
        public            List<string> Tags       { get; set; }

        [Reactive] public ObservableCollection<ITreeItem> Children { get; set; }
    }

TreeView bindings look like this:

                <TreeView Items="{Binding Items}"
                          SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
                          ClipToBounds="True">
                    <TreeView.Styles>
                        <Style Selector="TreeViewItem">
                            <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
                        </Style>
                    </TreeView.Styles>
                    <TreeView.DataTemplates>
                        <TreeDataTemplate DataType="treeDataStructures:TreeGroup"
                                          ItemsSource="{Binding Children}">
                            <VirtualizingStackPanel Classes="treeListItem"
                                        Orientation="Horizontal"
                                        ClipToBounds="True">
                                <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}">
                                    <!-- <Interaction.Behaviors> -->
                                    <!--     <ButtonClickEventTriggerBehavior> -->
                                    <!--         <InvokeCommandAction Command="{Binding $parent[UserControl].DataContext.SetCheckedState}" -->
                                    <!--                              CommandParameter="{Binding}"/> -->
                                    <!--     </ButtonClickEventTriggerBehavior> -->
                                    <!-- </Interaction.Behaviors> -->
                                </CheckBox>
                                <TextBlock Foreground="Red" 
                                           Text="{Binding Name}" />
                            </VirtualizingStackPanel>
                        </TreeDataTemplate>
                        <DataTemplate DataType="treeDataStructures:TreeEndpoint">
                            <VirtualizingStackPanel Classes="treeListItem"
                                                    Orientation="Horizontal"
                                                    ClipToBounds="True">
                                <CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}"/>
                                <TextBlock Text="{Binding Name}" />
                            </VirtualizingStackPanel>
                        </DataTemplate>
                    </TreeView.DataTemplates>
                </TreeView>
kekekeks commented 3 years ago

The built-in treeview is not virtualized. There is an example of a manual treeview implementation on top of ItemsRepeater here https://github.com/kekekeks/example-avalonia-huge-tree

SCLDGit commented 3 years ago

Are there any plans to add virtualization to the TreeView? I'll check out your example in the meantime.

grokys commented 3 years ago

Probably not - virtualizing lists with variable height items is fraught with problems, and TreeView is an extreme example of this.

I am considering adding a TreeList control at some point though, which would work similarly to @kekekeks's example. No promises on timeline though.

SCLDGit commented 3 years ago

Any updates over time would be much appreciated. As is, without some additional guidance on potential workarounds, we're more or less dead in the water in terms of migrating our app to Avalonia. The tree is a big part of our workflow and our customers routinely have 5-10k root level items plus many more in the hierarchy below the root as part of their workflows.

robloo commented 3 years ago

5-10k root level items plus many more in the hierarchy below the root as part of their workflows.

It's almost never necessary to load that many items in a TreeView at once. I suggest you implement your own form of "virtualizing" which is basically do not add children of collapsed nodes to the TreeView. When a node is expanded, dynamically load it's chiildren.

That is what file explorers and other similar apps do where it is impossible for performance reasons to load the entire file system at once in the TreeView.

SCLDGit commented 3 years ago

That's a big ask of users coming from WPF where tree virtualization is built into the control (and it really screws with MVVM). Loading children only at expansion is not an option in our case for various technical reasons to do with our customers' various workflows, and regardless doesn't address the situation where customers have large numbers of items that aren't nested at all.

robloo commented 3 years ago

@SCLDGit Can you provide examples? I have never seen a case where so many items need to be loaded at once in a TreeView. Perhaps 3D/graphics software only? The TreeView design itself is intentionally designed to expand/collapse large sets with hierarchy so not all is visible at the same time. DataGrid is when everything needs to always be visible (you can even show hierarchy and expand/collapse groups with a few tricks).

I understand it isn't MVVM friendly at first, you will have to create a subset view model of your huge view model (it can be done). Many optimizations aren't MVVM friendly. In large production apps it's common not to fully follow MVVM in the theoretical sense for exactly these reasons. Performance is more important than beautiful MVVM code. MVVM actually holds you back in some cases with it's change propagation and binding requirements (Avalonia less so than others).

That said, the WinUI TreeView has never been stable (and isn't useful for production) and I've never seen anything but issues in the edge cases with ItemsRepeater. It might be better to re-base Avalonia on the WPF source code now for TreeView.

SCLDGit commented 3 years ago

Our users use the tree as a visual representation of their network infrastructure, they can group (or not) into hierarchies as they please. Most of our users manage 1000+ endpoints with our software, some as many as 20k, and the tree operates as a multi-functional center point for all endpoint interaction (e.g. checking checkboxes at group or individual levels to add items to a task, selecting groups and/or individual nodes for generating reports, etc.). The ability to (potentially) view the entire tree at once (with scrolling, of course) is vital to the operation of the system.

image

grokys commented 3 years ago

@SCLDGit is this for a commercial application? Would you be willing to pay for a commercial control for this? If so we might have a solution.

SCLDGit commented 3 years ago

@grokys

@SCLDGit is this for a commercial application? Would you be willing to pay for a commercial control for this? If so we might have a solution.

Yes, this is for a commercial application. We currently have DevExpress controls licensed. For the right price, we'd certainly be willing to pay for Avalonia controls that could get us transitioned to a cross-platform application in the near future.

grokys commented 2 years ago

@SCLDGit if you'd like to drop us an email at team@avaloniaui.net we can have a chat.

KvanTTT commented 2 years ago

@SCLDGit Can you provide examples? I have never seen a case where so many items need to be loaded at once in a TreeView. Perhaps 3D/graphics software only?

Something like AST or parse tree exploring. For instance, https://astexplorer.net/ but offline.

markusalbrecht-procam commented 9 months ago

We are encountering the same issue in our CAD software as we load thousands of objects and the treeitems have several SVG icons it lacks heavily:

image

Any solution planned for this ?

maxkatz6 commented 9 months ago

@markusalbrecht-procam please try TreeDataGrid

markusalbrecht-procam commented 9 months ago

@maxkatz6 we will use this as the last option as we already rewrote from listitems to treeview - the result is now better but not optimal as we use a lot svgs inside the tree for all items that seems to be the problem. Without images it works fine.

timunie commented 9 months ago

@markusalbrecht-procam you can try lazy loading also

markusalbrecht-procam commented 9 months ago

@timunie In fact if the treeviewitems are not expanded the images are not loaded so that works from scratch no lazy loading needed - performance is good. The problem is when Nodes are already expanded (as we implemented the IsExpanded state lately) and then reload the tree - what happens when we switch between different documents - then it also needs to load all the images of the expanded.

timunie commented 9 months ago

🤔 For images I use in several places I use some kind of ImageCache where I can reuse an existing one. But that only solves the I/O operation needed, still you have a lot of items rendered. In my App I also have a tree-view like view with > 1000 items. I ended up using a ListBox with a MarginConverter and using DynamicData to filter out collaped items. Cannot share it here for reasons, but the idea should be clear enogh to implement it on your side if you hit performance issues.

markusalbrecht-procam commented 9 months ago

@timunie Yes this is definitely something we will try to cache the images as the current SvgImage integration loads all indiviudal images even those are mainly the same, like e.g.: Lock or Eye icons. So in reality we would only need to cache about 20-30 images and then use those in the nodes/subnodes/.... As you see below, those are many but most of them similar so no need to load them a hundred times.

image

As I mentioned before we needed to switch from ListBox to TreeView as ListBox was reacting really abd with the images and in case many layers where expanded it always lead to memory overflow on scrolling. I am confident that we will get this working with treeview. Will keep you updated ;).

StefanKoell commented 9 months ago

Just to chime in as I am facing a similar issue. I'm using the TreeDataGrid with thousands of nodes and was using SVG images as well. What I noticed was some lag in Debug mode on my devbox which went away when no debugger was attached in release mode but on older/slower hardware the lag was still quite noticeable. Removing the image also removed the lag and everything was buttery smooth. So the lag is definitely related to using images.

I did some quick testing by caching the images beforehand and the lag was immediately gone - even in debug mode.

timunie commented 9 months ago

okay one last comment here. The icons look really "plain" svg pathes. In such cases I only use PathIcon control and set Data to a StreamGeometry placed in resources. Additional benefit: I can be styled :-)

markusalbrecht-procam commented 9 months ago

@timunie and Stefan. Ok this sound promising so we will definitely go for caching and also test the PathIcon approach. Thanks for your input. I will inform you once we changed ang tested it.

markusalbrecht-procam commented 9 months ago

BTW - we are currently using the Avalonia.Svg.Skia.SvgImageExtension so this seems to have no caching options

image

I guess we will need to write a wrapper arround this ? So an extension of SvgImageExtension probably

StefanKoell commented 9 months ago

That's correct. What I did was simply putting the Source (IImage) into a dictionary and use this in case it's already there. You may need to inherit from Image or have a custom converter to do this.

markusalbrecht-procam commented 9 months ago

@StefanKoell and @timunie Thanks for assistance the final implementation works perfect now. We are using the Avalonia TreeView combined with ImageCaching, now switching between documents and upating the tree even with IsExpanded states work within milliseconds. Solved from my point of view.

TreeView image

Caching image

image