17

In a WPF application an ObservableCollection is filled and updated by LINQ to SQL queries. Then UI objects are updated using values from this ObservableCollection.

Is it possible and reasonable that operations of updating this ObservableCollection by LINQ to SQL queries were executed in a separate thread?

If yes, will, in this case, it be one and the same instance of this ObservableCollection? (I mean, if it is not the same one for taking values from LINQ datacontext and the one for giving values to update UI, then I will not be able to update UI)

rem
  • 16,105
  • 37
  • 108
  • 179

5 Answers5

30

.Net 4.5 provides a solution within the BindingOperations class.

You can now use the BindingOperations.EnableCollectionSynchronization method as follows:

private readonly object _personCollectionLock;
private ObservableCollection<Person> _personCollection;

public ObservableCollection<Person> PersonCollection
{
  get { return _personCollection; }
  set
  { 
    _personCollection = value;
    BindingOperations.EnableCollectionSynchronization(_personCollection, _personCollectionLock);
  }

I have only just tried this in my development environment but everything seems to be working correctly now when I update the collection from a background thread.

There is a more in-depth discussion of this solution at: http://10rem.net/blog/2012/01/16/wpf-45-observable-collection-cross-thread-change-notification

The MSDN entry for this method is at: https://msdn.microsoft.com/en-us/library/system.windows.data.bindingoperations.enablecollectionsynchronization(v=vs.110).aspx

Brian Hinchey
  • 3,501
  • 1
  • 41
  • 36
28

With the built-in ObservableCollection<T> class, you can't change the content from a separate thread if the UI is bound to the collection, it throws a NotSupportedException (but change notification for properties of collection items works fine). I wrote an AsyncObservableCollection<T> class to handle this case. It works by invoking the event handlers on the UI synchronization context

Thomas Levesque
  • 278,830
  • 63
  • 599
  • 738
  • a posible alternative http://stackoverflow.com/questions/12881489/asynchronously-adding-to-observablecollection-or-an-alternative – Narkha Sep 25 '13 at 15:53
  • Or you can try this which is comletely thread-safe, works from any thread, and can be databound by multiple UI threads : http://www.codeproject.com/Articles/64936/Multithreaded-ObservableImmutableCollection – Anthony Apr 15 '14 at 00:59
  • Nice job on the collection!! – Trae Moore Jun 26 '14 at 17:31
  • @Thomas Levesque how to deal with "ghosts" (probably because the UI is enemerating while content is being added). See https://stackoverflow.com/questions/13624289/wpf-datagrid-is-adding-extra-ghost-row. Do you have a solution for that? Perhaps some locking required? – Werner May 22 '18 at 09:48
  • Brilliant! Very easy to implement. Thank you! – Smitty-Werben-Jager-Manjenson Jul 15 '21 at 14:08
7

In our app, we have a TreeView bound to an ObservableCollection, which we regularly update in a background thread, requesting data from our storage. It works perfectly!

Whoops. I was mis-informed =))

Right, we're actually subclassing the ObservableCollection<T> and override the OnCollectionChanged method to avoid the UI crossthreading exception. We're using this solution:

public class MTObservableCollection<T> : ObservableCollection<T>
{
   public override event NotifyCollectionChangedEventHandler CollectionChanged;
   protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
   {
      var eh = CollectionChanged;
      if (eh != null)
      {
         Dispatcher dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()
                 let dpo = nh.Target as DispatcherObject
                 where dpo != null
                 select dpo.Dispatcher).FirstOrDefault();

        if (dispatcher != null && dispatcher.CheckAccess() == false)
        {
           dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));
        }
        else
        {
           foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())
              nh.Invoke(this, e);
        }
     }
  }
}

Without that override you'd get an exception like that

System.NotSupportedException : This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Now the only problem we have is the selected item position, in some cases if the currently selected item is deleted from the collection the TreeView moves the selection to the next item (which causes some other unnecessary UI actions in our app). But that's a small issue.

Max Galkin
  • 16,522
  • 10
  • 64
  • 110
  • 2
    How is that possible? Surely you are marshaling back to the UI thread to apply the changes from your data storage? Otherwise, you'd get an exception... – Kent Boogaart Jan 20 '10 at 20:42
  • 3
    You have to create a sub class of ObservableCollection that marshalls the CollectionChanged event back to the UI thread. I find that this is easier to manage than having the ViewModel managing update. – Nigel Sampson Jan 20 '10 at 21:09
  • Thanks, Kent, Nigel -- you are right! I've corrected my answer, it was my mistake. – Max Galkin Jan 21 '10 at 09:21
2

Trying to understand your question here:

Scenario 1
1. LINQ to SQL retrieves data set from database and adds to ObservableCollection A.
2. Periodically, more data is retrieved from database and added to A. Old data is removed from A.

Scenario 2
1. LINQ to SQL retrieves data set from database and adds to ObservableCollection A.
2. Periodically, data in A is updated with new data from database (no add/remove).

With Scenario 1, you're going to have to use the UI thread. The UI thread owns the ObservableCollection and you'll get an exception if you try to use it in another thread.

With Scenario 2, thumbs up. As long as you don't try to add or remove items from the collection itself, you can update the item as much as you want in a background thread.

apandit
  • 3,294
  • 1
  • 25
  • 32
2

I did not receive the same System.NotSupportedException, but unfortunately my UI did not Update properly if I set the my ObservableCollectiony<MyType> from another thread.

The only thing that worked for me is presented by https://www.codeproject.com/Tips/1111432/Update-data-to-WPF-control-from-another-thread:

In ViewModel

private SynchronizationContext _syncContext = SynchronizationContext.Current;

private ObservableCollection<PackageModel> _packageModelList;
public ObservableCollection<PackageModel> PackageModelList
{
     get => _packageModelList;
     set
     {
         if (_packageModelList == value)
             return;
         packageModelList = value;
         RaisePropertyChanged("PackageModelList");
     }
}      

In other thread

 _syncContext.Send(x => { PackageModelList = OtherThreadPackageModels; },null);

Hope this helps others.

MaxWolodin
  • 99
  • 7