如何更快地在 WPF 应用程序中添加控件

How to make adding controls in a WPF application faster

我正在开发一个允许用户编辑数据记录的 WPF 应用程序。

为了进行测试,我将 50 行添加到树视图中,这需要大约 200 毫秒。 这会造成明显的卡顿,因为应用程序在此期间没有交互。 这只是创建和填充控件的时间,没有数据加载或任何可以在线程中完成的工作。

由于所有这些行都适合屏幕,我认为将其设为虚拟化面板不会有任何好处。

有可能让它更快吗?我如何将这些添加到多个“框架”上?我该如何描述这个?如何确定我的 WPF 应用程序应该能够呈现的合理数量的控件?

编辑:添加一个最小的例子来重现。

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>

截图:

根据 VS 探查器,添加项目后布局步骤需要额外 150 毫秒。

我准备了一个支持 MVVM 的应用程序。示例中有 100.000 个 TreeViewItems。这些项目有 400.000 个 TreeViewItems 作为子项。以下示例是单击按钮的简单示例 您将需要添加额外的绑定属性以访问在文本框中输入的数据。您可以查看 here 以获得更多性能改进。

注意:虚拟化是一个很大的优势。样品 100.000 也用 1.000.000 进行了测试。没有性能问题。当您虚拟化数据时,项目将在它们出现在 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
}

不要忘记数据上下文:)

 public MainWindow()
 {
     InitializeComponent();

     DataContext = new TreeViewModel();
 }

在 main Button_Click 中,您正在清除项目,然后将它们一一添加。发生的情况是,在每次操作之后,都会引发一个 collection changed 事件 - 被 UI 捕获,导致在每次 Clear/Add.

之后进行布局和渲染

虚拟化会有所帮助,如果这些项目 不在屏幕上 如上所述。另外你可以考虑2种策略:

  1. 操作collection然后赋值:
    var items = new ObservableCollection<MyTreeViewItem>();
    items.Add(....)
    myTreeview.Items = items;

这有点违反直觉,因为人们会认为现场 collection 不需要那种技巧。我喜欢看的方式是:初始化不应该被任何人打断/UI。尽管应该听取增量更改(在那里,布局和渲染不应该引人注意)。

  1. 使用支持范围操作的collection,即一次添加所有项。
 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));
        }
    }

此选项的区别在于布局和渲染的数量。添加 100 个项目仍然只会呈现一次(而不是 100 次)。 Clear & AddRange 是 2 布局和渲染。