Home > database >  WPF Application UI freezing despite collection view source being updated by a background worker
WPF Application UI freezing despite collection view source being updated by a background worker

Time:01-05

I have an app that retrieves data from a database and displays it in data grid on the main window. The maximum number of items being displayed is ~5000.

I don't mind a time delay in display the results, but i'd like to display a loading animation whilst this is happening. However, even when using a background worker to update the collection view source the UI freezes before displaying the rows.

Is it possible to add all these rows without freezing the UI? Apply filters to the collection view source also seems to freeze the UI which i'd like to avoid also if possible.

Thanks in advance!

XAML for the data grid:

<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 for collection view source:

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

Code within function being called after retrieving data from database:

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


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

Code within worker:

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

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

Observable collection class:

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));
        }
    }

Base class for observable collection:

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()
        {

        }
    }

Filter Function:

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;
                }
            }
        }

CodePudding user response:

There are a couple of problems with your design here.

The way the filter of a collectionview works is it iterates through the collection one by one and returns true/false.

Your filter is complicated and will take a while per item.

It's running 5000 times.

You should not use that approach to filter.

A rethink and fairly substantial refactor is advisable.

Do all your processing and filtering in a Task you run as a background thread.

Forget all that synchronisation context stuff.

Once you've done your processing, return a List of your finalised data back to the UI thread

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

If that doesn't get edited or sorted then set a List property your itemssource is bound to. If it does either then new up an observablecollection

  YourDocuments = new Observablecollection<Document>(yourReturnedList);

passing your list as a constructor paremeter and set a observablecollection property your itemssource is bound to.

Hence you do ALL your expensive processing on a background thread.

That is returned to the UI thread as a collection.

You set itemssource to that via binding.

Two caveats.

Minimise the number of rows you present to the UI.

If it's more than a couple of hundred then consider paging and maybe an intermediate cache.

Remove this out your binding

    , UpdateSourceTrigger=PropertyChanged

And never use it again until you know what it does.

Some generic datagrid advice:

Avoid column virtualisation.

Minimise the number of columns you bind.

If you can, have fixed column widths.

Consider the simpler listview rather than datagrid.

CodePudding user response:

The problem is your Filter callback. Currently you iterate over three lists inside the event handler (in order to create the filter predicate collections for lookup).
Since the event handler is invoked per item in the filtered collection, this introduces excessive work load for each filtered item.

For example, if each of the three iterations involves 50 items and the filtered collection contains 5,000 items, you execute a total of 5035000 = 750,000 iterations (150 for each event handler invocation).

I recommend to maintain the collections of selected items outside the Filter event handler, so that it doesn't have to be created for each individual item (event handler invocation). The three collections are only updated when a related SelectedItems proeprty has changed.

To further speed up the lookup in the Filter event handler I also recommend to replace the List<T> with a HashSet<T>.
While List.Contains is an O(n) operation, HashSet.Containsis O(1), which can make a huge difference.

You need to track the SelectedItems that are the source for those collections separately to update them.

Another crucial step is to ensure that UI virtualization is enabled, which is the default for the DataGrid (DataGrid.EnableRowVirtualization = true).
This way the Filter event is not raised for all 5,000 items at once, but only for those that are visible.

The following example should speed up your filtering significantly.

/* 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 DocumentFilter(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);
}
  • Related