Search code examples
c#wpfcollectionviewsource

ObservableCollection created on a Thread Pool Thread does not filter


The application I am working on is is based on WPF and the MVVMLightToolkit. I know I am not providing you a mcve but the whole application is really complex and it is hard to provide such an example. I hope someone will be able to help with the whole picture.

In that application, one action takes a lot of time (initialization stuff), so I run it in a task to not freeze the UI:

public class MainViewModel : ViewModelBase
{
    public ICommand HeavyActionCommand {get; private set;}
    public MainViewModel()
    {
        this.HeavyActionCommand = new RelayCommand(this.HeavyAction);
    }
    private async void HeavyAction()
    {
        var subViewModel = new SubViewModel();
        await Task.Run(async () => await subViewModel.ActualHeavyAction());
    }
}

If I do not wrap the ActualHeavyAction in the Task.Run method, the UI freeze. Doing this, as far as I understand, the ActualHeavyAction is not ran on the UI thread but on a Thread Pool Thread (correct me if I am wrong).

Among other things ActualHeavyAction initializes an ObservableCollection that I need to filter regarding to some user live inputs (in the following class, the property UserInput is bound to a TextBox). I had something like:

public class SubViewModel: ViewModelBase
{
    private _userInput;
    public string UserInput
    {
        get { return _userInput; }
        set
        {
            if (_userInput != value)
            {
                _userInput = value;
                this.RaisePropertyChanged();

                // Run the filter on the collection when the user enters new inputs
                CollectionViewSource.GetDefaultView(this.MyCollection).Refresh();
            }
        }
    }

    public ObservableCollection MyCollection {get; private set;}

    public async Task ActualHeavyAction()
    {
        /// lots of heavy stuff

        var myCollection = await _context.Objects.GetCollectionAsync();
        this.MyCollection = new ObservableCollection(myCollection);

        this.RaisePropertyChanged(nameof(this.MyCollection));

        CollectionViewSource.GetDefaultView(this.MyCollection).Filter = MyFilter;

        /// some other heavy stuff
    }

    public bool MyFilter(object obj)
    {
        // Blah blah blah
    }
}

Until here, I do not have any trouble. The problem occures later, when another action, ran on the UI Thread modify that collection. I get the recurrent:

This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread

To fix it, I try to add the .NET 4.5 EnableCollectionSynchronization feature:

BindingOperations.EnableCollectionSynchronization(this.MyCollection, lockObject); // lockObject is a static new object() defined in the SubViewModel class

I try to add it right after and just before the call to: CollectionViewSource.GetDefaultView

Doing so, I do not get the exception when I modify MyCollection, but calling Refresh() on the CollectionView does not run the MyFilter method (the exact same code work on other ViewModels that are not initialized on a Thread Pool Thread).

Do you have any idea of what's wrong with my code?


Solution

  • The BindingOperations.EnableCollectionSynchronization method should be called on the UI thread. So you need to create the ObservableCollection and call this method on the UI thread before you try to access the collection from the background thread.

    But the only method that should be called on a background thread in your ActualHeavyAction() is the GetCollectionAsync() method.

    Once the task that calls this method completes, you could create the ObservableCollection and apply the filter back on the UI thread. Or just returned an already filtered list from the task.

    Filtering an ICollectionView using the Filter property is a flexible but quite slow operation so if your source collection contains a lot of items, this might not be the best option to implement filtering.