31

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);
                }
            });                
    }
}
LasseBP
  • 326
  • 1
  • 3
  • 4
  • I am from Microsoft .NET Framework team. Could you send us project that reproduces the issue on netfx45compat at Microsoft dot com? I would like to take a look. Sincerely, Varun Gupta – Varun Jan 28 '13 at 07:06
  • 1
    Did you make any progress with this issue? I don't see it on my Win8 dev box, but I have a user who has Win7 and .NET 4.5 and is unable to use my software. We are trying to uninstall 4.5 and go to 4.0. – Thomas Feb 18 '13 at 20:00
  • 1
    Verified: rollback fixed the problem. – Thomas Feb 18 '13 at 20:32
  • Rollback is not a solution, specially if you want to run the application on Windows 8 (and/or Windows 8 is in your development workstation). – Mattia Vitturi May 03 '13 at 12:35
  • Hello All, I am from Microsoft .NET Framework compatibility team. Pls contact us on netfx45compat at Microsoft dot com if you are able to reproduce this problem. Also let us know if your existing app is affected by the issue or are you trying to build new app. Thanks! – Varun Aug 22 '13 at 01:06

5 Answers5

47

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

Jehof
  • 34,674
  • 10
  • 123
  • 155
  • You should also use the lock object in your background thread -> `lock(_syncLock) { Items.Add(item) }` – DELUXEnized Aug 27 '15 at 14:25
  • @DELUXEnized there is no need for that cause bindingoperations will do this automatically for me – Jehof Aug 28 '15 at 13:19
  • as far as I understood this method, it only tells the binding system which lock to use when accessing the collection. you still have to ensure that you lock the collection when you use it from a background thread. how should the collection know when it is accessed from a background thread? – DELUXEnized Aug 31 '15 at 08:25
  • @DELUXEnized thanks for you information. You are correct with the lock-statement. I will fix it – Jehof Aug 31 '15 at 10:15
  • 2
    It occured to me that if you instantiate the collection in a thread different from the UI Thread you NEED to set collection virtualization on the UI Thread. You can do that with something like `Application.Current.Dispatcher.Invoke(() => BindingOperations.EnableCollectionSynchronization(Items, _syncLock));` – Filippo Vigani Aug 24 '16 at 09:32
  • I do not think the lock object should be static, otherwise you lock all instances if any is accessed (in this case probably not that important because you might not have multiple `MainWindow` instances). – H.B. Oct 22 '19 at 10:49
14

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 });
            }
        }
    }
}
VahidN
  • 18,457
  • 8
  • 73
  • 117
6

The answer from Jehof is correct.

We cannot yet target 4.5 and had this issue with our custom observable collections that already allowed background updates (by using the Dispatcher during event notifications).

If anyone finds it useful, I have used the following code in our application that targets .NET 4.0 to enable it to use this functionality if the execution environment is .NET 4.5:

public static void EnableCollectionSynchronization(IEnumerable collection, object lockObject)
{
    // Equivalent to .NET 4.5:
    // BindingOperations.EnableCollectionSynchronization(collection, lockObject);
    MethodInfo method = typeof(BindingOperations).GetMethod("EnableCollectionSynchronization", new Type[] { typeof(IEnumerable), typeof(object) });
    if (method != null)
    {
        method.Invoke(null, new object[] { collection, lockObject });
    }
}
Chris
  • 3,081
  • 3
  • 32
  • 37
0

This is for Windows 10 Version 1607 users using the release version of VS 2017 that may have this issue.

Microsoft Visual Studio Community 2017
Version 15.1 (26403.3) Release
VisualStudio.15.Release/15.1.0+26403.3
Microsoft .NET Framework
Version 4.6.01586

You didn't need the lock nor EnableCollectionSynchronization.

<ListBox x:Name="FontFamilyListBox" SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}" Width="{Binding FontFamilyWidth, Mode=TwoWay}"
         SelectedItem="{Binding FontFamilyItem, Mode=TwoWay}"
         ItemsSource="{Binding FontFamilyItems}"
          diag:PresentationTraceSources.TraceLevel="High">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="typeData:FontFamilyItem">
            <Grid>
                <TextBlock Text="{Binding}" diag:PresentationTraceSources.TraceLevel="High"/>

            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

public ObservableCollection<string> fontFamilyItems;
public ObservableCollection<string> FontFamilyItems
{
    get { return fontFamilyItems; }
    set { SetProperty(ref fontFamilyItems, value, nameof(FontFamilyItems)); }
}

public string fontFamilyItem;
public string FontFamilyItem
{
    get { return fontFamilyItem; }
    set { SetProperty(ref fontFamilyItem, value, nameof(FontFamilyItem)); }
}

private List<string> GetItems()
{
    List<string> fonts = new List<string>();
    foreach (System.Windows.Media.FontFamily font in Fonts.SystemFontFamilies)
    {
        fonts.Add(font.Source);
        ....
        other stuff..
    }
    return fonts;
}

public async void OnFontFamilyViewLoaded(object sender, EventArgs e)
{
    DisposableFontFamilyViewLoaded.Dispose();
    Task<List<string>> getItemsTask = Task.Factory.StartNew(GetItems);

    try
    {
        foreach (string item in await getItemsTask)
        {
            FontFamilyItems.Add(item);
        }
    }
    catch (Exception x)
    {
        throw new Exception("Error - " + x.Message);
    }

    ...
    other stuff
}
Nasheayahu
  • 49
  • 8
  • This is not a new question, but and answer to the problem asked, but for Window 10 users that may come across this issue. – Nasheayahu Apr 17 '17 at 18:31
0

The other solutions seems a bit excessive, you could just use delegate to keep threads in sync:

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
            Task.Factory.StartNew(() =>
            {
                foreach (var item in Enumerable.Range(1, 500))
                {
                   App.Current.Dispatcher.Invoke((Action)delegate
                   {
                      Items.Add(item);
                   }
                }
            });                
    }

This should work just fine.

KGDI
  • 36
  • 3