Обновление до .NET 4.5: элемент ItemsControl несовместим со своим источником элементов

Я создаю приложение, которое использует много ItemControls (datagrids и listviews). Чтобы легко обновить эти списки из фоновых потоков, я использовал это расширение для ObservableCollections, которое отлично работало:

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

Сегодня я установил VS12 (который, в свою очередь, установил .NET 4.5), поскольку я хочу использовать компонент, написанный для .NET 4.5. Прежде чем даже обновить мой проект до .NET 4.5 (начиная с версии 4.0), мой datagrid начал бросать InvalidOperationException при обновлении из workthread. Сообщение об исключении:

Это исключение было выбрано потому, что генератор для управления "System.Windows.Controls.DataGrid Items.Count: 5" с именем "(неназванный)" получил последовательность событий CollectionChanged, которые не согласуются с текущим состоянием элементов коллекция. Были обнаружены следующие отличия:   Накопленный счет 4 отличается от фактического счета 5. [Накопленный счет (счетчик Reset + #Adds - #Removes с последнего Reset).]

Код репрограммы:

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>

код:

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);
                }
            });                
    }
}

Ответ 1

WPF 4.5 предоставляет некоторые новые функции для доступа к коллекциям для потоков, отличных от UI.

Это WPF позволяет вам получать доступ и изменять коллекции данных по потокам кроме той, которая создала коллекцию. Это позволяет вам использовать фоновый поток для приема данных из внешнего источника, например в качестве базы данных и отображения данных в потоке пользовательского интерфейса. Используя другое нить для изменения коллекции, ваш пользовательский интерфейс остается реагируя на взаимодействие пользователя.

Это можно сделать, используя статический метод EnableCollectionSynchronization в классе BindingOperations.

Если у вас есть много данных для сбора или изменения, вы можете использовать фоновый поток для сбора и изменения данных, чтобы пользователь интерфейс останется реактивным для ввода. Чтобы включить несколько потоков для доступ к коллекции, вызовите метод EnableCollectionSynchronization. Когда вы вызываете эту перегрузку Метод EnableCollectionSynchronization (IEnumerable, Object), система блокирует сбор при доступе к нему. Чтобы указать обратный вызов для блокировки коллекции, вызовите EnableCollectionSynchronization (IEnumerable, Object, CollectionSynchronizationCallback).

Использование выглядит следующим образом. Создайте объект, который используется как блокировка для синхронизации коллекции. Затем вызовите метод EnableCollectionSynchronization для BindingsOperations и передайте ему коллекцию, которую вы хотите синхронизировать, и объект, который используется для блокировки.

Я обновил ваш код и добавил подробности. Также я изменил коллекцию на обычный ObservableCollection, чтобы избежать конфликтов.

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);
                }
            }
        });                
  }
}

Смотрите также: http://10rem.net/blog/2012/01/20/wpf-45-cross-thread-collection-synchronization-redux

Ответ 2

Подводя итог этой теме, этот AsyncObservableCollection работает с приложениями .NET 4 и .NET 4.5 WPF.

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 .NET 4.5
                method.Invoke(null, new object[] { collection, lockObject });
            }
        }
    }
}

Ответ 3

Ответ от Иегофа прав.

Мы пока не можем настроить таргетинг на 4.5, и эта проблема связана с нашими пользовательскими наблюдаемыми коллекциями, которые уже допускали фоновое обновление (с помощью диспетчера во время уведомлений о событиях).

Если кто-то считает это полезным, я использовал следующий код в нашем приложении, предназначенный для .NET 4.0, чтобы он мог использовать эту функцию, если среда выполнения .NET.NET:

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 });
    }
}

Ответ 4

Это для пользователей Windows 10 Version 1607, использующих версию выпуска VS 2017, которая может иметь эту проблему.

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

Вам не нужна блокировка или 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
}