oli.G
oli.G

Reputation: 1350

Asynchronously adding to ObservableCollection (or an alternative)

Here's what I have - a ListBox with an ItemsSource set to a ObservableCollection<T> - where T is my custom class representing a file, containing just 2 DependencyProperties: Filename and ThumbnailPath. - The listbox also has a custom DataTemplate defined, in order to nicely display a image and filename under it.

The purpose of the listbox is to display video files in the current folder (selected in a TreeView), with thumbnails (generated asynchronously; not part of this problem).

So when I change the folder in the TreeView, the ObservableCollection is cleared and filled up again, which is automatically reflected in the the ListBox items.

Here's the problem: The UI becomes unresponsive and it takes up to several seconds to update. Again, thumbnails do not have significance here (I tried disabling them). I think what takes the most time is the construction of 50-100 instances of my custom class, and their visual representation - it has to initialize an Image object for each one. But it's just my guess - could you please confirm or exclude the possibility?

I'm beginning to think ObservableCollection may not the way to go here, since from what I read and a little from what I tried, there's no way to add items asynchronously, at least if these items are DependencyObjects. I tried creating my class instances with a BackgroundWorker and adding them to the collection in the ProgressChanged event handler, but it throws an exception (some threading vs dependencyobjects problem).

Is there something that I'm missing? Or would I be better off by simply ditching the ObservableCollection and writing a good old async for loop to add the items?

Upvotes: 11

Views: 21724

Answers (4)

Rashid Hasanov
Rashid Hasanov

Reputation: 21

Try Thomas Levesque's solution.

 public class AsyncObservableCollection<T> : ObservableCollection<T>
    {
        private SynchronizationContext _synchronizationContext = SynchronizationContext.Current;

        protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
        {
            if (SynchronizationContext.Current == _synchronizationContext)
                RaiseCollectionChanged(e);
            else
                _synchronizationContext.Send(RaiseCollectionChanged, e);
        }

        private void RaiseCollectionChanged(object param)
            => base.OnCollectionChanged((NotifyCollectionChangedEventArgs)param);

        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (SynchronizationContext.Current == _synchronizationContext)
                RaisePropertyChanged(e);
            else
                _synchronizationContext.Send(RaisePropertyChanged, e);
        }

        private void RaisePropertyChanged(object param)
            => base.OnPropertyChanged((PropertyChangedEventArgs)param);
    }

Upvotes: 0

Aaron. S
Aaron. S

Reputation: 527

yea its an old question, i know, but questions like this are ALL over. i found this possible before IAsynEnumerable existed but its even easier now that IAsyncEnumerable exists.

property in ViewModel class

public ObservableCollection<Card> Cards { get; set; }

ViewModel constructor

public CardViewModel(IDbContextFactory<CardContext> ccf)
{
    this.ctxFactory = ccf; //DB Context, using DI
    this.Cards = new ObservableCollection<Card>();

    Task.Run(async () =>
    {
        await foreach (Card card in GetAllCards())
        {
            this.Cards.Add(card);
        }
    });
}

private IAsyncEnumerable<Card> GetAllCards()
{
    CardContext cc = this.ctxFactory.CreateDbContext();
            
    return cc.Cards
        .Include(cc => cc.Edition)
        .Include(cc => cc.Site)
        .Include(cc => cc.Condition)
        .Include(cc => cc.Purchases)
        .Include(cc => cc.Trades)
        .AsNoTracking()
        .AsAsyncEnumerable();
}

Cards is bound in View to a Datagrid

ItemsSource="{Binding Cards}"

'Cards' are added to grid one at a time and i can interact with the grid while its loading and the app doesnt freeze.

data comes from DB using EF Core 5, which is what CardContext is, in this example

Upvotes: 2

Rohit Vats
Rohit Vats

Reputation: 81243

Since your ObservableCollection is bound to UI hence it gets generated on UI thread so any further updates (delete/add/clear) have to be on the same UI thread. It doesn't allow updates from another thread.

However, what you can do is to create an instance of your class (or all time consuming operation on background thread) and once you are done, add the object in ObservableCollection using Dispatcher of your UI thread like this -

App.Current.Dispatcher.BeginInvoke((Action)delegate()
                          {
                              observableCollection.Add(instanceOfYourClass);
                          });

What Dispatcher do is put the operation on its associated thread. Hence, the item will always be added on UI thread but can be created in background thread.

Here are few links which might get you going - Updating from BW and the other one is here

Upvotes: 20

Andreas
Andreas

Reputation: 4013

With .net 4.5 you can use EnableCollectionSynchronization

 object lockObj = new object();
        BindingOperations.EnableCollectionSynchronization(yourObservableCollection, lockObj);

Upvotes: 11

Related Questions