Home > OS >  How to make adding controls in a WPF application faster
How to make adding controls in a WPF application faster

Time:04-27

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.

CodePudding user response:

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 1000 was also tested with 10,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();
 }

CodePudding user response:

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.

  • Related