升级到.net 4.5: ItemsControl与其项源不一致

本文关键字:不一致 ItemsControl net | 更新日期: 2023-09-27 17:53:42

我正在构建一个应用程序,它使用了许多itemcontrol(数据网格和列表视图)。为了方便地从后台线程更新这些列表,我使用了这个扩展到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)之前,当从工作线程更新时,我的数据网格开始抛出InvalidOperationException。异常信息:

由于控件"System.Windows.Controls.DataGrid Items"的生成器抛出此异常。Count:5' with name '(未命名)'已接收到一系列CollectionChanged事件,这些事件与Items集合的当前状态不一致。检测到以下差异:累计计数4与实际计数5不同。[累计计数为(上次重置计数+自上次重置以来的#添加- #删除)]

再生产代码: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);
                }
            });                
    }
}

升级到.net 4.5: ItemsControl与其项源不一致

WPF 4.5提供了一些在非ui线程上访问集合的新功能。

WPF允许您访问和修改线程上的数据集合除了创建收藏的那个。这使您能够使用后台线程从外部源接收数据,例如作为数据库,并在UI线程上显示数据。通过使用另一个线程来修改集合,则用户界面保持不变响应用户交互

这可以通过在BindingOperations类上使用静态方法EnableCollectionSynchronization来完成。

如果您有很多数据要收集或修改,您可能希望使用一个后台线程来收集和修改数据,以便用户界面将对输入保持响应。启用多个线程来访问集合时,调用EnableCollectionSynchronization方法。当你调用这个重载的EnableCollectionSynchronization(IEnumerable, Object)方法当您访问集合时,系统将其锁定。要指定回调要自己锁定集合,请调用EnableCollectionSynchronization (IEnumerable,对象,CollectionSynchronizationCallback)过载。

用法如下。创建一个对象,该对象用作集合同步的锁。然后调用BindingsOperations的EnableCollectionSynchronization方法,并将要同步的集合和用于锁定的对象传递给它。

我已经更新了你的代码并添加了细节。我还将集合更改为正常的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

总结一下这个主题,这个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's .NET 4.5
                method.Invoke(null, new object[] { collection, lockObject });
            }
        }
    }
}

Jehof的答案是正确的。

我们还没有达到4.5的目标,并且我们的自定义可观察集合已经允许后台更新了(通过在事件通知期间使用Dispatcher)。

如果有人觉得有用,我已经在我们的应用程序中使用了以下代码,目标是。net 4.0,如果执行环境是。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 });
    }
}

这是针对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
}

其他解决方案似乎有点过分,您可以使用委托来保持线程同步:

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