8

At present I have two WPF listboxes imitating the following functionality

Word 2007 customize screen
(source: psu.edu)

I am using 2 ObservableCollections to allow users to select whatever items they require (flexibility is the key here). The main issue is that I have thousands of items that are grouped in both listboxes. All in all the design works really well (with a few dozen items), but my stumbling block is when a user copies all the available items from the left to the right as the screen freezes (time to run on a different thread?).

Looking at ObservableCollection it lacks an AddRange method and there are various implementations available on the internet. I also know the CollectionChanged event is needlessly being fired as each item is copied over draining performance horribly.

It may well be that I have to allow users to choose from groups of over 10 000 items in the future, which sounds like a bad idea, but is non-negotiable as the grouping on the listbox (CollectionViewSource) works really well, but has the side effect of switching off the Virtualising of both the listboxes

What can I do to improve the performance when loading a listbox with thousands of items when databound to an ObservableCollection? Are there any AddRange type implementations that you would recommend? Is the only choice I have here to run this on a background thread which seems expensive because I am not loading data from a database?

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Vault
  • 201
  • 3
  • 6
  • See this http://stackoverflow.com/questions/1007691/observablecollection-databinding-performance – Sauron Sep 09 '09 at 09:32
  • Does this answer your question? [How to Avoid Firing ObservableCollection.CollectionChanged Multiple Times When Replacing All Elements Or Adding a Collection of Elements](https://stackoverflow.com/questions/13302933/how-to-avoid-firing-observablecollection-collectionchanged-multiple-times-when-r) – Sabito stands with Ukraine Oct 28 '20 at 15:51

4 Answers4

2

I have removed the CollectionViewSource and the grouping and the items are copied over in 1/2 a second, but with the grouping on it can take up to a minute because virtualisation does not work with the grouping.

I will need to decide whether to use the CollectionViewSource

Vault
  • 201
  • 3
  • 6
2

I couldn't resist answering this. I don't think you won't need this answer anymore, but maybe somebody else can use it.

Don't think too hard (do not approach this multithreaded (this will make things error-prone and unnecessary complicated. Only use threading for hard calculations/IO), all those different actiontypes will make it very difficult to buffer. The most annoying part is, that if you remove or add 10000 items your application (listboxes) will be very busy with handling the events raised by the ObservableCollection. The event already supports multiple items. So.....

You could buffer the items until it changes the action. So Add actions will be buffered and wil be raised as batch if the 'user' changes action or flushes it. Haven't test it, but you could do something like this:

// Written by JvanLangen
public class BufferedObservableCollection<T> : ObservableCollection<T>
{
    // the last action used
    public NotifyCollectionChangedAction? _lastAction = null;
    // the items to be buffered
    public List<T> _itemBuffer = new List<T>();

    // constructor registeres on the CollectionChanged
    public BufferedObservableCollection()
    {
        base.CollectionChanged += new NotifyCollectionChangedEventHandler(ObservableCollectionUpdate_CollectionChanged);
    }

    // When the collection changes, buffer the actions until the 'user' changes action or flushes it.
    // This will batch add and remove actions.
    private void ObservableCollectionUpdate_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // if we have a lastaction, check if it is changed and should be flush else only change the lastaction
        if (_lastAction.HasValue)
        {
            if (_lastAction != e.Action)
            {
                Flush();
                _lastAction = e.Action;
            }
        }
        else
            _lastAction = e.Action;

        _itemBuffer.AddRange(e.NewItems.Cast<T>());
    }

    // Raise the new event.
    protected void RaiseCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        if (this.CollectionChanged != null)
            CollectionChanged(sender, e);
    }

    // Don't forget to flush the list when your ready with your action or else the last actions will not be 'raised'
    public void Flush()
    {
        if (_lastAction.HasValue && (_itemBuffer.Count > 0))
        {
            RaiseCollectionChanged(this, new NotifyCollectionChangedEventArgs(_lastAction.Value, _itemBuffer));
            _itemBuffer.Clear();
            _lastAction = null;
        }
    }

    // new event
    public override event NotifyCollectionChangedEventHandler CollectionChanged;
}

Have fun!, J3R03N

  • This doesn't actually work. When you try to raise the collection changed event with multiple items, you'll get a NotSupportedException (Range actions are not supported). Suggest looking at other solutions such as:http://binarysculpting.com/2012/04/03/adding-many-entries-to-an-observable-collection-in-a-performance-friendly-way/, or http://peteohanlon.wordpress.com/2008/10/22/bulk-loading-in-observablecollection/, or http://stackoverflow.com/questions/670577/observablecollection-doesnt-support-addrange-method-so-i-get-notified-for-each. – Charlie Nov 26 '13 at 01:31
  • In addition to Jeroeon's comment, the ObservableCollection has an OnCollectionChanged method that you can override. You should use that instead of subscribing to the CollectionChanged event. – Seanba Mar 23 '18 at 16:30
1

You could probably inherit from ObservableCollection<T> (or directly implement INotifyCollectionChanged) to add BeginUpdate and EndUpdate methods. Changes made between calls to BeginUpdate and EndUpdate would be queued, then combined into one (or several if there are separate ranges) NotifyCollectionChangedEventArgs object that would be passed to the handlers of the CollectionChanged event when EndUpdate is called.

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 1
    As far as I know, WPF controls doesn't support range updates for collection, and throw an exception when they receive more than 1 item in one CollectionChanged event. – Tomáš Kafka Nov 26 '09 at 23:34
  • 1
    WTF ?! Why provide the ability to specify multiple items in the event args if they don't support it ? I had implemented the collection described in my answer, but hadn't have time to actually test it... I just did, and it seems you're right :(. So my collection can't be used for binding scenarios... – Thomas Levesque Nov 26 '09 at 23:58
1

You can find a Thread safe observable collection here. Make your Observable collection thread safe and bind it to listbox.

Sauron
  • 16,668
  • 41
  • 122
  • 174
  • That's the approach I'm using and it works pretty well. You can use a BackgroundWorker to fill your ObservableCollection and see your ListBox being populated on the fly. – japf Sep 09 '09 at 10:10
  • This approach is still throwing an unreasonably large number of events on the UI thread because each item added will throw its own collection changed event. This isn't going to fix the problem. – Jared Harding Jan 18 '13 at 19:35