Threading problem when adding items to an ObservableCollection
Asked Answered
S

1

7

I'm updating an ObservableCollection of a WPF ViewModel in a WCF Data Service asynchronous query callback method:

ObservableCollection<Ent2> mymodcoll = new ObservableCollection<Ent2>();
 ...
query.BeginExecute(OnMyQueryComplete, query);
 ...
private void OnMyQueryComplete(IAsyncResult result)
    {
        ...
        var repcoll = query.EndExecute(result);

        if (mymodcoll.Any())
        {
            foreach (Ent c in repcoll)
            {
                var myItem = mymodcoll.Where(p => p.EntID == c.EntID).FirstOrDefault();
                if (myItem != null) 
                {
                    myItem.DateAndTime = c.DateAndTime; // here no problems
                    myItem.Description = c.Description;
                     ...
                }
                else
                {
                    mymodcoll.Add(new Ent2 //here I get a runtime error
                    {
                        EntID = c.EntID,
                        Description = c.Description,
                        DateAndTime = c.DateAndTime,
                        ...
                    });
                }
            }
        }
        else
        {
            foreach (Ent c in repcoll)
            {
                mymodcoll.Add(new Ent2 //here, on initial filling, there's no error
                {
                    EntID = c.EntID,
                    Description = c.Description,
                    DateAndTime = c.DateAndTime,
                    ...
                });
            }
        }
    }  

The problem is, when a query result collection contains an item which is not present in the target collection and I need to add this item, I get a runtime error: The calling thread cannot access this object because a different thread owns it. (I pointed out this line of code by a comment)

Nevertheless, if the target collection is empty (on initial filling) all items have been added without any problem. (This part of code I also pointed out by a comment). When an item just needs to update some of its fields, there are no problems as well, the item gets updated ok.

How could I fix this issue?

Singultus answered 25/4, 2011 at 18:53 Comment(0)
D
3

First case: Here you a modifying an object in the collection, not the collection itself - thus the CollectionChanged event isn't fired.

Second case: here you are adding a new element into the collection from a different thread, the CollectionChanged event is fired. This event needs to be executed in the UI thread due to data binding.

I encountered that problem several times already, and the solution isn't pretty (if somebody has a better solution, please tell me!). You'll have to derive from ObservableCollection<T> and pass it a delegate to the BeginInvoke or Invoke method on the GUI thread's dispatcher.

Example:

public class SmartObservableCollection<T> : ObservableCollection<T>
{
    [DebuggerStepThrough]
    public SmartObservableCollection(Action<Action> dispatchingAction = null)
        : base()
    {
        iSuspendCollectionChangeNotification = false;
        if (dispatchingAction != null)
            iDispatchingAction = dispatchingAction;
        else
            iDispatchingAction = a => a();
    }

    private bool iSuspendCollectionChangeNotification;
    private Action<Action> iDispatchingAction;

    [DebuggerStepThrough]
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        if (!iSuspendCollectionChangeNotification)
        {
            using (IDisposable disposeable = this.BlockReentrancy())
            {
                iDispatchingAction(() =>
                {
                    base.OnCollectionChanged(e);
                });
            }
        }
    }
    [DebuggerStepThrough]
    public void SuspendCollectionChangeNotification()
    {
        iSuspendCollectionChangeNotification = true;
    }
    [DebuggerStepThrough]
    public void ResumeCollectionChangeNotification()
    {
        iSuspendCollectionChangeNotification = false;
    }


    [DebuggerStepThrough]
    public void AddRange(IEnumerable<T> items)
    {
        this.SuspendCollectionChangeNotification();
        try
        {
            foreach (var i in items)
            {
                base.InsertItem(base.Count, i);
            }
        }
        finally
        {
            this.ResumeCollectionChangeNotification();
            var arg = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
            this.OnCollectionChanged(arg);
        }
    }


}
Drachm answered 25/4, 2011 at 18:58 Comment(4)
Thank you! I tried your way, making my target collection of SmartObservableCollection type. But now I get the same error in your base class, in OnCollectionChanged(NotifyCollectionChangedEventArgs e) method, in base.OnCollectionChanged(e); line of code. What I could miss?Singultus
You need to supply a delegate to either the Invoke or BeginInvoke method of the Dispatcher associated with the UI thread.Drachm
What is the "this.BlockReentrancy()" method? Is that something you wrote, or something in WPF? It doesn't exist in WP7/Silverlight.Livvyy
BlockReentrancy. Basically, it prevents changes to the collection while in the using block. Dunno why it isn't in wp7/silverlight.Drachm

© 2022 - 2024 — McMap. All rights reserved.