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

问题描述 投票:0回答:6

我正在构建一个应用程序,它使用许多 ItemControls(数据网格和列表视图)。为了轻松地从后台线程更新这些列表,我使用了 ObservableCollections 的扩展,效果很好:

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

今天我安装了VS12(VS12又安装了.NET 4.5),因为我想使用为.NET 4.5编写的组件。在将我的项目升级到 .NET 4.5(从 4.0)之前,我的数据网格在从工作线程更新时就开始抛出 InvalidOperationException。异常消息:

抛出此异常是因为名称为“(unnamed)”的控件“System.Windows.Controls.DataGrid Items.Count:5”的生成器已收到与 Items 集合的当前状态不一致的 CollectionChanged 事件序列。 检测到以下差异: 累计计数 4 与实际计数 5 不同。[累计计数为(上次重置时的计数 + #Adds - 自上次重置后的 #Removes)。]

重现代码:

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);
                }
            });                
    }
}
c# wpfdatagrid .net-4.5
6个回答
47
投票

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

WPF 使您能够访问和修改线程上的数据集合 除了创建该集合的人之外。这使您能够 使用后台线程从外部源接收数据,例如 作为数据库,并在UI线程上显示数据。通过使用另一个 线程修改集合,您的用户界面仍然存在 响应用户交互。

这可以通过使用 BindingOperations 类上的静态方法

EnableCollectionSynchronization
来完成。

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


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

7
投票

Jehof 的回答是正确的。

我们还不能以 4.5 为目标,并且我们的自定义可观察集合存在此问题,这些集合已经允许后台更新(通过在事件通知期间使用调度程序)。

如果有人觉得它有用,我在面向 .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 });
    }
}

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

您不需要 lock 也不需要 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
}

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
投票

我根据@KGDI的答案准备了这样的扩展。 也许有人会发现在某个地方使用它很方便。 例如,我在自写测试中发现了它的用途

    public static class CommonExtensions
    {
        public static void RunMethod(this Type type, string name, params object[] args)
            => _runMethod(null, type, name, args);

        public static void RunMethod(this object obj, string name, params object[] args)
            => _runMethod(obj, obj.GetType(), name, args);

        private static void _runMethod(object obj, Type type, string name, params object[] args)
        {
            Type[] types = new Type[args.Length];
            for (int i = 0; i < args.Length; i++)
                types[i] = args[i].GetType();

            MethodInfo method = type.GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic, null, CallingConventions.Any, types, null);

            if (method != null)
                method.Invoke(obj, args);
            else
                throw new Exception($"Method of type {type.FullName} not found");
        }
    }
© www.soinside.com 2019 - 2024. All rights reserved.