2

Related issues have come up in a number of previous questions (see below), but none of the solutions discussed seem to work in this case. The application in question uses an ObservableCollection<T> to drive a regular WPF ListBox (with some custom styling) via data-binding. The ObservableCollection may contain a large number of items, but as the ListBox by default operates in a virtualized way, this isn't an issue. ('Static' performance with hundreds of thousands of items is very respectable.)

However, if the collection already contains a large number of items and then a further large number of items is added (via ObservableCollection<T>.Add()), inevitably the UI grinds to a halt for a while due to all the NotifyCollectionChanged events being fired. The recommended workaround here is to sub-class the ObservableCollection and implement an AddRange() method, which adds the new elements to the (protected) Items collection, and then to call NotifyCollectionChanged with NotifyCollectionChangedAction.Reset.

In my experience, this solves one problem, but creates another. Whilst there is no longer a large number of Add events being raised, the Reset notification causes the ListBox to reevaluate the whole collection, which if it contains many tens of thousands of items, can take considerable time. This, coupled with the fact that in the target application, large numbers of items may be added frequently, means the application is chewing a lot of CPU for a long time whilst the ListBox digests all the new information.

It's arguable in hindsight that ObservableCollection<T> was not the right vehicle for offering up such a large dataset to the user, but having come this far, it's hard to see an easy way to rearchitect the app to deal with this issue.

Suggestions gratefully received; many thanks in advance.

Related questions:

Community
  • 1
  • 1
paytools-steve
  • 3,580
  • 3
  • 26
  • 21
  • Is your performance issue regarding `ObservableCollection` related to `UI` being updated? Or you are bringing up an issue with `Adding/Reseting` within `ObservableCollection`? – 123 456 789 0 Mar 13 '14 at 21:28
  • Put it this way, if I comment out the line that raises the NotifyCollectionChanged event with NotifyCollectionChangedAction.Reset, the CPU stays near zero whilst hundreds of items are being added to the ObservableCollection. – paytools-steve Mar 13 '14 at 21:51
  • Just to be sure, I just ran some profiling on the app - 90% of the busy time is spent in `System.Windows.ContextLayoutManager.UpdateLayout` (where it appears making thousands of calls to `UIElement.Arrange` and `UIElement.Measure`). – paytools-steve Mar 13 '14 at 22:34
  • @SteveWilkinson: What about not doing `Reset` and simply reporting the added items instead? – Jon Mar 13 '14 at 23:03
  • Then pretty simple don't call the line the raises the NotifyCollectionChanged event until the hundred of lines are added. – paparazzo Mar 14 '14 at 00:07
  • @Blam He is using `ObservableCollection` which automatically notifies the UI when there are updates within the collection. – 123 456 789 0 Mar 14 '14 at 03:05
  • @SteveWilkinson Is the data needed to be loaded right away and shown to the UI? Does the app need to hold on to that large amount of collection right away when you add/delete/modify? – 123 456 789 0 Mar 14 '14 at 03:06
  • @lll - I want the app to appear to continue to be responsive ie., for updates to appear in ListBox, even if there is some inevitable lag. I'm really thinking I have no choice but to implement some sort of throttling mechanism, one where I invoke the Reset action every second or so under high load. – paytools-steve Mar 17 '14 at 15:47
  • @SteveWilkinson Is the update has to be shown right away i.e. Add/Update/Delete on the ListBox? I tested a project with DATA + UI Virtualization with 1 million with Add/Update/Delete and it doesn't lag to me. Can you give me a sample project to reproduce it? – 123 456 789 0 Mar 18 '14 at 14:55
  • @lll - you were right - see my answer below. – paytools-steve Mar 19 '14 at 18:50

3 Answers3

0

Try not binding directly to your ObservableCollection, bind to ICollectionView instead.

That way your UI will not be affected while adding items to your OC because your Items Control is not bound to your OC and when you're done adding items to OC you just update your ICollectionView by calling CollectionViewSource.GetDefatultView() and your UI is updated with the new OC state...

Here is how I do it and I never noticed any UI issues (granted I never tried with hundreds of thousands of items but I did try with thousands of items and didn't notice any UI issues so I'm pretty sure this would solve your problem:

Task.Factory.StartNew(() => SessionList = serviceAgent.GetSessions(someId, someStartDate)).
ContinueWith(t => Sessions.Add(SessionList), TaskScheduler.FromCurrentSynchronizationContext()).
ContinueWith(t => SessionsView = CollectionViewSource.GetDefaultView(Sessions),
                TaskScheduler.FromCurrentSynchronizationContext());

Sessions is the OC and SessionsView is the ICollectionView...

This also has added benefits. This will fetch records on a background thread utilizing TPL and it will allow you to manipulate your View in many ways without affecting the actual underlying collection or having to run LINQ queries against the collection and displaying the resulting collection...

Dean Kuga
  • 11,878
  • 8
  • 54
  • 108
  • Unless I am misunderstanding something, I don't see how this would change anything. The binding engine already binds to the default `ICollectionView` anyway, even if you think you are binding to the collection itself. – Jon Mar 13 '14 at 23:02
  • Yes, but if you have a Collection View separate from your OC AND your Items Control is bound to that instead of the OC AND you refresh your binding AFTER all the items were added to the OC your are not binding to the default Collection View you mention so how is adding items to OC going to affect the UI? From personal experience I did notice UI freezing when I was binding directly to OC and that disappeared after biding to an instance of Collection View... Did you ever try to do it the way I described and observed continued UI freezing? – Dean Kuga Mar 13 '14 at 23:13
  • No, so I can't say. But it would be more convincing if you also had a theory about why binding to a non-default ICV would help. Or perhaps if you posted a before/after sample somewhere. – Jon Mar 13 '14 at 23:17
  • You're right actually, since the Collection View is a View of the actual collection created by the call to GetDefaultView() the UI will be updated while items are being added to the actual collection. However, I just tried this approach and while 10,000 items (which implement INotifyPropertyChanged) were being added to the collection the UI was fully responsive so I don't think adding 100,000 items would make any difference.... – Dean Kuga Mar 13 '14 at 23:46
  • I guess Collection View's relation to the underlying collection would have to be somehow severed prior to adding the items to the collection and re-established once the items have been added to avoid any changes to UI while the items are being added but I've never tried that and I'm not sure how that could be done... – Dean Kuga Mar 13 '14 at 23:49
  • @DeanK. - thanks for the response - it looks like you copied-and-pasted some code from your app, but it isn't really obvious to me how to go about adding this level of indirection from scratch in my app. (I think the combination of custom styling on my ListBox and more complicated ListBox item structure is slowing things down, but I don't have a lot of choice on that - if the ListBox is still receiving Add events, then this approach won't help, and some sort of "smart" (and probably ugly) throttling is my only option.) – paytools-steve Mar 17 '14 at 15:51
0

Granted this is just string but I am not seeing the ListBox take a long time to digest the new information. Can't just use Notify or the collection will be out of sych

This click add 1 million rows to 1 million rows in about 0.4 seconds.
Not just the time to add the rows - UI refreshed in 0.4 seconds.

If you scroll down before adding this will even bring the proper item into view.

private List<string> myString = new List<string>();
public IEnumerable<string> MyString { get { return myString; } }
private void click(object sender, RoutedEventArgs e)
{
    for (int j = 1000; j < 1000000; j++)
        myString.Add(j.ToString());
    lb.ItemsSource = null;
    lb.ItemsSource = MyString;
}
paparazzo
  • 44,497
  • 23
  • 105
  • 176
  • Thanks for taking the time to post this, but it doesn't really help. The issue is that I have a data source that is receiving large number of updates, and that data source is currently data bound to a ListBox, as part of an MVVM architected app. Once you take into account custom styling on the ListBox and a few other UI-related factors, adding a million items takes a considerable time, during which the UI appears to have hung. – paytools-steve Mar 17 '14 at 15:44
  • If adding the items is what is taking time then do that on a another thread. You stated the problem was the NotifyCollectionChanged events and this eliminates those event. – paparazzo Mar 17 '14 at 16:11
  • So it is string - it is a lot of strings. I tested add 1 million rows at a time and got 0.40 response time every time all the way to an out of memory exception. I suspect you have something that broke virtualization. Save a static set of rows and just add them. If response time is static then you have virtualization. If response got up with each static add then virtualizaion is broken. – paparazzo Mar 17 '14 at 18:54
  • yes, although it wasn't virtualisation that was broken, it was the overhead of multiple calls via the Dispatcher that caused the issue. – paytools-steve Mar 19 '14 at 18:51
  • Then why does the answer I posted not work. In your fist comment you said the problem was styling. You never actually tested this did you? – paparazzo Mar 19 '14 at 19:05
0

Further research, and some thinking about start-up behaviour turned up the answer to this problem.

At start-up, the app is able to read 200,000 items into the ListBox in a few seconds, so it should easily be able to sustain an update rate of 100 Adds per second. The fundamental issue was the involvement of the Dispatcher, as the creation of new items was taking place in a separate thread. So changing the code from this:

foreach (T item in items) {
   dispatcher.Invoke(DispatcherPriority.DataBind, (Action<T>)((item) => {
     Add(item); }), item);
}

To this:

dispatcher.Invoke(DispatcherPriority.DataBind, (Action<IEnumerable<T>>)((itms) => {
   foreach (T item in itms) { 
      Add(item); }
   }), items);

i.e., iterating over the items to add within the dispatcher rather than outside made a tremendous difference to performance, meaning that I didn't need to call the Reset action after all.

paytools-steve
  • 3,580
  • 3
  • 26
  • 21
  • 1
    I might disagree with the solution that you provided. What happens if the UI has a bunch of databinding that is bound to it that needs to show up before those collection that are being added? That would create a tremendous amount of performance hit for the Dispatcher thread. – 123 456 789 0 Mar 19 '14 at 19:01