Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Upgrading to .NET 4.5: An ItemsControl is inconsistent with its items source

I'm building an application, which uses many ItemControls(datagrids and listviews). In order to easily update these lists from background threads I used this extension to ObservableCollections, which has worked fine:

http://geekswithblogs.net/NewThingsILearned/archive/2008/01/16/have-worker-thread-update-observablecollection-that-is-bound-to-a.aspx

Today I installed VS12(which in turn installed .NET 4.5), as I want to use a component which is written for .NET 4.5. Before even upgrading my project to .NET 4.5 (from 4.0), my datagrid started throwing InvalidOperationException when updated from a workerthread. Exception message:

This exception was thrown because the generator for control 'System.Windows.Controls.DataGrid Items.Count:5' with name '(unnamed)' has received sequence of CollectionChanged events that do not agree with the current state of the Items collection. The following differences were detected: Accumulated count 4 is different from actual count 5. [Accumulated count is (Count at last Reset + #Adds - #Removes since last Reset).]

Repro code:

XAML:

<Window x:Class="Test1.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>       <DataGrid ItemsSource="{Binding Items, Mode=OneTime}" PresentationTraceSources.TraceLevel="High"/>           </Grid> </Window> 

Code:

public partial class MainWindow : Window {     public ExtendedObservableCollection<int> Items { get; private set; }      public MainWindow()     {         InitializeComponent();         Items = new ExtendedObservableCollection<int>();         DataContext = this;         Loaded += MainWindow_Loaded;     }      void MainWindow_Loaded(object sender, RoutedEventArgs e)     {             Task.Factory.StartNew(() =>             {                 foreach (var item in Enumerable.Range(1, 500))                 {                     Items.Add(item);                 }             });                     } } 
like image 969
LasseBP Avatar asked Jan 15 '13 11:01

LasseBP


2 Answers

WPF 4.5 provides some new functionality to access collections on non-UI Threads.

It WPF enables you to access and modify data collections on threads other than the one that created the collection. This enables you to use a background thread to receive data from an external source, such as a database, and display the data on the UI thread. By using another thread to modify the collection, your user interface remains responsive to user interaction.

This can be done by using the static method EnableCollectionSynchronization on the BindingOperations class.

If you have a lot of data to collect or modify, you might want to use a background thread to collect and modify the data so that the user interface will remain reactive to input. To enable multiple threads to access a collection, call the EnableCollectionSynchronization method. When you call this overload of the EnableCollectionSynchronization(IEnumerable, Object) method, the system locks the collection when you access it. To specify a callback to lock the collection yourself, call the EnableCollectionSynchronization(IEnumerable, Object, CollectionSynchronizationCallback) overload.

The usage is as follows. Create an object that is used as a lock for the synchronization of the collection. Then call the EnableCollectionSynchronization method of the BindingsOperations and pass to it the collection you want to synchronize and the object that is used for locking.

I have updated your code and added the details. Also i changed the collection to the normal ObservableCollection to avoid conflicts.

public partial class MainWindow : Window{   public ObservableCollection<int> Items { get; private set; }    //lock object for synchronization;   private static object _syncLock = new object();    public MainWindow()   {     InitializeComponent();     Items = new ObservableCollection<int>();      //Enable the cross acces to this collection elsewhere     BindingOperations.EnableCollectionSynchronization(Items, _syncLock);      DataContext = this;     Loaded += MainWindow_Loaded;   }    void MainWindow_Loaded(object sender, RoutedEventArgs e)   {         Task.Factory.StartNew(() =>         {             foreach (var item in Enumerable.Range(1, 500))             {                 lock(_syncLock) {                   Items.Add(item);                 }             }         });                   } } 

See also: http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux

like image 154
Jehof Avatar answered Sep 24 '22 12:09

Jehof


To summarize this topic, this AsyncObservableCollection works with .NET 4 and .NET 4.5 WPF apps.

using System; using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; using System.Windows.Data; using System.Windows.Threading;  namespace WpfAsyncCollection {     public class AsyncObservableCollection<T> : ObservableCollection<T>     {         public override event NotifyCollectionChangedEventHandler CollectionChanged;         private static object _syncLock = new object();          public AsyncObservableCollection()         {             enableCollectionSynchronization(this, _syncLock);         }          protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)         {             using (BlockReentrancy())             {                 var eh = CollectionChanged;                 if (eh == null) return;                  var dispatcher = (from NotifyCollectionChangedEventHandler nh in eh.GetInvocationList()                                   let dpo = nh.Target as DispatcherObject                                   where dpo != null                                   select dpo.Dispatcher).FirstOrDefault();                  if (dispatcher != null && dispatcher.CheckAccess() == false)                 {                     dispatcher.Invoke(DispatcherPriority.DataBind, (Action)(() => OnCollectionChanged(e)));                 }                 else                 {                     foreach (NotifyCollectionChangedEventHandler nh in eh.GetInvocationList())                         nh.Invoke(this, e);                 }             }         }          private static void enableCollectionSynchronization(IEnumerable collection, object lockObject)         {             var method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization",                                      new Type[] { typeof(IEnumerable), typeof(object) });             if (method != null)             {                 // It's .NET 4.5                 method.Invoke(null, new object[] { collection, lockObject });             }         }     } } 
like image 25
VahidN Avatar answered Sep 23 '22 12:09

VahidN