-2

I'm working on a WPF app that lets users edit data records.

For a test, I'm adding 50 rows to a tree view and this takes ~200ms. This creates noticable stutter as the application isn't interactive during that time. This is only the time for creating and populating controls, no data loading or any work that could be done in a thread.

Since all these rows fit on a screen, I think it would not benefit from making it a virtualizing panel.

Is it possible to make this faster? How would I add these over multiple "frames"? How can I profile this? How can I determine a reasonable number of controls that my WPF app should be able to render?

Edit: Adding a minimal example to reproduce.

MainWindow.xaml:

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel>
        <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="10">
            <Button Width="100" Click="Button_Click">Test</Button>
            <TextBlock Margin="10" x:Name="resultTextBox">Result</TextBlock>
        </StackPanel>
        <TreeView x:Name="myTreeView"></TreeView>
    </DockPanel>
</Window>

MainWindow.cs:

using System.Diagnostics;
using System.Windows;

namespace WpfApp1
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var myStopwatch = new Stopwatch();
            myStopwatch.Start();

            this.myTreeView.Items.Clear();
            for (int i = 0; i < 50; i++)
            {
                this.myTreeView.Items.Add(new MyTreeViewItem());
            }

            myStopwatch.Stop();
            this.resultTextBox.Text = "It took " + myStopwatch.ElapsedMilliseconds + " ms to add 50 tree view items.";
        }
    }
}

MyTreeViewItem.xaml:

<TreeViewItem x:Class="WpfApp1.MyTreeViewItem"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <TreeViewItem.Header>
        <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
            <TextBlock>I'm a TextBlock</TextBlock>
            <Button>I'm a button</Button>
            <CheckBox>I'm a checkbox</CheckBox>
            <TextBox>I'm a text box</TextBox>
            <ComboBox SelectedIndex="0">
                <ComboBox.Items>
                    <ComboBoxItem>I'm a combobox</ComboBoxItem>
                </ComboBox.Items>
            </ComboBox>
        </StackPanel>
    </TreeViewItem.Header>
    <TreeViewItem.Items>
        <TreeViewItem Visibility="Collapsed"></TreeViewItem>
    </TreeViewItem.Items>
</TreeViewItem>

Screenshot: enter image description here

According to the VS profiler, it takes an additional 150ms for the Layout step after adding the items.

Toast
  • 596
  • 2
  • 19
  • 39
  • You could run a profiler and see where your app gets stuck. – Dark Templar Apr 19 '22 at 20:44
  • It is very difficult to comment when there is no information about your app. How do you add the data, where do you add it? Simply, updating the UI after adding 50 rows instead of updating every time you add a row will provide a little more performance for you. – Ozgur Saklanmaz Apr 20 '22 at 05:20
  • @OzgurSaklanmaz How do I do one or the other in WPF? – Toast Apr 20 '22 at 09:03
  • I don't know what and how he added, how can I describe it? Is the row in a DataGrid or is it a row in your imagination? There is a lot of missing information. You can find new answers if you update the question with your stages. – Ozgur Saklanmaz Apr 20 '22 at 11:43
  • @OzgurSaklanmaz I'm adding a row like this: `myTreeViewItem.Items.Add(new TreeViewItem());`. You mentioned "updating the UI". As far as I know, I'm not ever "updating the UI" explicitly. Is there a way to control when that happens? – Toast Apr 20 '22 at 12:02
  • What is myTreeViewItem? observablecollection ? – Ozgur Saklanmaz Apr 20 '22 at 12:33
  • What I mean by updating the UI is: For example, every time an Observablecollection adds an element, it updates the interface it is connected to in the background. You can manipulate this. After adding the elements, you can have the update triggered. Also why don't you update the issue? Is there a reason why their codes are so secret? – Ozgur Saklanmaz Apr 20 '22 at 12:36
  • It's all tree view items (or custom classes derived from it). I'm not using an ObservableCollection. TreeViewItem.Items is an ItemsCollection. It seems that I can only add one item at a time to an ItemsCollection. – Toast Apr 20 '22 at 13:39
  • The project has thousands of lines of code and I'm not sure which of those are relevant to the question. I can try to create a minimal example that shows a WPF app being slow, but I'm not sure if that's worth it since the question already got downvoted and closed. – Toast Apr 20 '22 at 13:41
  • I have prepared an example for you, but I cannot post an answer because the question is closed. When the question opens I will post the answer or you can open a new question. – Ozgur Saklanmaz Apr 21 '22 at 06:39
  • You are effectively eliminating any chance of UI virtualization by adding TreeViewItem instances directly to the TreeView. You must add data models instead, define a Itemtemplate for the data model and let the TreeView generate and manage the lifetime of the item containers. Then set the attached property VirtualizingStackPanel.IsVirtualizing on the TreeView to `true`. this will give you UI virtualization and should fix your performance issues related to TreeView rendering. – BionicCode Apr 22 '22 at 19:21
  • @BionicCode I'm using virtualization now and as expected, it gives a performance improvement for large numbers of items. However, I can fit ~50 lines on a screen, so these don't benefit from virtualization, but adding them is still too slow. – Toast Apr 22 '22 at 22:52
  • 1
    50 items of your simple items is not much. If your ComboBox holds many items too, you must enable virtualization for it too. TreeView virtualization does not virtualize the depth of the tree. Only the root. To further improve the performance you must implement data virtualization: for example load the first two levels and on expanding a node, preload the next, so that you you have always two (or more) levels preloaded. – BionicCode Apr 22 '22 at 23:13
  • 1
    To improve the initial loading time: to avoid the consecutive changes of the Items property which triggers the layout re-calculation of all the previously added visible items, add the items to a collection first. Populate this source collection and then assign/bind it to the TreeView.ItemsSource property. This will result in layout calculation to occur only once per item. – BionicCode Apr 22 '22 at 23:13
  • Did you take a look at this: [How do I suspend painting for a control and its children?] (https://stackoverflow.com/questions/487661/how-do-i-suspend-painting-for-a-control-and-its-children/487757#487757). It's possible that the performance and stutter are do the frequent redrawing of the TreeView as you add the items in a tight loop. – Sam Goldberg Apr 25 '22 at 20:33
  • @OzgurSaklanmaz the question was reopened, you can post your answer. Thank you! – Toast Apr 27 '22 at 04:18
  • @SamGoldberg That question is about Winforms, does the answer work for WPF applications too? – Toast Apr 27 '22 at 04:19

2 Answers2

1

I have prepared an MVVM supported application. There are 100.000 TreeViewItems in the example. These items have 400.000 TreeViewItems as children. The following example is a simple example of clicking the buttons You will need to add extra binding properties to access the data entered in the TextBoxes. You can check here for additional performance improvements.

Note: Virtualization is a big advantage. Sample 100.000 was also tested with 1.000.000. There is no performance issue. When you virtualize data, items will be loaded as soon as they appear in the UI.

MainWindow.xaml

    <Window.Resources>
    <ResourceDictionary>
        <Style x:Key="TreeViewItemStyle" TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="False" />
        </Style>

        <HierarchicalDataTemplate x:Key="HeaderTemplate"
                                  ItemsSource="{Binding Children, Mode=OneTime}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="I'm a TextBlock."/>
                <Button Content="I'm a Button."/>
                <CheckBox Content="I'm a CheckBox."/>
                <TextBox Text="I'm a TextBox."/>
                <ComboBox SelectedIndex="0">
                    <ComboBoxItem Content="I'm a ComboBox"/>
                </ComboBox>
            </StackPanel>
        </HierarchicalDataTemplate>

    </ResourceDictionary>
</Window.Resources>

<DockPanel>
    <Button
            DockPanel.Dock="Top"
            Content="Add Items"
        Command="{Binding AddTreeViewItemCommand}"/>
    <TreeView  ItemContainerStyle="{StaticResource TreeViewItemStyle}"
               ItemsSource="{Binding Items}"
               VirtualizingStackPanel.IsVirtualizing="True"
               VirtualizingStackPanel.VirtualizationMode="Recycling"
               ItemTemplate="{StaticResource HeaderTemplate}"
  />
</DockPanel>

TreeViewModel.cs

 public class TreeModel
{
    public TreeModel(string name)
    {
        this.Name = name;
        this.Children = new List<TreeModel>();
    }

    public List<TreeModel> Children { get; private set; }

    public string Name { get; private set; }
}

TreeViewModel.cs

public class TreeViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private List<TreeModel> _items;

    public List<TreeModel> Items
    {
        get { return _items; }
        set
        {
            _items = value;
            OnPropertyChanged();
        }
    }

    private List<TreeModel> CreateTreeViewItems()
    {
        List<TreeModel> Items = new List<TreeModel>();
        for (int i = 0; i < 100000; i++)
        {
            TreeModel root = new TreeModel("Item 1")
            {
                Children =
            {
                new TreeModel("Sub Item 1")
                {
                    Children =
                    {
                        new TreeModel("Sub Item 1-2"),
                        new TreeModel("Sub Item 1-3"),
                        new TreeModel("Sub Item 1-4"),
                    }
                },
            }
            };
            Items.Add(root);
        }

        return Items;
    }

    public RelayCommand AddTreeViewItemCommand { get; set; }
    public TreeViewModel()
    {
        AddTreeViewItemCommand = new RelayCommand(AddTreeViewItem);
    }

    private void AddTreeViewItem(object param)
    {
        Items = CreateTreeViewItems();
    }

    protected void OnPropertyChanged([CallerMemberName] string name = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

RelayCommand.cs

 public class RelayCommand : ICommand
{
    #region Fields
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion

    #region Constructors

    public RelayCommand(Action<object> execute) : this(execute, null) { }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion

    #region ICommand Members

    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }
    #endregion
}

Don't forget the datacontext :)

 public MainWindow()
 {
     InitializeComponent();

     DataContext = new TreeViewModel();
 }
Ozgur Saklanmaz
  • 528
  • 3
  • 17
1

In main Button_Click you are clearing the items and then add them one by one. What happens is that after each operation a collection changed event is raised -caught by the UI leading to layout and render after each Clear/Add.

Virtualization helps, if those items are not on the screen as mentioned above. Besides you could consider 2 kinds of strategies:

  1. Manipulate the collection and then assign it:
    var items = new ObservableCollection<MyTreeViewItem>();
    items.Add(....)
    myTreeview.Items = items;

This is a bit contra intuitive as one would like to think that live collections should not need that kind of trick. The way I like to see it is: initialization should not be interrupted by anyone/ UI. Incremental changes though should be listened to (there, the layout & render should not be noticeable).

  1. Use a collection which supports range operations, i.e. adding all items in one go.
 public class MyObservableCollection<T> : ObservableCollection<T>
    {
        public void AddRange(params T[] items)
        {
            foreach (var item in items)
            {
                this.Items.Add(item); // does not raise event!
            }

            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, items));
            OnPropertyChanged(new PropertyChangedEventArgs(nameof(Count));
        }
    }

The difference in this option is the number of layout & renders. Adding 100 items would still be rendered only once (instead of 100 times). Clear & AddRange is 2 layout & renders.

Mheriff
  • 81
  • 4