Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add items to ObservableCollection that was created on a background thread

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:

  • Thread A is the WPF UI thread
  • Thread B is a worker thread used to create the viewmodels (and the ObservableCollection)
  • Thread C is another worker thread that adds items to the ObservableCollection

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>
like image 437
Ove Avatar asked Nov 27 '25 22:11

Ove


1 Answers

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.

like image 99
Drew Noakes Avatar answered Nov 29 '25 10:11

Drew Noakes



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!