Обновление ObservableCollection с помощью CollectionSynchronization - PullRequest
0 голосов
/ 09 июля 2020

Я создаю приложение WPF в соответствии с MVVM. У меня есть ObservableCollection в моей ViewModel, которая используется Datagrid. Мое приложение должно проверять удаленный ресурс на наличие новых дополнений, и когда они поступают, обновлять GUI и сохранять элементы в базе данных.

Я делаю это так: у меня есть ServiceLayer, который проверяет наличие новые элементы, сохраняет их в базу данных и запускает событие. Мой пользовательский интерфейс подписывается на это событие и обновляет ObservableCollection.

Проблема заключалась в том, что при появлении новых элементов попытка обновить ObservableCollection вызвала ошибку "Этот тип CollectionView не поддерживает изменения в своей SourceCollection из поток отличается от потока диспетчера "

Я немного прочитал и мне удалось исправить эту проблему, добавив блокировку CollectionSynchronization

Это работает - но это хороший подход, т.е. есть ли какие-либо очевидные проблемы, которые это вызовет?

В моем ServiceLayer

 public async void CheckForNewOffers_Tick(object sender, ElapsedEventArgs e)
    {
        var webOffers = await _webAccess.GetLatestOffers();
        var newOffers = new Collection<Offer>();

        //CheckIfOffersAreNew
        foreach(Offer o in webOffers)
        {
            if(!_currentOffers.Any(co=>co.Id == o.Id))
            {
                _currentOffers.Add(o);
                newOffers.Add(o);
            }
        }

        //Any new offers - save them in DB and signal event
        if(newOffers.Count>0)
        {
            _data.StoreNewOffers(newOffers);
            OnReceivingNewOffers(newOffers);
        }
    }



    public event EventHandler<ICollection<Offer>> ReceivedNewOffers;

    protected virtual void OnReceivingNewOffers(ICollection<Offer> newOffers)
    {
        ReceivedNewOffers?.Invoke(this, newOffers);
    }

}

И в моем UI

_itemsLock = new object();
            BindingOperations.EnableCollectionSynchronization(_currentOffers, _itemsLock);

public void HandleNewOffers(object sender, ICollection<Offer> newOffers)
    {
        lock (_itemsLock)
        {
            //Implement code to add newOffers to ObservableCollection
            foreach (Offer no in newOffers)
            {
                if (!_currentOffers.Any(co => co.Id == no.Id))
                {
                    _currentOffers.Add(MapOffer(no));
                }
            }
        }
    }

1 Ответ

2 голосов
/ 09 июля 2020

Исключение возникает из-за сродства потока в WPF , что является преднамеренным.

И ItemsControl, и CollectionView имеют сходство с потоком, в котором был создан ItemsControl , что означает, что их использование в другом потоке запрещено и вызывает исключение.

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

Решение операций привязки

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

Диспетчерское решение

Более общий способ решения этой проблемы - делегировать добавление элементов в поток пользовательского интерфейса .

public void HandleNewOffers(object sender, ICollection<Offer> newOffers)
{
   // The same as you do, but simplified with Linq to return a filtered enumerable
   var filteredOffers = newOffers.Where(no => !_currentOffers.Any(co => co.Id == no.Id)).Select(MapOffer);

   // Use this to invoke the add method synchronously on the UI thread
   System.Windows.Application.Current.Dispatcher.Invoke(() => AddToCurrentOffers(filteredOffers));

   // Use this to alternatively invoke the add method asynchronously on the UI thread
   System.Windows.Application.Current.Dispatcher.InvokeAsync(() => AddToCurrentOffers(filteredOffers));
}

// Just a helper method that can also be inlined as lambda
private void AddToCurrentOffers(IEnumerable<Offer> offers)
{
   foreach (var offer in offers)
   {
      _currentOffers.Add(offer);
   }
}

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

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

...