Как мне обновить ObservableCollection через рабочий поток? - PullRequest
65 голосов
/ 19 января 2010

У меня есть ObservableCollection<A> a_collection; Коллекция содержит 'n' предметов. Каждый предмет А выглядит так:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

По сути, все это связано с представлением списка WPF + элементом управления подробным представлением, который показывает b_subcollection выбранного элемента в отдельном представлении списка (двухсторонние привязки, обновления на измененных свойства и т. Д.). Проблема появилась для меня, когда я начал реализовывать потоки. Идея заключалась в том, чтобы вся a_collection использовала свой рабочий поток для «выполнения работы», а затем обновляла соответствующие им b_subcollections и отображала результаты в реальном времени.

Когда я попробовал это, я получил исключение, в котором говорилось, что только поток Dispatcher может изменить ObservableCollection, и работа остановилась.

Может кто-нибудь объяснить проблему и как ее обойти?

Приветствия

Ответы [ 4 ]

107 голосов
/ 30 января 2013

Новая опция для .NET 4.5

Начиная с .NET 4.5 существует встроенный механизм для автоматической синхронизации доступа к коллекции и отправки событий CollectionChanged в поток пользовательского интерфейса. Чтобы включить эту функцию, вам нужно позвонить BindingOperations.EnableCollectionSynchronization из вашего потока пользовательского интерфейса .

EnableCollectionSynchronization делает две вещи:

  1. Запоминает поток, из которого он вызывается, и заставляет конвейер привязки данных маршалировать CollectionChanged события в этом потоке.
  2. Получает блокировку коллекции до тех пор, пока не будет обработано маршализованное событие, поэтому обработчики событий, выполняющие поток пользовательского интерфейса, не будут пытаться прочитать коллекцию, пока она изменяется из фонового потока.

Очень важно, это не обо всем позаботится : чтобы обеспечить потокобезопасный доступ к по своей сути не поточно-безопасной коллекции вы должны сотрудничать с платформой, приобретая та же самая блокировка от фоновых тем, когда коллекция собирается измениться.

Поэтому для правильной работы необходимо выполнить следующие шаги:

1. Решите, какой тип блокировки вы будете использовать

Это определит, какую перегрузку EnableCollectionSynchronization следует использовать. Большую часть времени будет достаточно простого оператора lock, поэтому эта перегрузка является стандартным выбором, но если вы используете какой-то необычный механизм синхронизации, то также существует поддержка для пользовательских блокировок .

2. Создайте коллекцию и включите синхронизацию

В зависимости от выбранного механизма блокировки вызовите соответствующую перегрузку в потоке пользовательского интерфейса . Если вы используете стандартный оператор lock, вам нужно предоставить объект блокировки в качестве аргумента. При использовании пользовательской синхронизации необходимо предоставить делегат CollectionSynchronizationCallback и объект контекста (который может быть null). При вызове этот делегат должен получить пользовательскую блокировку, вызвать переданную ему Action и снять блокировку перед возвратом.

3. Сотрудничайте, блокируя коллекцию перед ее изменением

Вы также должны заблокировать коллекцию, используя тот же механизм, когда собираетесь изменить ее самостоятельно; сделайте это с lock() для того же объекта блокировки, переданного в EnableCollectionSynchronization в простом сценарии, или с тем же механизмом пользовательской синхронизации в настраиваемом сценарии.

63 голосов
/ 19 января 2010

Технически проблема не в том, что вы обновляете ObservableCollection из фонового потока.Проблема заключается в том, что при этом коллекция вызывает событие CollectionChanged в том же потоке, который вызвал изменение - это означает, что элементы управления обновляются из фонового потока.

Для заполнения коллекции из фонаВ то время как элементы управления привязаны к нему, вам, вероятно, придется создать собственный тип коллекции с нуля, чтобы решить эту проблему.Хотя есть более простой вариант, который может сработать для вас.

Публикация вызовов Add в потоке пользовательского интерфейса.

public static void AddOnUI<T>(this ICollection<T> collection, T item) {
    Action<T> addMethod = collection.Add;
    Application.Current.Dispatcher.BeginInvoke( addMethod, item );
}

...

b_subcollection.AddOnUI(new B());

Этот метод будет возвращен немедленно (до того, как элемент фактически добавитсязатем) в потоке пользовательского интерфейса, элемент будет добавлен в коллекцию, и все должны быть счастливы.активностьБолее эффективное решение будет объединять несколько элементов и периодически публиковать их в потоке пользовательского интерфейса, чтобы не вызывать потоки для каждого элемента.

Класс BackgroundWorker реализует шаблонэто позволяет вам сообщать о прогрессе с помощью метода ReportProgress во время фоновой операции.Ход выполнения сообщается в потоке пользовательского интерфейса через событие ProgressChanged.Это может быть другой вариант для вас.

15 голосов
/ 22 января 2015

С .NET 4.0 вы можете использовать эти однострочные:

.Add

Application.Current.Dispatcher.BeginInvoke(new Action(() => this.MyObservableCollection.Add(myItem)));

.Remove

Application.Current.Dispatcher.BeginInvoke(new Func<bool>(() => this.MyObservableCollection.Remove(myItem)));
2 голосов
/ 17 августа 2018

Коллекция кодов синхронизации для потомков. При этом используется простой механизм блокировки для включения синхронизации коллекции. Обратите внимание, что вам нужно включить синхронизацию коллекции в потоке пользовательского интерфейса.

public class MainVm
{
    private ObservableCollection<MiniVm> _collectionOfObjects;
    private readonly object _collectionOfObjectsSync = new object();

    public MainVm()
    {

        _collectionOfObjects = new ObservableCollection<MiniVm>();
        // Collection Sync should be enabled from the UI thread. Rest of the collection access can be done on any thread
        Application.Current.Dispatcher.BeginInvoke(new Action(() => 
        { BindingOperations.EnableCollectionSynchronization(_collectionOfObjects, _collectionOfObjectsSync); }));
    }

    /// <summary>
    /// A different thread can access the collection through this method
    /// </summary>
    /// <param name="newMiniVm">The new mini vm to add to observable collection</param>
    private void AddMiniVm(MiniVm newMiniVm)
    {
        lock (_collectionOfObjectsSync)
        {
            _collectionOfObjects.Insert(0, newMiniVm);
        }
    }
}
...