다음을 통해 공유


AddRange and ObservableCollection

The Issue

Recently a friend asked a question about ObservableCollection. The question was how to do an “AddRange” method on it.

The obvious answer is, of course, just write a method which iterates over the input collection and calls Add for each. But this really isn’t the answer, because the question really isn’t about AddRange – the question is really about raising a single CollectionChanged event when adding multiple items.

Background

Some quick background on this. The INotifyCollectionChanged interface, which ObservableCollection implements, raises a CollectionChanged event, the payload of which is a NotifyCollectionChangedEventArgs object. The object has the ability to represent not just a single item being added/removed/moved/replaced, but a set of contiguous items.

The typical use of ObservableCollection is to databind some ItemsControl’s ItemSource to it. This means a few things:

· Updates to the collection must occur on the UI thread

· Updates will cause some UI updates

 

If we are adding a few items then the overhead of the marshalling to the UI thread, and the UI updates isn’t too bad. But if we are adding lots of items (i.e. thousands), then the overhead really adds up and can significantly impact the performance of the app. My friend saw AddRange as a way to avoid this overhead – first by having a single UI thread invoke, and second by doing 1 big UI update rather than lots of little ones.

 Modifying ObservableCollection

There is nothing magical with INotifyCollectionChanged and ObservableCollection. We could implement our own collection which implement INotifyCollectionChanged, but we’d rather not do that much work if we don’t have to.

Fortunately ObservableCollection is just a subclass of Collection<T> which also implements the INotifyCollectionChanged, and Collection<T> was designed to be a base class and provides nice extensibility points. All that means is implementing AddRange is as simple as:

· Creating a subclass of ObservableCollection

· Declare the method, which

o Uses the protected Items property to add all the items

o Raise the right events

 

Using Reflector we can look at the implementation of ObservableCollection, and find that it basically does the same thing.

Here is the code

        public void AddRange(IEnumerable<T> dataToAdd)

        {

            this.CheckReentrancy();

            //

            // We need the starting index later

            //

            int startingIndex = this.Count;

            //

            // Add the items directly to the inner collection

            //

            foreach (var data in dataToAdd)

            {

                this.Items.Add(data);

            }

            //

            // Now raise the changed events

            //

     this.OnPropertyChanged("Count");

            this.OnPropertyChanged("Item[]");

            //

            // We have to change our input of new items into an IList since that is what the

            // event args require.

            //

           var changedItems = new List<T>(dataToAdd);

            this.OnCollectionChanged(changedItems, startingIndex);

        }

The OnPropertyChanged/OnCollectionChanged are small helper methods which do the obvious thing.

Not Quite Done

The next thing to do is to wire up my new collection to an ItemsControl and call AddRange, which I did. Well it doesn’t work. It turns out that ListCollectionView doesn’t implement support for non-single item CollectionChanged events. In fact it raises a NotSupportedException with a message of “Range actions are not supported.”

Well that is pretty lame. What is the point of having the event support multiple items, when lacking any implementation which can either generate such an event, or consume it if it was generated?

Seems to me that the payload should have just been constrained to support single item changes.

Now I suppose we could implement our own ICollectionView, but that seems like a whole lot of work. Surely there must be a better way.

The Suggestion

Well, we have a worthy goal, but the initial approach of using AddRange isn’t panning out – and we don’t even know if it will give us the performance we want.

So what can we do to achieve our performance goals? Here are some suggestions which may help:

· Don’t Invoke items individually to the UI thread. Instead batch them up.

· Prefer BeginInvoke over Invoke so that the background thread can continue to harvest new items from the API or whatever while the UI is updating

· Use visual virtualization to reduce the cost of adding items which are not currently in view

 

Those suggestions tend to work pretty well, and are pretty low cost to implement. If those don’t work then you need to bring out the big guns – which should start by doing real performance analysis using tools like perfmon and a profiler. That will help identify the bottleneck so you can focus your improvements.

Comments