首页 文章

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

提问于
浏览
25

我正在构建一个应用程序,它使用许多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)之前,我的数据网格从workerthread更新时开始抛出InvalidOperationException . 异常消息:

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

Repro代码:

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

4 回答

  • 11

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

    它使用WPF可以访问和修改除创建集合之外的线程上的数据集合 . 这使您可以使用后台线程从外部源(如数据库)接收数据,并在UI线程上显示数据 . 通过使用另一个线程来修改集合,您的用户界面仍然可以响应用户交互 .

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

    如果要收集或修改大量数据,则可能需要使用后台线程来收集和修改数据,以便用户界面对输入保持反应 . 要使多个线程能够访问集合,请调用EnableCollectionSynchronization方法 . 当您调用EnableCollectionSynchronization(IEnumerable,Object)方法的此重载时,系统会在您访问它时锁定该集合 . 要指定自己锁定集合的回调,请调用EnableCollectionSynchronization(IEnumerable,Object,CollectionSynchronizationCallback)重载 .

    用法如下 . 创建一个对象,该对象用作集 Contract 步的锁 . 然后调用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

  • 0

    总结此主题,此 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 });
                }
            }
        }
    }
    
  • 6

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

    这适用于 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
    

    你不需要 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
    }
    

相关问题