-3

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?

John Doe
  • 117
  • 9
  • 2
    The problem is not the collection - as you should have noticed - but the ui which you stress with a huge amount - how many? - of items. For each item the ui will create and render controls and that can slow down your system. But you can try virtualization where the controls are reused by the ui and only the visible items are rendered – Sir Rufo Jan 22 '23 at 11:40
  • @SirRufo I know that the problem is rendering and since I have my custom DataTemplate's its completely fine that it takes some time to render them. The problem is the spinner gets frozen completely during that time. Even if the rendering took only 1 second the spinner will be frozen for 1 second. Ultimately my question is if its somehow possible to keep the loader spinning animation intact while the rendering is going on. Imagine rendering took only 3 seconds, but that means the loader will freeze for 3 seconds and even though 3 seconds is not much it leaves the impression the program crashed. – John Doe Jan 22 '23 at 12:01
  • Every rendering is done in the context of the MainThread. DataTemplate, Spinner, etc. and there is only one MainThread. There is nothing you can do against that basic rule – Sir Rufo Jan 22 '23 at 12:24
  • BTW I do not know what you know unless you write it down in the question. Writing a lot about different collections and not mention rendering does not show me you know about the rendering problem and the MainThread – Sir Rufo Jan 22 '23 at 12:28
  • The important part here is your templates. You should make them initially simple with fixed size.avoid column virtualization. So long as your datagrid really is virtualizing, your problem would then go away. The number of items in your observable collection is pretty much academic if you reduce down the number of measure arrange passes. – Andy Jan 22 '23 at 13:31
  • I'd have to see your templates and understand purpose better to give more specific advice. Maybe row details or a side by side view with a panel for selected item could achieve whatever the user requirements are. – Andy Jan 22 '23 at 13:34
  • @Andy In my code the biggest thing that makes rendering slow is "ScrollViewer.CanContentScroll" is set to "false" to have smooth scrolling, not setting it to false makes it render very quickly. But my problem here is not the loading time, but rather the freezing of spinning animation which makes it look like the app crashed. I have a spinner on the toppest level and during task I make it visible. But from what I learned, since the spinner and the content I am trying to render are on the same window they share the render thread and it causes the animation to freeze. – John Doe Jan 22 '23 at 14:20
  • You turned virtualization off . ScrollViewer.CanContentScroll" is set to "false" – Andy Jan 22 '23 at 14:22
  • As you scroll with virtualized ui you incur that measure arrange cost. – Andy Jan 22 '23 at 14:24
  • @Andy yeah removing the ScrollViewer.CanContentScroll makes the rendering pretty mich instant. But setting it to false makes the scrolling much smoother. Is there other way to improve scrolling without setting it to false? In my case I am fine rendering taking like 5 seconds and having better scrolling if the spinner doesn't freeze. – John Doe Jan 22 '23 at 14:31
  • Yes there is, I posted an answer. – Andy Jan 22 '23 at 14:33

2 Answers2

1

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.

Andy
  • 11,864
  • 2
  • 17
  • 20
-2

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/

John Doe
  • 117
  • 9
  • Technically, you can crank up another STA thread and render another window on that. Almost always a bad idea though. It's not rendering is the problem here. It's many expensive measure arrange passes. Just fixed row height and column width could well be enough to sort this out. – Andy Jan 22 '23 at 14:20