自定义ObservableCollection <T>或BindingList <T>,支持定期通知

Cit*_*iew 10 c# wpf large-data-volumes bindinglist observablecollection

摘要

我有一个快速变化的大型数据集,我希望将其绑定到UI(带有分组的Datagrid).这些变化分为两个层次;

  • 经常在集合中添加或删除项目(单程500秒)
  • 每个项目都有4个属性,在其生命周期中最多可更改5次

数据的特征如下;

  • 该系列共有约5​​000件商品
  • 可以在一秒内添加项目,然后进行5次属性更改,然后将其删除.
  • 项目也可能暂时保持某种临时状态,并应显示给用户.

我遇到问题的关键要求;

  • 用户应该能够通过对象上的任何属性对数据集进行排序

我想做什么;

  • N秒更新一次UI
  • 仅引发相关的NotifyPropertyChangedEvents

如果第1项具有属性状态,该状态从A - > B - > C - > D移动到我需要/仅想要引发一个"状态"改变事件的区间中,A-> D.

我感谢用户不需要每秒更新数千次UI.如果添加了一个项目,其状态已更改并在UI更新之间的N秒窗口内全部删除,则它永远不会访问DataGrid.

数据网格

DataGrid是我用来显示数据的组件.我目前正在使用XCeed DataGrid,因为它提供了简单的动态分组.我没有在情感上投入其中,如果我可以提供一些动态分组选项(其中包括经常更改的属性),那么股票DataGrid会很好.

我的系统中的瓶颈目前是在项目属性发生变化时重新排序所花费的时间

这占用了YourKit Profiler中98%的CPU.

用不同的方式来表达问题

给定两个BindingList/ObservableCollection实例,这些实例最初相同,但第一个列表已经有一系列额外的更新(您可以监听),生成最小的更改集以将一个列表转换为另一个列表.

外部阅读

我需要的是George Tryfonas 的这个ArrayMonitor的等价物,但是通用它来支持添加和删除项目(它们永远不会被移动).

NB我真的很感谢有人编辑问题的标题,如果他们能想到更好的总结.

编辑 - 我的解决方案

XCeed网格将单元格直接绑定到网格中的项目,而排序和分组功能由BindingList上引发的ListChangedEvents驱动.这有点直观,并排除了下面的MontioredBindingList,因为行将在组之前更新.

相反,我自己包装项目,捕获属性更改事件并将其存储在HashSet中,如Daniel建议的那样.这对我很有用,我会定期迭代这些项目并要求他们通知任何更改.

MonitoredBindingList.cs

这是我对绑定列表的尝试,可以轮询更新通知.可能有一些错误,因为它最终对我没用.

它创建一个添加/删除事件队列,并通过列表跟踪更改.ChangeList与基础列表具有相同的顺序,因此在我们通知添加/删除操作后,您可以针对正确的索引引发更改.

/// <summary>
///  A binding list which allows change events to be polled rather than pushed.
/// </summary>
[Serializable]

public class MonitoredBindingList<T> : BindingList<T>
{
    private readonly object publishingLock = new object();

    private readonly Queue<ListChangedEventArgs> addRemoveQueue;
    private readonly LinkedList<HashSet<PropertyDescriptor>> changeList;
    private readonly Dictionary<int, LinkedListNode<HashSet<PropertyDescriptor>>> changeListDict;

    public MonitoredBindingList()
    {
        this.addRemoveQueue = new Queue<ListChangedEventArgs>();
        this.changeList = new LinkedList<HashSet<PropertyDescriptor>>();
        this.changeListDict = new Dictionary<int, LinkedListNode<HashSet<PropertyDescriptor>>>();
    }

    protected override void OnListChanged(ListChangedEventArgs e)
    {
        lock (publishingLock)
        {
            switch (e.ListChangedType)
            {
                case ListChangedType.ItemAdded:
                    if (e.NewIndex != Count - 1)
                        throw new ApplicationException("Items may only be added to the end of the list");

                    // Queue this event for notification
                    addRemoveQueue.Enqueue(e);

                    // Add an empty change node for the new entry
                    changeListDict[e.NewIndex] = changeList.AddLast(new HashSet<PropertyDescriptor>());
                    break;

                case ListChangedType.ItemDeleted:
                    addRemoveQueue.Enqueue(e);

                    // Remove all changes for this item
                    changeList.Remove(changeListDict[e.NewIndex]);
                    for (int i = e.NewIndex; i < Count; i++)
                    {
                        changeListDict[i] = changeListDict[i + 1];
                    }

                    if (Count > 0)
                        changeListDict.Remove(Count);
                    break;

                case ListChangedType.ItemChanged:
                    changeListDict[e.NewIndex].Value.Add(e.PropertyDescriptor);
                    break;
                default:
                    base.OnListChanged(e);
                    break;
            }
        }
    }

    public void PublishChanges()
    {
        lock (publishingLock)
            Publish();
    }

    internal void Publish()
    {
        while(addRemoveQueue.Count != 0)
        {
            base.OnListChanged(addRemoveQueue.Dequeue());
        }

        // The order of the entries in the changeList matches that of the items in 'this'
        int i = 0;
        foreach (var changesForItem in changeList)
        {
            foreach (var pd in changesForItem)
            {
                var lc = new ListChangedEventArgs(ListChangedType.ItemChanged, i, pd);
                base.OnListChanged(lc);
            }
            i++;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Dan*_*rth 5

我们在这里讨论两件事:

  1. 对集合的更改.这会引发事件INotifyCollectionChanged.CollectionChanged
  2. 项目属性的更改.这会引发事件INotifyPropertyChanged.PropertyChanged

界面INotifyCollectionChanged需要由您的自定义集合实现.界面INotifyPropertyChanged需要由您的项目实现.此外,该PropertyChanged事件仅告诉您项目上的哪个属性已更改,而不是之前的值.
这意味着,您的项目需要具有如下所示的实现:

  • 有一个每N秒运行一次的计时器
  • 创建一个HashSet<string>包含已更改的所有属性的名称.因为它是一个集合,所以每个属性只能包含一次或零次.
  • 更改属性时,将其名称添加到哈希集中(如果它尚未包含在哈希集中).
  • 计时器过去后,PropertyChanged为哈希集中的所有属性引发事件,然后将其清除.

您的收藏将具有类似的实施.然而,这有点困难,因为您需要考虑在计时器事件之间添加和删除的项目.这意味着,添加项目时,您可以将其添加到哈希集"addedItems"中.如果删除了某个项,则将其添加到"removedItems"哈希集(如果它尚未位于"addedItems"中).如果它已经在"addedItems"中,请从那里删除它.我想你明白了.

为了遵循关注点分离和单一责任的原则,INotifyPropertyChanged以默认方式实现项目并创建包含事件合并的包装器会更好.这样做的好处是,您的项目不会混杂不属于那里的代码,并且这个包装器可以是通用的,并用于实现的每个类INotifyPropertyChanged.
集合也是如此:您可以为所有实现的集合创建一个通用包装器INotifyCollectionChanged,让包装器对事件进行整合.