3

I'm converting a project from Windows Forms to WPF format. Currently, I've bounded all the data to elements. I now raise a problem in ObservableCollection, saying:

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

How can I make my code threadsafe? Or how do I guide the changes to the Dispatcher thread? I've seen a few posts about it but I'm confused on how to apply it on my own project. Maybe someone can shed some light on this for me?

This is my code of ObservableList.cs:

public class ObservableList<T> : ObservableCollection<T>
{
    #region Private members

    bool isInAddRange = false;

    #endregion Private members

    #region Public methods

    /// <summary>
    /// Creates a new empty ObservableList of the provided type. 
    /// </summary>
    public ObservableList()
    {

    }

    /// <summary>
    /// Handles the event when a collection has changed.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // intercept this when it gets called inside the AddRange method.
        if (!isInAddRange)
            base.OnCollectionChanged(e);
    }

    /// <summary>
    /// Adds a collection of items to the ObservableList.
    /// </summary>
    /// <param name="items"></param>
    public void AddRange(IEnumerable<T> items)
    {
        isInAddRange = true;
        foreach (T item in items)
        {
            Add(item);
        }

        isInAddRange = false;

        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,items.ToList());
        base.OnCollectionChanged(e);

    }

    #endregion Public methods
}

}

EDIT: After the answer given by ywm I changed my AddRange class to:

public void AddRange(IEnumerable<T> items)
{
    isInAddRange = true;
    foreach (T item in items)
    {
        if (item != null)
        {
            Dispatcher.CurrentDispatcher.Invoke((Action)(() =>
                {
                    Add(item);
                }));
        }
    }

    isInAddRange = false;

    var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,items.ToList());
    base.OnCollectionChanged(e);

}

Now, all my ObservableList's are null.

Joetjah
  • 6,292
  • 8
  • 55
  • 90

3 Answers3

4

When you add items to an ObservableCollection you need to invoke the UI dispatcher thread to do so.

This is done like so:

  Dispatcher.CurrentDispatcher.Invoke(() =>
  {
        foreach (var myModel in itemsToAdd)
        {
                    Images.Add(mymodel);                   
        } 
  });

Then in the class using it,

    public ObservableList<String> Strings { get; set; }

    public MyViewModel()
    {
        this.Strings = new ObservableList<string>();

        this.Strings.AddRange(new[] { "1", "2", "3", "4" });
    }
Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
ywm
  • 1,107
  • 10
  • 14
  • 1
    I would also extend you solution by recommending a dedicated method which adds a items and syncrhonizes the thread like AddItemThreadSafe(myItem item) so you dont get redundant code when you add items from multiple sources. You can also think about creating your own ThreadSafeObservableCollection which wraps the normale observable collection – Boas Enkler Feb 22 '13 at 12:57
  • Ok so I invoked the UI thread. I had to cast the lambda expression to `Action`-type first. When I tried to run this, I gained a lot of nullpointer exceptions. As for Boas Enkler's comment, I like the idea of creating an extra class which takes care of safe threading. Though, I have no idea where to start nor why the given solution doesn't work... – Joetjah Feb 22 '13 at 13:19
  • I looked a bit more into it and it seems getting an ObservableList returns `null`. This means adding items didn't quite work yet. Let me update my question to reflect the changes I made myself – Joetjah Feb 22 '13 at 13:23
  • I've updated my answer and tested your code, it appears to work when adding strings. – ywm Feb 22 '13 at 13:52
  • I've updated my code to reflect yours and I still get a nullpointer exception. The created ObservableList is `null` so I can't add items in it. This isn't the case without using the Invoke from Dispatch (but after a few seconds, the application crashes because of the problem stated in my Question). – Joetjah Feb 22 '13 at 14:02
  • Is it a problem when I cast the lambda to `Action`?? – Joetjah Feb 22 '13 at 14:03
  • No, because I included the cast, only difference is I added a constraint to the generic declaration `where T : class` – ywm Feb 22 '13 at 14:18
  • I'm sorry but I just can't get it to work. When copy-pasta'ing the code from http://stackoverflow.com/questions/2137769/where-do-i-get-a-thread-safe-collectionview it works, but I have no clue what I'm doing... – Joetjah Feb 22 '13 at 14:29
  • It seems to be a bit hit and miss, I'm using .net 4, which may have a possible effect. – ywm Feb 22 '13 at 15:06
  • I'm using .NET 4.0 as well. When I use the code you provided, my lists seem to be `null`. That sounds strange to me: why has this anything to do with creating the lists? – Joetjah Feb 25 '13 at 12:49
  • The problem was that the Collection isn't created by the UI thread, but by an other thread. See my response for more information about it! Thank you for your time and help! – Joetjah Feb 25 '13 at 13:08
0

While you are now calling the Add method in the UI thread, you are still raising the event in the AddRange method in the calling thread. So you'll end up with the same problem as before you made the change.

try this:

public void AddRange(IEnumerable<T> items)
{
    isInAddRange = true;
    foreach (T item in items)
    {
        if (item != null)
        {
            Dispatcher.CurrentDispatcher.Invoke((Action)(() =>
                {
                    Add(item);
                }));
        }
    }

    isInAddRange = false;

    var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,items.ToList());
    Dispatcher.CurrentDispatcher.Invoke((Action)(() =>
    {
        base.OnCollectionChanged(e);
    });

}
eoldre
  • 1,075
  • 18
  • 28
  • The problem was that the Collection isn't created by the UI thread, but by an other thread. See my response for more information about it! Thank you for your time and help! – Joetjah Feb 25 '13 at 13:08
0

I've found a clean solution here.

The problem that probably occured here is not that the UI thread should be called on the changes, but the thread which created the Collection! And that doesn't necessarily have to be the UI thread!

So, I changed my code to the following:

public class ObservableList<T> : ObservableCollection<T>
{
    #region Private members

    bool isInAddRange = false;
    private readonly Dispatcher _currentDispatcher;

    #endregion Private members

    #region Public methods

    /// <summary>
    /// Creates a new empty ObservableList of the provided type. 
    /// </summary>
    public ObservableList()
    {
        //Assign the current Dispatcher (owner of the collection) 
        _currentDispatcher = Dispatcher.CurrentDispatcher;
    }

    /// <summary>
    /// Executes this action in the right thread
    /// </summary>
    ///<param name="action">The action which should be executed</param>
    private void DoDispatchedAction(Action action)
    {
        if (_currentDispatcher.CheckAccess())
            action.Invoke();
        else
            _currentDispatcher.Invoke(DispatcherPriority.DataBind, action);
    }

    /// <summary>
    /// Handles the event when a collection has changed.
    /// </summary>
    /// <param name="e"></param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // intercept this when it gets called inside the AddRange method.
        if (!isInAddRange)
        {
            DoDispatchedAction(() => base.OnCollectionChanged(e));
        }
    }

    /// <summary>
    /// Adds a collection of items to the ObservableList.
    /// </summary>
    /// <param name="items"></param>
    public void AddRange(IEnumerable<T> items)
    {
        isInAddRange = true;
        foreach (T item in items)
        {
            if (item != null)
                {
                    Add(item);
            }
        }       

        isInAddRange = false;

        var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add,items.ToList());
        DoDispatchedAction(() => base.OnCollectionChanged(e));

    }

    #endregion Public methods
}

I do admit I haven't tested the AddRange() method itself yet. A sample given on the linked site is:

/// <summary>
/// Inserts a item at the specified index
/// </summary>
///<param name="index">The index where the item should be inserted</param>
///<param name="item">The item which should be inserted</param>
protected override void InsertItem(int index, T item)
{
    DoDispatchedAction(() => base.InsertItem(index, item));
}

Thank you all for your attempts to help me out!

Joetjah
  • 6,292
  • 8
  • 55
  • 90