0

In a pursuit for more responsive way to update ListBox with large number of items I turned to Rx. This is my implementation of it:

    ObservableCollection<FileData> _fileCollection = new ObservableCollection<FileData>();
    public ObservableCollection<FileData> FileCollection { get { return _fileCollection; } }
    public static object _fileCollectionLock = new object();

    public Subject<FileData> subject = new Subject<FileData>();

    public MainWindow( )
    {
        InitializeComponent();
        BindingOperations.EnableCollectionSynchronization(_fileCollection, _fileCollectionLock);
        UpdateFileList(subject);
    }

    private void UpdateFileList(IObservable<FileData> sequence)
    {
        sequence.Subscribe(value=>_fileCollection.Add(value));
    }

    private void ListFiles(string fullpath)
    { 
        _fileCollection.Clear(); //crashed once
        Task.Factory.StartNew(() =>
        {
            DirectoryInfo info = new DirectoryInfo(fullpath);
            IEnumerable files = info.EnumerateFiles(Filter + "*", SearchOption.TopDirectoryOnly,true);
            foreach (FileInfo file in files)
            {
                ...
                FileData fd = new FileData(filename, filedate, filesize, fileext);      
                subject.OnNext(fd);

It happened once that in my code crashed on _fileCollection.Clear(); (Forgot the error, sorry). Do I need to use locks and where?

Daniel
  • 1,064
  • 2
  • 13
  • 30
  • Where are you calling ListFiles? – Michal Ciechan Apr 03 '14 at 15:54
  • @MichalCiechan I call it from folder click, from refresh button... I removed "async" keyword if that makes it confusing because it is not important here – Daniel Apr 03 '14 at 15:57
  • are you not always getting a cross thread access violation at _fileCollection.Add(value)? – Michal Ciechan Apr 03 '14 at 16:00
  • @MichalCiechan Not while using but now I tried calling Listfiles for several folders and got "System.ArgumentOutOfRangeException:Insertion index was out of range" without pointing me to the exception location. It does not happen every time even then. – Daniel Apr 03 '14 at 16:19

1 Answers1

2

There are several problems with your implementation, rooted in a misunderstanding of how to correctly use EnableCollectionSynchronization. To be honest though, I am not surprised - it is poorly documented and the design is not great (I suspect mainly due to having to make it work with WPF without breaking changes).

Correct usage of EnableCollectionSynchronization

I will briefly cover the correct usage:

EnableCollectionSynchronization registers a lock that WPF will use whenever it needs to access the collection (e.g. when controls enumerate it).

In .NET 4.5, collection change events raised on background threads (i.e. threads that are not the thread from where EnableCollectionSynchronization was called) are queued and marshalled to the UI thread anyway.

Importantly, when using this method you must still lock any access to the collection yourself using the same lock you registered with EnableCollectionSynchronization. In particular, there is nothing in your code preventing Clear() running concurrently with Add() and this is most certainly the cause of the exception you are seeing.

A good place to call EnableCollectionSynchronization is from a handler to the BindingOperations.CollectionRegistering event, which guarantees the call is made before any CollectionView instances are created.

Best Practice for updating UI bound collections

All that said, I believe that you should abandon this approach entirely - it is far better to instead undertake to only update UI bound collections on the UI thread. RX is great for this, you can use ObserveOn (see here for a good primer) to marshall updates to the UI thread. If you do this, you no longer need to worry about synchronizing access to the collection and your life will be much simpler.

My advice - do all the work of figuring out what updates are required on a background thread, but modify the collection on the UI thread itself. This approach is almost always fast enough, and if it isn't you probably need to think about your design.

Have at look at my answer here for a discussion and links to articles on updating on the UI.

Also have a look at this question for another discussion/analysis of issues with large numbers of changes happening on the UI thread, and how you can buffer huge numbers of changes to avoid hogging the dispatcher.

It's also worth looking at the ReactiveUI framework. See also here.

Community
  • 1
  • 1
James World
  • 29,019
  • 9
  • 86
  • 120
  • I understand the first part of the answer and see my problem. I looked through all linked code from the 2nd part and I am not sure yet that it will guarantee better performance. I ran some old version of the code and noticed that I did not get much benefit of Rx and TaskFactory in comparison to .Add(fd) everything on UI thread. Enumerating or GetFiles takes 0ms so I believe bottleneck is ListBox update after all items are added. I even suppressed NotifyPropertyChange during that time without any difference. All items have a lot of bindings to different data in FileData class so his might be it – Daniel Apr 04 '14 at 15:13
  • I would be very surprised if Rx made anything *faster*. It's adding code so it should make things slower. It's benefit is to make certain coding problems easier, not improve performance. It can help with the gymnastics required to keep UIs responsive when coordinating background tasks and make reacting to ux events easier. Certainly, if you can use simple code to achieve your goals without it, go for it! – James World Apr 04 '14 at 17:44
  • Thank you James! I have learned a lot from your post and fixed crashing by keeping everything on the UI thread. Improvising with await Task.Delay(1) before .Add(fd) in the loop prevents animations from stuttering. If you have better solution please let me know. – Daniel Apr 04 '14 at 19:34
  • 1
    My immediate reaction was that I didn't like this - it wouldn't be my natural choice... but if it works, why not? It's simple. What's its doing is giving WPF time in which to update the UI as a result of each change. Keep an eye on things. If it ever degrades you may want to look at posting back to the dispatcher (from the UI thread itself can be fine) in batches - just may sure that rather than queuing all the batches up in one go, you queue the *next* batch at the end of the current one. Rx can make this quite easy with the DispatcherScheduler from rx-xaml. – James World Apr 04 '14 at 20:33
  • I will look at that too, thanks. It checks before Delay if animation is running and since it is only up to 200ms at a time it does not affect overall performance and still makes everything smooth. I spent many days dealing with this issue and I can live with this. Cheers! – Daniel Apr 04 '14 at 21:09
  • This answer needs more up-votes. It's amazing to me that _using_ the lock is not mentioned anywhere in MSDN. – Charlie Jul 29 '14 at 02:56