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!
UPDATE 06.01.2023
Updated as per the suggestions from BionicCode and Andy and now everything is running very smoothly - thank you for the help!
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;
}
}
}
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.
EDIT: Experimentation seems to confirm this statement is true. AFAIK virtualisation is purely in creation of UI from the collection. Collectionviewsource > Collectionview > Itemssource. Creation of UI rows is virtualised by the virtualising stackpanel but the whole collection will be read into itemssource.
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
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
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.
The custom observablecollection is a bad idea. You should just use List or Observablecollection where t is a viewmodel. Any viewmodel should implement inotifypropertychanged. Always.
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
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.