Подтвердить что ты не робот

Как обновить ObservableCollection с помощью рабочего потока?

У меня есть ObservableCollection<A> a_collection; Коллекция содержит элементы "n". Каждый элемент A выглядит следующим образом:

public class A : INotifyPropertyChanged
{

    public ObservableCollection<B> b_subcollection;
    Thread m_worker;
}

В принципе, все это связано с представлением WPF + элементом управления представлением деталей, который показывает b_subcollection выбранного элемента в отдельном списке (2-сторонние привязки, обновления на propertychanged и т.д.). Проблема возникла для меня, когда я начал выполнять потоки. Вся идея состояла в том, чтобы весь a_collection использовал рабочий поток, чтобы "выполнить работу", а затем обновить их соответствующие b_subcollections и отобразить результаты в реальном времени в реальном времени.

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

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

Приветствия

4b9b3361

Ответ 1

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

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

Опубликовать Добавить вызовы в поток пользовательского интерфейса.

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. Это может быть для вас еще одним вариантом.

Ответ 2

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

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

EnableCollectionSynchronization выполняет две вещи:

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

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

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

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

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

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

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

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

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

Ответ 3

С .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)));

Ответ 4

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

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);
        }
    }
}