如何更快地在 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种策略:
- 操作collection然后赋值:
var items = new ObservableCollection<MyTreeViewItem>();
items.Add(....)
myTreeview.Items = items;
这有点违反直觉,因为人们会认为现场 collection 不需要那种技巧。我喜欢看的方式是:初始化不应该被任何人打断/UI。尽管应该听取增量更改(在那里,布局和渲染不应该引人注意)。
- 使用支持范围操作的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 布局和渲染。
我正在开发一个允许用户编辑数据记录的 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种策略:
- 操作collection然后赋值:
var items = new ObservableCollection<MyTreeViewItem>();
items.Add(....)
myTreeview.Items = items;
这有点违反直觉,因为人们会认为现场 collection 不需要那种技巧。我喜欢看的方式是:初始化不应该被任何人打断/UI。尽管应该听取增量更改(在那里,布局和渲染不应该引人注意)。
- 使用支持范围操作的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 布局和渲染。