Fast performing and thread safe observable collection
Asked Answered
Y

4

26

ObservableCollections raise notifications for each action performed on them. Firstly they dont have bulk add or remove calls, secondly they are not thread safe.

Doesn't this make them slower? Cant we have a faster alternative? Some say ICollectionView wrapped around an ObservableCollection is fast? How true is this claim.

Yokoyama answered 7/10, 2011 at 12:7 Comment(2)
Try this collection which takes care of this problem as well as other multi-threaded problems (though any cross-threaded solution will be slower) that will inevitably crop up with other approaches : codeproject.com/Articles/64936/…Ideology
When you say "thread safe" do you mean that you need to be able to update the collection from multiple threads?Obscuration
S
76

ObservableCollection can be fast, if it wants to. :-)

The code below is a very good example of a thread safe, faster observable collection and you can extend it further to your wish.

using System.Collections.Specialized;

public class FastObservableCollection<T> : ObservableCollection<T>
{
    private readonly object locker = new object();

    /// <summary>
    /// This private variable holds the flag to
    /// turn on and off the collection changed notification.
    /// </summary>
    private bool suspendCollectionChangeNotification;

    /// <summary>
    /// Initializes a new instance of the FastObservableCollection class.
    /// </summary>
    public FastObservableCollection()
        : base()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// This event is overriden CollectionChanged event of the observable collection.
    /// </summary>
    public override event NotifyCollectionChangedEventHandler CollectionChanged;

    /// <summary>
    /// This method adds the given generic list of items
    /// as a range into current collection by casting them as type T.
    /// It then notifies once after all items are added.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void AddItems(IList<T> items)
    {
       lock(locker)
       {
          this.SuspendCollectionChangeNotification();
          foreach (var i in items)
          {
             InsertItem(Count, i);
          }
          this.NotifyChanges();
       }
    }

    /// <summary>
    /// Raises collection change event.
    /// </summary>
    public void NotifyChanges()
    {
        this.ResumeCollectionChangeNotification();
        var arg
             = new NotifyCollectionChangedEventArgs
                  (NotifyCollectionChangedAction.Reset);
        this.OnCollectionChanged(arg);
    }

    /// <summary>
    /// This method removes the given generic list of items as a range
    /// into current collection by casting them as type T.
    /// It then notifies once after all items are removed.
    /// </summary>
    /// <param name="items">The source collection.</param>
    public void RemoveItems(IList<T> items)
    {
        lock(locker)
        {
           this.SuspendCollectionChangeNotification();
           foreach (var i in items)
           {
             Remove(i);
           }
           this.NotifyChanges();
        }
    }

    /// <summary>
    /// Resumes collection changed notification.
    /// </summary>
    public void ResumeCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = false;
    }

    /// <summary>
    /// Suspends collection changed notification.
    /// </summary>
    public void SuspendCollectionChangeNotification()
    {
        this.suspendCollectionChangeNotification = true;
    }

    /// <summary>
    /// This collection changed event performs thread safe event raising.
    /// </summary>
    /// <param name="e">The event argument.</param>
    protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
        // Recommended is to avoid reentry 
        // in collection changed event while collection
        // is getting changed on other thread.
        using (BlockReentrancy())
        {
            if (!this.suspendCollectionChangeNotification)
            {
                NotifyCollectionChangedEventHandler eventHandler = 
                      this.CollectionChanged;
                if (eventHandler == null)
                {
                    return;
                }

                // Walk thru invocation list.
                Delegate[] delegates = eventHandler.GetInvocationList();

                foreach
                (NotifyCollectionChangedEventHandler handler in delegates)
                {
                    // If the subscriber is a DispatcherObject and different thread.
                    DispatcherObject dispatcherObject
                         = handler.Target as DispatcherObject;

                    if (dispatcherObject != null
                           && !dispatcherObject.CheckAccess())
                    {
                        // Invoke handler in the target dispatcher's thread... 
                        // asynchronously for better responsiveness.
                        dispatcherObject.Dispatcher.BeginInvoke
                              (DispatcherPriority.DataBind, handler, this, e);
                    }
                    else
                    {
                        // Execute handler as is.
                        handler(this, e);
                    }
                }
            }
        }
    }
}

Also ICollectionView that sits above the ObservableCollection is actively aware of the changes and performs filtering, grouping, sorting relatively fast as compared to any other source list.

Again observable collections may not be a perfect answer for faster data updates but they do their job pretty well.

Splendor answered 7/10, 2011 at 12:16 Comment(20)
+1 for the custom implementation. But does Microsoft say this anywhere, about the ICollectionView and ObservableCollection pairYokoyama
I don't have BlockReentrancy() or DispatcherObject in my Silverlight project, am I missing a reference or is this WPF?Capstan
Another question, if this is thread safe AND faster, why does Microsoft not do this for the standard ObservableCollection? Is there a 'cost' to this that means it is not a best 'default' implementation?Capstan
Define "faster". I don't see how it can possibly be faster to raise collection changed with RESET when adding two items, rather than just adding those two items separately. If your collection has 1000 items in it, the UI is going to have to refresh for all 1000 items instead of 2! I think it is technically possible to write an intelligent observable collection that batches together updates for you and raises collection changed events with the correct actions, but raising a RESET every time is . . . awful.Overmodest
@KentBoogaart unfortunately, WPF only listens to CollectionChanged on Reset.Nullification
@KentBoogaart A good idea would be to set a threshold value, up to which collection is notified for every item added, and calling reset when threshold is exceeded.Derril
@Baboon: that's flat out incorrect. Adding a single item to an ObservableCollection raises a single CollectionChanged event with Action=Add. WPF would be terribly inefficient if it required a full refresh for every collection change! The OP presents a collection with an API and name that purports to be fast when it is actually going to be much slower in an overwhelming number of use cases.Overmodest
@Gregory: I think you're somewhat missing the point. The CollectionChanged event (and NotifyCollectionChangedEventArgs class) already cater for a single event to notify of multiple additions/removals/whatever, as long as those changes are sequential within the collection.Overmodest
@KentBoogaart I implemented an ObservableLinkedList, and without Reset the UI isn't refreshed. So no, it is not "flat out" incorrect, it's experience.Nullification
@Baboon: sorry, but my point stands. WPF does just fine with ObservableCollection's single event with action Add. You must have made a mistake elsewhere in your implementation. You can very easily prove this by newing up an OC<string>, attaching an event handler, and adding an item. You get a single event with action Add.Overmodest
@KentBoogaart I'm talking about inheriting from ObservableCollection or composing over it. Direct usage works (but it's not you who directly raise the event).Nullification
@Baboon: not making any sense, sorry. WPF doesn't care who raises the event. Perhaps you should post your implementation in a separate question and I can chime in there.Overmodest
@KentBoogart Of course ObservableCollection can fire events for single item added. The problem this code solves (and why it is being called fast) is adding a lot of items to the collection. If you would add a lot of items one at a time, you would notice huge performance drop due to large amount of NotifyCollectionChanged events fired. Which is why it is better to supress add event for a time and reset the collection when sufficient amount of items is to be added.Derril
@Kent , Baboon & Gregory, sorry for not being tracking the progress on this thread. But Yes, as Gregory said, the AddItems implementation is practical for large number of items. For a few hundreds I wouldnt even bother to use AddItems. But the performance improvement I get when I add multi-thousands of items to the collection is significant. You can test that yourself. A reset for 1000 newly added items and 1000 individual notifications make a lot of difference. Also WPF item container reuses virtualized items anyways. So even for Reset call, the item container reuses indi rows.Splendor
DispatcherObject and prioty can't be used by my common library. What can I do about it?Basis
When you say "can't be used by my common library" what do you mean?Splendor
This is a solution to the AddRange problem but it's definitely not thread-safe. Try binding to this collection on the ui and have some both the UI and background threads updating it and you will quickly run into exceptions.Ideology
@Anthony, Thx for noting that down for me. I have added the locker object now. See the edit. Thx again.Splendor
This is definitely not thread safe. Even though you added locker around your methods, the methods implemented in the parent i.e. ObservableCollection are not using it. I do not think you can actually create thread safe collection by subclassing ObservableCollection.Vtol
What if change IList to IEnumerable? I think performance would be better, for example when using Linq we don't have to call ToList() to compile.Valentijn
D
5

Here is a compilation of some solutions which I made. The idea of collection changed invokation taken from first answer.

Also seems that "Reset" operation should be synchronous with main thread otherwise strange things happen to CollectionView and CollectionViewSource.

I think that's because on "Reset" handler tries to read the collection contents immediately and they should be already in place. If you do "Reset" async and than immediately add some items also async than newly added items might be added twice.

public interface IObservableList<T> : IList<T>, INotifyCollectionChanged
{
}

public class ObservableList<T> : IObservableList<T>
{
    private IList<T> collection = new List<T>();
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    private ReaderWriterLock sync = new ReaderWriterLock();

    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
    {
        if (CollectionChanged == null)
            return;
        foreach (NotifyCollectionChangedEventHandler handler in CollectionChanged.GetInvocationList())
        {
            // If the subscriber is a DispatcherObject and different thread.
            var dispatcherObject = handler.Target as DispatcherObject;

            if (dispatcherObject != null && !dispatcherObject.CheckAccess())
            {
                if ( args.Action == NotifyCollectionChangedAction.Reset )
                    dispatcherObject.Dispatcher.Invoke
                          (DispatcherPriority.DataBind, handler, this, args);
                else
                    // Invoke handler in the target dispatcher's thread... 
                    // asynchronously for better responsiveness.
                    dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, args);
            }
            else
            {
                // Execute handler as is.
                handler(this, args);
            }
        }
    }

    public ObservableList()
    {
    }

    public void Add(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Add(item);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                      NotifyCollectionChangedAction.Add, item));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void Clear()
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Clear();
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                        NotifyCollectionChangedAction.Reset));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public bool Contains(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.Contains(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void CopyTo(T[] array, int arrayIndex)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.CopyTo(array, arrayIndex);
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public int Count
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                return collection.Count;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
    }

    public bool IsReadOnly
    {
        get { return collection.IsReadOnly; }
    }

    public bool Remove(T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            var index = collection.IndexOf(item);
            if (index == -1)
                return false;
            var result = collection.Remove(item);
            if (result)
                OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index));
            return result;
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return collection.GetEnumerator();
    }

    public int IndexOf(T item)
    {
        sync.AcquireReaderLock(Timeout.Infinite);
        try
        {
            var result = collection.IndexOf(item);
            return result;
        }
        finally
        {
            sync.ReleaseReaderLock();
        }
    }

    public void Insert(int index, T item)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            collection.Insert(index, item);
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public void RemoveAt(int index)
    {
        sync.AcquireWriterLock(Timeout.Infinite);
        try
        {
            if (collection.Count == 0 || collection.Count <= index)
                return;
            var item = collection[index];
            collection.RemoveAt(index);
            OnCollectionChanged(
                    new NotifyCollectionChangedEventArgs(
                       NotifyCollectionChangedAction.Remove, item, index));
        }
        finally
        {
            sync.ReleaseWriterLock();
        }
    }

    public T this[int index]
    {
        get
        {
            sync.AcquireReaderLock(Timeout.Infinite);
            try
            {
                var result = collection[index];
                return result;
            }
            finally
            {
                sync.ReleaseReaderLock();
            }
        }
        set
        {
            sync.AcquireWriterLock(Timeout.Infinite);
            try
            {
                if (collection.Count == 0 || collection.Count <= index)
                    return;
                var item = collection[index];
                collection[index] = value;
                OnCollectionChanged(
                        new NotifyCollectionChangedEventArgs(
                           NotifyCollectionChangedAction.Replace, value, item, index));
            }
            finally
            {
                sync.ReleaseWriterLock();
            }
        }

    }
}
Dimenhydrinate answered 3/4, 2014 at 12:34 Comment(0)
C
2

I can't add comments because I'm not cool enough yet, but sharing this issue I ran into is probably worth posting even though it's not really an answer. I kept getting an "Index was out of range" exception using this FastObservableCollection, because of the BeginInvoke. Apparently changes being notified can be undone before the handler is called, so to fix this I passed the following as the fourth parameter for the BeginInvoke called from the OnCollectionChanged method (as opposed to using the event args one):

dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

Instead of this:

dispatcherObject.Dispatcher.BeginInvoke
                          (DispatcherPriority.DataBind, handler, this, e);

This fixed the "Index was out of range" issue I was running into. Here's a more detailed explaination / code snpipet: Where do I get a thread-safe CollectionView?

Campbell answered 10/6, 2013 at 22:12 Comment(1)
That's because the FastObservableCollection isn't thread-safe. The collections referred to by that link aren't either because they don't provide TryXXX methods and because of that you will always have issues such as exceptions when trying to access something that is no longer there because the check and operations are not atomic. Try codeproject.com/Articles/64936/…Ideology
A
-1

An example where is created a synchronized Observable list:

newSeries = new XYChart.Series<>();
ObservableList<XYChart.Data<Number, Number>> listaSerie;
listaSerie = FXCollections.synchronizedObservableList(FXCollections.observableList(new ArrayList<XYChart.Data<Number, Number>>()));
newSeries.setData(listaSerie);
Alliteration answered 26/3, 2014 at 5:53 Comment(1)
Please complete your answer.Brigidabrigit

© 2022 - 2024 — McMap. All rights reserved.