具有定期通知支持的自定义ObservableCollection<T>或BindingList<T>

10

摘要

我有一个大型且不断变化的数据集,希望将其绑定到 UI(带分组的数据网格)。更改有两个层面:

  • 集合中频繁添加或删除项目(每个方向每秒500个)
  • 每个项目有4个属性,在其生命周期内最多更改5次

数据的特征如下:

  • 集合中约有 5000 个项目
  • 一个项目在一秒内可能被添加,然后进行 5 次属性更改,然后被删除。
  • 一个项目可能会在某个中间状态下保持一段时间,并应向用户显示。

我遇到问题的主要要求如下:

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

我希望做到以下几点:

  • 每隔 N 秒更新 UI
  • 只提高相关的 NotifyPropertyChanged 事件

如果项目 1 具有属性 State, 在间隔期间从 A -> B -> C -> D, 我希望仅引发一个 'State' 更改事件, 即 A->D。

我明白用户不需要在每秒更新 UI。如果一个项目在 UI 更新之间的 N 秒窗口内添加、更改状态并被删除,则它不应出现在数据网格中。

数据网格

数据网格是我用来显示数据的组件。我目前正在使用 XCeed DataGrid,因为它可以提供动态分组。如果可以提供一些动态分组选项(包括经常更改的属性),那么普通 DataGrid 将很好。

我的系统中瓶颈在于当项目属性更改时重新排序所需的时间

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

以不同方式表达问题

给定两个 BindingList / ObservableCollection 实例, 它们最初是相同的,但第一个列表此后已经有了一系列的附加更新(您可以监听), 生成将一个列表转换为另一个列表的最小更改集。

外部阅读材料

我需要类似 George Tryfonas 的 ArrayMonitor,但支持添加和删除项目(它们永远不会移动)。

注:如果有人能想到更好的摘要,请XCeed grid将单元格直接绑定到网格中的项,而排序和分组功能则受BindingList上引发的ListChangedEvents驱动。这略微有些反直觉,并且排除了下面的MonitoredBindingList,因为行会在分组之前更新。

相反,我包装了项目本身,捕获属性更改事件并将它们存储在HashSet中,就像Daniel建议的那样。这对我效果很好,我会定期遍历这些项目并要求它们通知任何更改。
这是我的一个绑定列表的尝试,可以轮询更新通知。它可能存在一些错误,因为最终对我没有用。
它创建了一个添加/删除事件队列,并通过列表跟踪更改。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++;
        }
    }
}
1个回答

5
我们在这里谈论两件事情:
1.集合的变化。这会触发事件INotifyCollectionChanged.CollectionChanged
2.项属性的变化。这会触发事件INotifyPropertyChanged.PropertyChanged
你的自定义集合需要实现接口INotifyCollectionChanged。你的项需要实现接口INotifyPropertyChanged。此外,PropertyChanged事件只告诉你项上哪个属性被更改了,但不知道之前的值。
这意味着,你的项需要有一个实现,大概像这样:
  • 每隔N秒运行一个计时器
  • 创建一个包含所有已更改属性名称的HashSet<string>。因为它是一个集合,每个属性只能包含一次或零次。
  • 当属性被更改时,如果其名称尚未在哈希集中,则将其添加到哈希集中。
  • 当计时器到期时,为哈希集中的所有属性引发PropertyChanged事件,并在此后清除哈希集。

您的集合将具有类似的实现。但它会更加困难,因为您需要考虑在两个计时器事件之间已添加和删除的项。这意味着当添加一个项时,您将其添加到"addedItems"哈希集中。如果删除一个项,则将其添加到"removedItems"哈希集中,如果它尚未在"addedItems"中。如果它已经在"addedItems"中,就从那里删除它。我想你明白了。

为了遵循关注点分离和单一职责原则,最好让您的项目以默认方式实现INotifyPropertyChanged并创建一个包装器来合并事件。这样做的好处是您的项目不会混杂着不属于它们的代码,并且此包装器可以被泛化并用于实现INotifyPropertyChanged的每个类。
集合也是同样的道理:您可以为所有实现INotifyCollectionChanged的集合创建一个通用包装器,并让包装器完成事件的合并。

嗨,丹尼尔 - 感谢您如此快速和深入的回复。我一直在尝试实现您描述的内容,尽管您纠正了我犯的一些错误。当我完成这个类后,我会在这里发布它,以便任何人都可以使用或调整它。不过我还有一个问题,在我的CustomObservableCollection中,当项目更改时应该引发哪个事件?或者我该如何将INotifyPropertyChanged传播到数据网格?NotifyCollectionChangedAction.Replace似乎不太对。 - CityView
@CityView:集合不负责通知数据网格其项目属性的更改任务。当您将集合绑定到数据网格时,数据网格会绑定到您的集合的CollectionChanged事件以及每个显示的单个项目的PropertyChanged事件。 - Daniel Hilgarth
@Daniel:有道理 - 这不适用于 XCeed Grid,这就是我感到困惑的原因。对于 Exceed,似乎需要扩展 BindingList。谢谢,我今天会发布我的尝试。 - CityView
@CityView:我明白了。在这种情况下,最好让您的自定义集合实现IBindingList<T>而不是INotifyCollectionChanged,并使其订阅添加到列表中的每个项目的PropertyChanged事件,然后为每个具有更改属性的项目引发ListChanged事件。 - Daniel Hilgarth
@Daniel:有一些琐事需要注意:在集合的实现中,当添加一个项目时,需要对已经移除的项目进行对称性检查,以确保其不存在于中。 - Vlad
@Vlad:我知道。我懒得把所有的组合都写下来,这就是为什么我写了“我想你明白了”。也许我应该更清楚地指出,这实际上意味着:“还有一些工作要做”;-) - Daniel Hilgarth

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