2015-12-28 3 views
2

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

Я продолжаю получать исключение, когда пытаюсь добавить элементы в ObservableCollection, потому что я добавляю их из другого фонового потока после выполнения некоторой обработки. Это NotSupportedException, говоря Additional information: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Вот темы я использую:

  • Thread А нить WPF UI
  • Thread B является рабочий поток используется для создания ViewModels (и ObservableCollection)
  • Thread с другой рабочий поток, который добавляет элементы в ObservableCollection

Я пытаюсь держать мои ViewModels separat из моих представлений, поэтому я не хочу иметь ссылки на диспетчер WPF внутри своих моделей просмотра, поэтому я не могу (не хочу) использовать Dispatcher.Invoke.

Я попытался использовать BindingOperations.EnableCollectionSynchronization, но, похоже, работает только тогда, когда он вызывается из потока пользовательского интерфейса WPF.

Как я могу синхронизировать доступ к моему 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> 
+1

[Асинхронное программирование: шаблоны для асинхронных приложений MVVM: привязка данных] (https://msdn.microsoft.com/magazine/dn605875.aspx) статья может вам помочь. –

ответ

1

Я думаю проблема заключается в том, что вы используете System.Timers.Timer. Это будет срабатывать в потоке, отличном от UI. Вместо этого рассмотрите возможность использования DispatcherTimer, который будет запускаться в потоке пользовательского интерфейса.

Если это всего лишь пример кода, чтобы выделить проблему, то в вашем реальном коде используйте Dispatcher.BeginInvoke, чтобы выполнить добавление в потоке пользовательского интерфейса.

+0

Да, я использовал 'System.Timers.Timer' как пример для иллюстрации проблемы. Кроме того, я бы предпочел не иметь ссылок на диспетчер интерфейса в моей модели ViewModel, потому что ViewModel не должен привязываться к какому-либо виду.Есть ли другой способ преодолеть эту проблему? – Ove

+0

Я не вижу преимущества в том, чтобы избежать использования 'Application.Current.Dispatcher' из вашей модели представления. Вы должны отсылать из фоновых потоков в поток пользовательского интерфейса где-нибудь, и если вы напрямую привязываетесь к модели представления, тогда это невозможно сделать в представлении. –

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

Я хотел бы оставить свои модели просмотра отдельно от своих просмотров, поэтому я не хочу использовать Dispatcher.Invoke. Я ищу решение, которое не загрязняет мои viewmodels. – Ove

0

Использование System.Windows.Threading.DispatcherTimer исправит вас проблему. Но я рекомендую переписать свой код и использовать операции async на основе задач для вашей долгосрочной задачи инициализации. Что именно вы хотите решить, используя таймер?

+0

Я использую таймер, чтобы проиллюстрировать проблему. В моем реальном приложении элементы добавляются из потока рабочего фона. Если я использую операции async на основе задач для моей задачи с длительным запуском init, модели просмотра все равно будут создаваться в отдельном потоке, и проблема все равно будет там. – Ove

+0

Это может, но вы можете вернуть список своих объектов из рабочего метода, а затем добавить эти новые элементы в ObservableCollection либо с помощью ожидания, либо с помощью задачи UITask = task.ContinueWith (() => { // добавьте свой пункты здесь }, TaskScheduler.FromCurrentSynchronizationContext()); –

Смежные вопросы