I have a DataGrid which gets populated by instances of class 'Article' and I have DataGridTemplateColumn-s to display data in my custom way. I also have a Loader implemented which is basically an overlay spinner for my long running tasks.
I fetch the list of articles from database in async Task and then I want to render those articles in UI. The problem is rendering the articles on UI takes quite a while (probably because my custom DataTemplates in DataGridTemplateColumns, I have expanders and other stuff there and displaying them takes a while) and it freezes the spinner. Since I fetch the data in Task during that time the spinner works fine, it freezes when it's time to display it on UI. I have tried several different ways, and nothing worked so far.
My initial version:
// Collection bound to my DataGrid's ItemsSource
public ObservableCollection<Article> Articles { get; set; } = new ObservableCollection<Article>();
// Inside the method of fetching arthicles:
public async Task Populate()
{
IsSpinnerVisibile = true;
// The spinner works fine during this time
List<Article> articles = new List<Article>();
await Task.Run(() =>
{
articles = new ArticleRepo().LoadArticles(Users[UserIndex], filter.GetFilterString());
});
// PROBLEM CODE - the spinner freezes for several seconds during this time, when I am adding articles to ObservableCollection
foreach (Article article in articles)
{
this.Articles.Add(article);
}
IsSpinnerVisible = false;
}
I tried switching ObservableCollection to List and just calling property change when list was populated but the result was exactly the same:
// Collection bound to my DataGrid's ItemsSource (changed it to List)
public List<Article> Articles { get; set; } = new List<Article>();
// Inside the method of fetching arthicles:
public async Task Populate()
{
IsSpinnerVisibile = true;
// The spinner works fine during this time
await Task.Run(() =>
{
this.Articles = new ArticleRepo().LoadArticles(Users[UserIndex], filter.GetFilterString());
});
// PROBLEM CODE - the spinner freezes for several seconds during this time
OnPropertyChanged("Articles");
IsSpinnerVisible = false;
}
// OnPropertyChanged() is a method for INotifyPropertyChanged implementation, here is how I do it:
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
I also tried putting the ObservableCollection filling part in Dispatcher (it was a possible solution I read here):
Dispatcher.CurrentDispatcher.BeginInvoke((Action)delegate ()
{
foreach (Article article in articles)
{
this.Articles.Add(article);
}
});
The only thing that semi-worked was this solution: C#/WPF Main Window freezes when loading data into datagrid - In the comments it was suggested to put data in collection chunk-by-chunk and calling Task.Delay() in between. Here is how I understood and implemented the solution:
// Collection bound to my DataGrid's ItemsSource
public ObservableCollection<Article> Articles { get; set; } = new ObservableCollection<Article>();
// Inside the method of fetching arthicles:
public async Task Populate()
{
IsSpinnerVisibile = true;
// The spinner works fine during this time
List<Article> articles = new List<Article>();
await Task.Run(() =>
{
articles = new ArticleRepo().LoadArticles(Users[UserIndex], filter.GetFilterString());
});
// PROBLEM CODE - in this case the spinner keeps spinning but its very laggy/jaggedy
int counter = 1;
foreach (Article article in articles)
{
// After 25 item 'chunk' call delay
if (counter == 25)
{
await Task.Delay(100);
counter = 0;
}
this.Articles.Add(article);
counter ;
}
IsSpinnerVisible = false;
}
But in the last solution even though the spinner doesn't completely freeze its spins in very laggy/jaggedy manner, because I assume it only spins during 'delay' time. So it sort of mini-freezes, then keeps going during delay then mini-feezes again and so on. So how should I implement so that the spinner doesn't freeze?
CodePudding user response:
The simplest way to fix your problems:
Do not make ScrollViewer.CanContentScroll" false.
Your UI will then virtualize and you don't need to worry about loading data in chunks.
Ensure row height and column width are fixed in your datagrid.
As you scroll with virtualized ui you incur the measure arrange cost per datagrid cell. This is what makes scrolling expensive. At the moment each row of data being templated into UI has expensive measure arrange calculations as the UI tries to work out if it needs to adjust columns, what height that next row will take so it can tell if the one after should now be in view... Etc.
Reduce the measure arrange cost and scrolling will be smoother.
CodePudding user response:
With further research this seems to be not possible. Since windows can't have multiple rendering threads, once you are waiting for long rendering operation to complete you can't render loading animation at the same time. One workaround I found is to create separate semi-transparent window overlay which has its own separate rendering thread and render the loading spinner there.
Source: http://graemehill.ca/wpf-responsiveness-asynchronous-loading-animations-during-rendering/