Wpf 组合框自定义模板绑定到大型集合时的性能

问题描述 投票:0回答:1

我用 ControlTemplate 制作了一个自定义组合框, 到目前为止,我对布局非常满意,但问题是绑定到大型集合时弹出窗口的性能, 总结一下布局,新的组合框有一个标签、切换按钮和弹出窗口

弹出窗口有一个列表框,用作新布局的项目呈现器,以及一个文本框“SearchBox”来过滤 CollectionView,

我将数据虚拟化应用于列表框,但我仍然遇到性能问题

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
        <VirtualizingStackPanel IsVirtualizing="True" VirtualizationMode="Recycling" />
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

还有类似的东西

ItemsPanel = new ItemsPanelTemplate();
var stackPanelTemplate = new FrameworkElementFactory(typeof(VirtualizingStackPanel));
ItemsPanel.VisualTree = stackPanelTemplate;

到组合框 ItemsPanel

我遇到的问题是,当弹出窗口即将出现时,它会冻结 UI 一段时间 当尝试使用 SearchBox_TextChanged 事件搜索或过滤集合时,UI 将冻结一段时间

新的布局行为:

这是 Githup

上的项目存储库

谢谢你!

c# wpf performance .net-core .net-8.0
1个回答
0
投票

首先,

ListBox
默认使用
VirtualizingStackPanel

只是不要:

  • 将附加属性

    ScrollViewer.CanContenScroll
    设置为
    false

  • 显式添加 ListBoxItem 容器。 例如:

    <ListBox>
      <ListBoxItem>
    </ListBox>
    
  • 请勿混合物品容器(例如添加

    Separator

接下来你必须改进你的过滤。您必须了解过滤器必须迭代完整的集合。

  1. 在过滤时提供繁忙指示器以避免冻结。然后在不同的线程上过滤。

  2. 如果可能,请考虑对集合进行预过滤,以减少初始大小。例如,仅提供最近三天的交易并对此数据集应用任何过滤器。强制用户显式展开列表。

  3. 在预过滤的上下文中,允许用户指定加载项目的数量似乎是无用或有害的(正如屏幕截图所暗示的那样)。至少确保这个数字与实际添加到

    ItemsSource
    的项目无关。

  4. 考虑禁用实时过滤,即用户键入后立即进行过滤。至少提供一个允许用户禁用它的操作,或者如果您知道项目列表将很大,则禁用它并允许用户启用它。当用户输入关键字后开始搜索(如 Visual Studion 搜索)时,您将获得更好的性能。

还支持实时过滤和排序的解决方案如下所示:

  • 创建一个包含总共项目数的数据池。该池可以分区。
  • 使用第二个集合作为实际数据源,并首先将第一页(例如 20 个项目)添加到
    ItemsSource
  • 根据过滤器表达式过滤池,并根据需要在后台线程上对结果进行排序
  • 使用过滤和排序的结果更新源集合

MainWindow.xaml

<Window>
  <StackPanel>
    <ProgressBar IsIndeterminate="{Binding IsBusy}" />
    <TextBox Text="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=FilterExpression}" />
    <ListBox ItemsSource="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=Numbers}" />
  </StackPanel>
</Window>

MainWindow.xaml.cs

partial class MainWindow : Window
{ 
  public string FilterExpression
  {
    get => (string)GetValue(FilterExpressionProperty);
    set => SetValue(FilterExpressionProperty, value);
  }
 
  public static readonly DependencyProperty FilterExpressionProperty = DependencyProperty.Register(
    "FilterExpression", 
    typeof(string), 
    typeof(MainWindow), 
    new PropertyMetadata(default(string), OnFilterExpressionChanged));

  public bool IsBusy
  {
    get => (bool)GetValue(IsBusyProperty);
    set => SetValue(IsBusyProperty, value);
  }
 
  public static readonly DependencyProperty IsBusyProperty = DependencyProperty.Register(
    "IsBusy", 
    typeof(bool), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  public ObservableCollection<int> Numbers
  {
    get => (ObservableCollection<int>)GetValue(NumbersProperty);
    set => SetValue(NumbersProperty, value);
  }
 
  public static readonly DependencyProperty NumbersProperty = DependencyProperty.Register(
    "Numbers", 
    typeof(ObservableCollection<int>), 
    typeof(MainWindow), 
    new PropertyMetadata(default));

  private object SyncLock { get; }
  private List<int> NumbersStore { get; }
  private ICollectionView NumbersStoreView { get; }
  private CancellationTokenSource FilterCancellationTokenSource { get; set; }

  public MainWindow()
  {
    InitializeComponent();

    this.Numbers = new ObservableCollection<int>();
    this.NumbersStore = new List<int>();
    this.NumbersStoreView = CollectionViewSource.GetDefaultView(this.NumbersStore);

    // Allow the collection to be modified on a background thread
    this.SyncLock = new object();
    BindingOperations.EnableCollectionSynchronization(this.Numbers, this.SyncLock);

    // Run initialization of huge collection on a background thread
    // to avoid long loading times were the UI is not available.
    this. Loaded += OnLoadedAsync;
  }

  public async void OnLoadedAsync(object sender, EventArgs e)
  {
    this.IsBusy = true;
    await Task.Run(Initialize);
    this.IsBusy = false;
  }

  public void Initialize()
  {
    // Initialize the data pool
    this.NumbersStore = Enumerable.Range(1, 30000).ToList();

    // Initialize the view with N items if you have implemented data pagination
    // or present an empty list that the user must fill 
    // by providing a filter expression
    // or show all items (make sure to enable UI virtualization 
    // if it is not enabled by default which is the case for 
    // e.g., for the ComboBox. ListBox has this feature enabled by default).
    foreach (int number in this.NumbersStore)
    {
      this.Numbers.Add(number);
    }
  }

  private static void OnFilterExpressionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
  {
    var _this = (MainWindow)d;

    _this.FilterCancellationTokenSource?.Cancel();
    _this.FilterCancellationTokenSource?.Dispose();
    _this.FilterCancellationTokenSource  = new CancellationTokenSource();
    var cancellationToken = _this.FilterCancellationTokenSource.Token;
    _ = Task.Run(() => _this.FilterNumbers(cancellationToken), cancellationToken);
  }

  private void FilterNumbers(CancellationToken cancellationToken)
  {
    try
    {
      // Show the busy indicator
      this.IsBusy = true;

      this.NumbersStoreView.Filter = IsItemAccepted; 
      if (cancellationToken.IsCancellationRequested)
      {
        return;
      }

      this.Numbers.Clear();
      foreach (int filteredNumber in this.NumbersStoreView)
      {
        if (cancellationToken.IsCancellationRequested)
        {
          return;
        }

        this.Numbers.Add(filteredNumber);
      }
    }
    finally
    {
      this.IsBusy = false;
    }
  }

  private bool IsItemAccepted(object item) 
    => int.TryParse(this.FilterExpression, out int number) 
         && (int)item == number;
}
© www.soinside.com 2019 - 2024. All rights reserved.