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

31
我正在开发一个应用程序,其中使用了许多ItemControls(datagrid和listview)。为了方便地从后台线程更新这些列表,我使用了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.0升级到.NET 4.5之前,我的数据表格在从workerthread更新时就开始抛出InvalidOperationException异常。异常消息如下:

此异常被抛出,是因为控件“System.Windows.Controls.DataGrid Items.Count:5”的生成器收到了与Items集合的当前状态不符的CollectionChanged事件序列。检测到以下差异:累积计数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 Framework 团队。你能否将能够在 netfx45compat at Microsoft dot com 上重现该问题的项目发送给我们?我想要查看一下。真诚地,Varun Gupta - Varun
1
你在这个问题上有任何进展吗?我在我的Win8开发机上没有看到它,但是我有一个用户使用的是Win7和.NET 4.5,无法使用我的软件。我们正在尝试卸载4.5并转向4.0。 - Thomas
1
已验证:回滚修复了问题。 - Thomas
回滚并不是一个解决方案,特别是如果你想在Windows 8上运行应用程序(或者Windows 8是你的开发工作站)。 - Mattia Vitturi
大家好,我来自微软.NET Framework兼容性团队。如果您能够重现此问题,请通过netfx45compat at Microsoft dot com与我们联系。同时,请告诉我们您的现有应用程序是否受到此问题的影响,还是您正在构建新的应用程序。谢谢! - Varun
5个回答

47

WPF 4.5 提供了一些新功能,可以让你在非UI线程上访问集合。

WPF 允许你在创建集合的线程之外的其他线程上访问和修改数据集合。这允许你使用后台线程从外部源(如数据库)接收数据,并在UI线程上显示数据。通过使用另一个线程来修改集合,用户界面仍然对用户交互保持响应。

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

如果你需要收集或修改大量数据,你可能想要使用后台线程来收集和修改数据,以便用户界面对输入保持反应性。要允许多个线程访问一个集合,请调用 EnableCollectionSynchronization 方法。当你调用此方法的 EnableCollectionSynchronization(IEnumerable, Object) 重载时,系统会在访问它时锁定集合。要指定回调来自己锁定集合,请调用 EnableCollectionSynchronization(IEnumerable, Object, 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


你应该在后台线程中使用锁对象 -> lock(_syncLock) { Items.Add(item) } - DELUXEnized
@DELUXEnized 没有必要这样做,因为绑定操作会自动为我完成这个。 - Jehof
就我理解的这个方法来说,它只是告诉绑定系统在访问集合时使用哪个锁。但你仍然需要确保在后台线程中使用集合时对其进行加锁。那么集合如何知道它是从后台线程访问的呢? - DELUXEnized
@DELUXEnized 感谢您的信息。您关于 lock 语句的正确性是正确的。我会修复它。 - Jehof
2
我想到了一个问题,如果您在与UI线程不同的线程中实例化集合,则需要在UI线程上设置集合虚拟化。您可以使用类似于Application.Current.Dispatcher.Invoke(() => BindingOperations.EnableCollectionSynchronization(Items, _syncLock));的代码来完成。 - Filippo Vigani
我认为锁对象不应该是静态的,否则如果访问任何一个实例,你就会锁定所有实例(在这种情况下可能并不重要,因为你可能没有多个 MainWindow 实例)。 - H.B.

14
为了总结这个主题,这个 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 });
            }
        }
    }
}

我应该对OnPropertyChanged()进行相同的覆盖吗? - Felix
适用于4.6.1版本,百分之百可用。 - user1618054
1
将锁定设置为静态应该会在修改任何一个集合实例时锁定所有集合实例。我怀疑这是否必要或是一个好主意。 - H.B.

6

Jehof的回答是正确的。

我们还无法针对4.5版本进行目标设置,并且我们遇到了自定义可观察集合的问题,这些集合已经允许使用Dispatcher进行事件通知时进行后台更新。

如果有人发现有用的话,我在我们的应用程序中使用了以下代码,以便在执行环境为.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 });
    }
}

0
其他解决方案似乎有点过度,你可以使用委托来保持线程同步:
    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);
                   }
                }
            });                
    }

这应该可以很好地工作。


0

这是针对使用 VS 2017 发布版本的 Windows 10 版本 1607 用户可能遇到的问题。

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

你不需要使用 lockEnableCollectionSynchronization

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

这不是一个新问题,而是对所提出问题的答案,但是针对可能遇到该问题的Windows 10用户。 - Nasheayahu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接