尽管后台工作线程正在更新集合视图源,但WPF应用程序UI仍冻结

5us2dqdw  于 2023-01-06  发布在  其他
关注(0)|答案(2)|浏览(154)

我有一个从数据库中检索数据并在主窗口的数据网格中显示的应用程序。正在显示的最大项目数为~5000。
我不介意在显示结果时有时间延迟,但是我希望在这一过程中显示一个加载动画。然而,即使使用后台工作器更新集合视图源代码,UI也会在显示行之前冻结。
有没有可能添加所有这些行而不冻结UI?应用过滤器到集合视图源代码似乎也冻结了UI,如果可能的话,我也想避免。
先谢了!
数据网格的XAML:

<DataGrid Grid.Column="1" Name="documentDisplay" ItemsSource="{Binding Source={StaticResource cvsDocuments}, UpdateSourceTrigger=PropertyChanged, IsAsync=True}" AutoGenerateColumns="False" 
                      Style="{StaticResource DataGridDefault}" ScrollViewer.CanContentScroll="True"
                      HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" ColumnWidth="*">

集合视图源的XAML:

<Window.Resources>
        <local:Documents x:Key="documents" />
        <CollectionViewSource x:Key="cvsDocuments" Source="{StaticResource documents}"
                                Filter="DocumentFilter">

从数据库检索数据后调用的函数中的代码:

Documents _documents = (Documents)this.Resources["documents"];
            
            BindingOperations.EnableCollectionSynchronization(_documents, _itemsLock);

            if (!populateDocumentWorker.IsBusy)
            {
                progressBar.Visibility = Visibility.Visible;
                populateDocumentWorker.RunWorkerAsync(jobId);
            }

工人内部代码:

Documents _documents = (Documents)this.Resources["documents"];

                lock (_itemsLock)
                {
                    _documents.Clear();
                    _documents.AddRange(documentResult.documents);
                }

可观察到的采集类别:

public class Documents : ObservableCollection<Document>, INotifyPropertyChanged
    {
        private bool _surpressNotification = false;

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (!_surpressNotification)
            {
                base.OnCollectionChanged(e);
            }
        }

        public void AddRange(IEnumerable<Document> list)
        {
            if(list == null)
            {
                throw new ArgumentNullException("list");

                _surpressNotification = true;
            }
            
            foreach(Document[] batch in list.Chunk(25))
            {
                foreach (Document item in batch)
                {
                    Add(item);
                }
                _surpressNotification = false;
            }
 
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

可观察集合的基类:

public class Document : INotifyPropertyChanged, IEditableObject
    {
        public int Id { get; set; }
        public string Number { get; set; }
        public string Title { get; set; }
        public string Revision { get; set; }
        public string Discipline { get; set; }
        public string Type { get; set;  }
        public string Status { get; set; }
        public DateTime Date { get; set; }
        public string IssueDescription { get; set; }
        public string Path { get; set; }
        public string Extension { get; set; }

        // Implement INotifyPropertyChanged interface.
        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(String info)
        {
            if(PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

        private void NotifyPropertyChanged(string propertyName)
        {

        }

        // Implement IEditableObject interface.
        public void BeginEdit()
        {

        }

        public void CancelEdit()
        {

        }

        public void EndEdit()
        {

        }
    }

过滤器功能:

private void DocumentFilter(object sender, FilterEventArgs e)
        {
            //Create list of all selected disciplines
            List<string> selectedDisciplines = new List<string>();
            
            foreach(var item in disciplineFilters.SelectedItems)
            {
                selectedDisciplines.Add(item.ToString());
            }

            //Create list of all select document types
            List<string> selectedDocumentTypes = new List<string>();

            foreach(var item in docTypeFilters.SelectedItems)
            {
                selectedDocumentTypes.Add(item.ToString());
            }

            // Create list of all selected file tpyes
            List<string> selectedFileTypes = new List<string>();

            foreach(var item in fileTypeFilters.SelectedItems)
            {
                selectedFileTypes.Add(item.ToString());
            }

            //Cast event item as document object
            Document doc = e.Item as Document;

            //Apply filter to select discplines and document types
            if( doc != null)
            {
                if (selectedDisciplines.Contains(doc.Discipline) && selectedDocumentTypes.Contains(doc.Type) && selectedFileTypes.Contains(doc.Extension))
                {
                    e.Accepted = true;
                } else
                {
                    e.Accepted = false;
                }
            }
        }
tzxcd3kk

tzxcd3kk1#

你的设计有几个问题。
collectionview的filter的工作方式是逐个迭代集合并返回true/false。

    • 编辑:实验似乎证实了这一说法。AFAIK虚拟化纯粹是从集合创建UI。集合视图源〉集合视图〉项源。UI行的创建由虚拟化堆栈面板虚拟化,但整个集合将被读入项源。**

你的过滤器很复杂,每个项目都需要一段时间。
它运行了5000次。
不应使用该方法进行筛选。
重新思考和相当实质性的重构是可取的。
在作为后台线程运行的任务中执行所有处理和过滤。
忘记所有同步上下文的东西。
完成处理后,向UI线程返回最终数据的List

async Task<List<Document>> GetMyDocumentsAsync
  {
     // processing filtering and really expensive stuff.
     return myListOfDocuments;
  }

如果没有编辑或者排序,那么设置你的itemssource绑定的List属性,如果有,那么新建一个可观的集合

YourDocuments = new Observablecollection<Document>(yourReturnedList);

将列表作为构造函数参数传递,并设置一个与itemssource绑定的observablecollection属性。
因此,您在后台线程上执行所有昂贵的处理。
作为集合返回给UI线程的。
您可以通过绑定将itemssource设置为该值。
自定义Observablecollection不是个好主意。你应该只使用List或Observablecollection,其中t是一个视图模型。任何视图模型都应该实现intifypropertychanged。总是。
两个警告。
最小化呈现给UI的行数。
如果超过几百个,则考虑分页,也许还可以考虑中间缓存。
把这个从你的包里拿出来

, UpdateSourceTrigger=PropertyChanged

在你知道它的作用之前不要再用它。
一些通用数据网格建议:
避免列虚拟化。
最小化绑定的列数。
如果可以,请使用固定的列宽。
考虑一下更简单的列表视图而不是数据网格。

omqzjyyz

omqzjyyz2#

问题出在Filter回调函数上,目前在事件处理程序中迭代了三个列表(为了创建用于查找的过滤 predicate 集合)。
由于事件处理程序是针对筛选集合中的 * 每个项 * 调用的,因此这会为每个 * 筛选项 * 带来过多的工作量。
例如,如果三次迭代中的每次迭代涉及50个项,而筛选的集合包含5,000个项,则总共执行50 * 3 * 5000 = 750,000次迭代(每次事件处理程序调用150次)。
我建议在Filter事件处理程序之外维护选定项的集合,这样就不必为每个单独的项创建集合(事件处理程序调用)。这三个集合仅在相关的SelectedItems属性更改时更新。
为了进一步加快Filter事件处理程序中的查找速度,我还建议将List<T>替换为HashSet<T>
List.Contains是一个 * O(n)* 操作,而HashSet.Contains是 * O(1)* 操作,这会产生巨大的差异。
您需要单独跟踪作为这些集合的源的SelectedItems,以更新它们。
下面的示例应该可以显著加快过滤速度。

/* Define fast O(1) lookup collections */
private HashSet<string> SelectedDisciplines { get; set; }
private HashSet<string> SelectedDocumentTypes { get; set; }
private HashSet<string> SelectedFileTypes { get; set; }

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'disciplineFilters.SelectedItems' is)
private void OnDisciplineSelectedItemsChanged()
  => this.SelectedDisciplines = new HashSet<string>(this.disciplineFilters.SelectedItems.Select(item => item.ToString()));

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'docTypeFilters.SelectedItems' is)
private void OnDocTypeSelectedItemsChanged()
  => this.SelectedDocumentTypes = new HashSet<string>(this.docTypeFilters.SelectedItems.Select(item => item.ToString()));

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'fileTypeFilters.SelectedItems' is)
private void OnFileTypeSelectedItemsChanged()
  => this.SelectedFileTypes = new HashSet<string>(this.fileTypeFilters.SelectedItems.Select(item => item.ToString()));
  
private void FilterDocuments(object sender, FilterEventArgs e)
{
  // Cast event item as document object
  if (e.Item is not Document doc) //if (!(e.Item is Document doc))
  {
    return;
  }

  // Apply filter to select discplines and document types
  e.Accepted = this.SelectedDisciplines.Contains(doc.Discipline) 
    && this.SelectedDocumentTypes.Contains(doc.Type) 
    && this.SelectedFileTypes.Contains(doc.Extension);
}

备注

您应该修复您的Documents.AddRange方法。
它应该使用NotifyCollectionChangedAction.AddNotifyCollectionChangedAction.Replace将触发绑定目标完全更新自己,这是您希望避免的。
使用适当的NotifyCollectionChangedEventArgs构造函数重载发送事件中添加项的完整范围:

OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, list as IList ?? list.ToList()));

进一步考虑

  • 既然你说你从数据库读取数据,你应该考虑让数据库为你过滤数据。如果查询正确,数据库将提供更好的性能,因为它是高度优化的过滤(搜索查询)。
  • 使用ICollectionView的过滤器功能将始终阻塞UI,直到集合被过滤。这是因为该过程不是异步的。这意味着您无法显示进度条,因为它不会实时更新。请考虑在从数据库获取项目时进行预过滤。当用户只能查看其中的10 - 50个项目时,加载5k个项目是没有意义的。
  • 如果你想显示一个进度条,你最好直接过滤集合。这需要一个专用的绑定源集合。因为你已经实现了一个自定义的ObservableCollection,它公开了一个AddRange方法,所以你可以开始了(不要忘记修复CollectionChanged事件数据)。

下面的示例扩展了上面的基本示例。
在使用数据库中的数据填充UnfilteredDocuments属性时,需要将DataGrid绑定到FilteredItemsSource属性:

// The binding source for the ProgressBar. 
// Can be bound to Visibility or used as predicate for a Trigger
// This property must be implemented as dependency property!
public bool IsFilterInProgress { get; set; }

// Binding source for the ItemsControl
public Documents FilteredItemsSource { get; } = new Documents();

// Structure for the database data
private List<Document> UnfilteredDocuments { get; } = new List<Document>();

/* Define fast O(1) lookup collections */
private HashSet<string> SelectedDisciplines { get; set; }
private HashSet<string> SelectedDocumentTypes { get; set; }
private HashSet<string> SelectedFileTypes { get; set; }
private object SyncLock { get; } = new object();

// Constructor
public MainWindow()
{
  InitializeComponent();

  // Enable CollectionChanged propagation to the UI thread
  // when updating a INotifyCollectionChanged collection from a background thread
  BindingOperations.EnableCollectionSynchronization(this.FilteredItemsSource, this.SyncLock);
} 

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'disciplineFilters.SelectedItems' is)
private async void OnDisciplineSelectedItemsChanged(object sender, EventArgs e)
{
  this.SelectedDisciplines = new HashSet<string>(this.disciplineFilters.SelectedItems.Select(item => item.ToString()));
  await ApplyDocumentFilterAsync();
}

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'docTypeFilters.SelectedItems' is)
private async void OnDocTypeSelectedItemsChanged(object sender, EventArgs e)
{
  this.SelectedDocumentTypes = new HashSet<string>(this.docTypeFilters.SelectedItems.Select(item => item.ToString()));
  await ApplyDocumentFilterAsync();
}

// Could be invoked from a SelectionChanged event handler
// (what ever the source 'fileTypeFilters.SelectedItems' is)
private async void OnFileTypeSelectedItemsChanged(object sender, EventArgs e)
{
  this.SelectedFileTypes = new HashSet<string>(this.fileTypeFilters.SelectedItems.Select(item => item.ToString()));
  await ApplyDocumentFilterAsync();
}

private async Task ApplyDocumentFilterAsync()
{
  // Show the ProgressBar
  this.IsFilterInProgress = true;

  // Allow displaying of a progress bar (prevent the UI from freezing)
  await Task.Run(FilterDocuments);

  // Hide the ProgressBar
  this.IsFilterInProgress = false;
}

private void FilterDocuments()
{
  this.FilteredItemsSource.Clear();
  IEnumerable<Document> filteredDocuments = this.UnfilteredDocuments.Where(IsDocumentAccepted);
  this.FilteredItemsSource.AddRange(filteredDocuments);
}

private bool IsDocumentAccepted(Document document)
  => this.SelectedDisciplines.Contains(doc.Discipline)     
    && this.SelectedDocumentTypes.Contains(doc.Type) 
    && this.SelectedFileTypes.Contains(doc.Extension);

相关问题