将项添加到在后台线程

时间:2015-12-28 20:02:18

标签: c# wpf mvvm observablecollection

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

当我尝试将项添加到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
  • 的工作线程

我试图将我的ViewModel与我的视图分开,所以我不希望在我的视图模型中引用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");
            }
        }        
    }
}

MainWindow.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();
        }
    }
}

MainWindow.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>

3 个答案:

答案 0 :(得分:1)

我认为这里的问题是你使用System.Timers.Timer。这将触发非UI线程。相反,请考虑使用将在UI线程上触发的DispatcherTimer

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

答案 1 :(得分:0)

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

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

答案 2 :(得分:0)

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