In my application, the ViewModels take some time to initialize so I create them on a background thread on startup in order to keep the main window responsive.
I keep getting an exception when I try to add items to an ObservableCollection because I'm adding them from another background thread after some processing is done. It's a NotSupportedException, saying Additional information: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.
Here are the threads I am using:
I am trying to keep my ViewModels separated from my views, so I don't want to have references to the WPF Dispatcher inside my viewmodels, so I can't (don't want to) use Dispatcher.Invoke.
I tried to use BindingOperations.EnableCollectionSynchronization, but it seems that only works when it's called from the WPF UI thread.
How can I synchronize access to my ObservableCollection so I can add items to it from a background thread? Is there an easy, elegant way to do so? Preferably without using third-party libraries?
I created a small example to illustrate the problem:
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>
I think the problem here is that you're using a System.Timers.Timer. That will fire on a non-UI thread. Instead, consider using a DispatcherTimer which will fire on the UI thread.
If however this is just example code to highlight the problem, then in your real code use Dispatcher.BeginInvoke in order to perform the addition on the UI thread.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With