8
votes

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?

4

4 Answers

2
votes

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

2
votes

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

1
votes

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.

1
votes

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