将项添加到在后台线程上创建的ObservableCollection中

本文关键字:创建 ObservableCollection 线程 后台 添加 | 更新日期: 2023-09-27 18:28:14

在我的应用程序中,ViewModels需要一些时间来初始化,所以我在启动时在后台线程上创建它们,以保持主窗口的响应。

当我尝试将项目添加到ObservableCollection时,我一直会遇到异常,因为我在完成一些处理后从另一个后台线程添加它们。它是NotSupportedException,表示Additional information: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

以下是我正在使用的线程:

  • 线程A是WPF UI线程
  • 线程B是一个工作线程,用于创建视图模型(以及ObservableCollection)
  • 线程C是另一个向ObservableCollection添加项的工作线程

我试图将ViewModels与视图分离,因此我不希望在视图模型中引用WPF Dispatcher,因此我不能(不希望)使用Dispatcher.Invoke

我尝试使用BindingOperations.EnableCollectionSynchronization,但似乎只有在从WPF UI线程调用它时才有效。

如何同步对ObservableCollection的访问,以便从后台线程向其添加项目?有没有一种简单、优雅的方法可以做到这一点?最好不使用第三方库?

我创建了一个小例子来说明这个问题:

AppViewModel.cs:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
using System.Windows.Data;
using System.Windows.Input;
namespace WpfObservableTest
{
    public class AppViewModel
    {
        private System.Timers.Timer _timer;
        public ObservableCollection<string> Items { get; private set; }
        private object _itemsLock = new object();
        public AppViewModel()
        {
            Console.WriteLine("AppViewModel ctor thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
            Items = new ObservableCollection<string>();
            BindingOperations.EnableCollectionSynchronization(Items, _itemsLock);
            _timer = new Timer(500);
            _timer.Elapsed += _timer_Elapsed;
        }
        public void StartTimer()
        {
            _timer.Start();
        }
        private void _timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            Console.WriteLine("TimerElapsed thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
            lock (_itemsLock)
            {
                Items.Add("Test");
            }
        }        
    }
}

主窗口.xaml.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfObservableTest
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private AppViewModel _viewModel;
        public MainWindow()
        {
            Task initTask = Task.Run(() => { _viewModel = new AppViewModel(); });
            initTask.Wait();
            DataContext = _viewModel;
            InitializeComponent();
        }
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            _viewModel.StartTimer();
        }
    }
}

主窗口.xaml:

<Window x:Class="WpfObservableTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Button Click="Button_Click" Grid.Row="0">Start</Button>
        <ListView ItemsSource="{Binding Items}" Grid.Row="1">
        </ListView>
    </Grid>
</Window>

将项添加到在后台线程上创建的ObservableCollection中

我认为这里的问题是您使用的是System.Timers.Timer。这将在非UI线程上激发。相反,可以考虑使用DispatcherTimer,它将在UI线程上激发。

然而,如果这只是突出问题的示例代码,那么在实际代码中使用Dispatcher.BeginInvoke在UI线程上执行添加。

我认为您可以将计时器中的代码重写为

            private void _timer_Elapsed(object sender, ElapsedEventArgs e)
            {
            Console.WriteLine("TimerElapsed thread id: " + System.Threading.Thread.CurrentThread.ManagedThreadId);
            lock (_itemsLock)
            {    
              System.Windows.Application.Current.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal,(Action)delegate()
                  {
                     Items.Add("Test");       
                  });
            }
        }

使用System.Windows.Threading.DispatcherTimer将解决您的问题。但我建议您重写代码,并对长期运行的初始化任务使用基于任务的异步操作。你到底想通过使用计时器来解决什么问题?