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?
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.