使用 CollectionSynchronization 更新 ObservableCollection

Updating ObservableCollection with CollectionSynchronization

我正在根据 MVVM 构建 WPF 应用程序。 我的 ViewModel 中有一个 ObservableCollection,它被 Datagrid 使用。 我的应用程序需要检查远程资源是否有新增内容,当这些资源到达时,更新 GUI 并将项目存储在数据库中。

我这样做的方式是我有一个 ServiceLayer 检查新项目,将它们保存到数据库,并触发一个事件。我的 UI 订阅了这个事件并更新了 ObservableCollection。

问题是新项目一到达,更新 ObservableCollection 的尝试就触发了错误“这种类型的 CollectionView 不支持从与 Dispatcher 线程不同的线程更改其 SourceCollection”

我做了一些阅读并设法通过添加 CollectionSynchronization 锁解决了这个问题

这有效 - 但这是一个好的方法吗,即它会导致任何明显的问题吗?

在我的服务层

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

异常的发生是因为 thread affinity in WPF,这是设计使然。

Both the ItemsControl and the CollectionView have affinity to the thread on which the ItemsControl was created, meaning that using them on a different thread is forbidden and throws an exception.

当您的集合发生变化时,它会引发一个 CollectionChanged 事件,该事件将由绑定 ObservableCollection 的控件处理。然后,此控件将在同一线程上更新其项目。在您的示例中,这不是 UI 线程,而是您的工作线程。

绑定操作解决方案

您使用 BindingOperations.EnableCollectionSynchronization 的方法从 .NET Framework 4.5 开始可用并且完全有效,但您必须确保您选择的同步机制没有死锁,这可能很棘手。我建议您查看 reference 了解详细信息。

调度程序解决方案

解决此问题的更通用方法是将添加项目委托给 UI 线程。

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 来调用辅助 lambda 方法,该方法将您的项目添加到 UI 线程上的 _currentOffers 集合中。您可以按照代码中的注释执行此操作 synchronously or asynchronously

我已将您的循环替换为 Linq 查询,该查询有意生成可枚举的结果。这样做的好处是,您可以将 lambda 排队等待在 UI 线程上对所有项目执行一次,而不是对单个项目执行无数次,这样可以更有效地减少会显着降低性能的上下文切换。另一方面,添加大量项可能是一个很长的 运行 操作,它会阻塞 UI 线程,因此会冻结您的应用程序。实际上,这取决于实际工作量和添加项目的频率,以便为您的用例做出正确的选择。这也适用于机制本身,无论是调度程序还是绑定操作方法。