自定义 ObservableCollection或 BindingList支持定期通知
摘要
我有一个快速变化的大型数据集,我希望将其绑定到 UI(带分组的数据网格)。这些变化有两个层面:
- 项目经常从集合中添加或删除(每次 500 个)
- 每个项目有 4 个属性,这些属性在其生命周期中最多会更改 5 次
数据的特征如下;
- 集合中有大约 5000 个项目
- ,一个项目可能在一秒钟内被添加,然后有 5 个属性更改,然后被删除。
- 项目也可能会保持某种临时状态一段时间,并且应该显示给用户。
我遇到问题的关键要求;
- 用户应该能够通过对象上的任何属性对数据集进行排序
我想要做什么;
- 仅每 N 秒更新一次 UI
- 仅引发相关的 NotifyPropertyChangedEvents
如果第 1 项有一个属性 State, 从 A 移动 -> B-> C-> D 在 我只需要/想要一次“状态”更改 要引发的事件,A->D。
我很欣赏用户不需要每秒更新数千次用户界面。如果在 UI 更新之间的 N 秒窗口内添加一个项目、更改其状态并删除所有项目,则它永远不会访问 DataGrid。
DataGrid
DataGrid 是我用来显示数据的组件。我目前正在使用 XCeed DataGrid,因为它提供了简单的动态分组。我并没有对它投入感情,如果我可以提供一些动态分组选项(其中包括经常更改的属性),那么库存 DataGrid 就很好了。
我的系统的瓶颈是 目前正处于重新排序所需的时间 当项目的属性发生变化时
这会占用 YourKit Profiler 中 98% 的 CPU。
用不同的方式表达问题
给定两个 BindingList / ObservableCollection 实例 最初是相同的但是 第一份清单此后有一系列 其他更新(您可以 听),生成最小集 将一个列表变成 其他。
外部阅读
我需要的是这个ArrayMonitor 由 George Tryfonas 编写,但被概括为支持添加和删除项目(它们永远不会被移动)。
注意,如果有人能想出更好的摘要,我将非常感谢编辑问题标题的人。
编辑 - 我的解决方案
XCeed 网格将单元格直接绑定到中的项目网格而排序和分组功能由 BindingList 上引发的 ListChangedEvents 驱动。这有点违反直觉,并且排除了下面的 MontioredBindingList,因为行将在组之前更新。
相反,我将项目本身包装起来,捕获属性更改事件并将它们存储在丹尼尔建议的哈希集中。这对我来说效果很好,我定期迭代这些项目并要求他们通知任何更改。
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++;
}
}
}
Summary
I have a large an rapidly changing dataset which I wish to bind to a UI (Datagrid with grouping). The changes are on two levels;
- Items are frequently added or removed from the collection (500 a second each way)
- Each item has a 4 properties which will change up to 5 times in its lifetime
The characteristics of the data are as follows;
- There are ~5000 items in the collection
- An item may, within a second, be added then have 5 property changes and then be removed.
- An item may also remain in some interim state for a while and should be displayed to the user.
The key requirement which I'm having problems with;
- The user should be able to sort the dataset by any property on the object
What I would like to do;
- Update the UI only every N seconds
- Raise only the relevant NotifyPropertyChangedEvents
If item 1 has a property State which
moves from A -> B -> C -> D in the
interval I need/want only one 'State' change
event to be raised, A->D.
I appreciate a user doesn't need to have the UI updated thousands of times a second. if an item is added, has its state changed and is removed all within the window of N seconds between UI updates it should never hit the DataGrid.
DataGrid
The DataGrid is the component which I am using to display the data. I am currently using the XCeed DataGrid as it provides dynamic grouping trivially. I am not emotionally invested in it, the stock DataGrid would be fine if I could provide some dynamic grouping options (Which includes the properties which change frequently).
The bottleneck in my system is
currently in the time taken to re-sort
when an item's properties change
This takes 98% of CPU in the YourKit Profiler.
A different way to phrase the question
Given two BindingList / ObservableCollection instances
which were initially identical but
the first list has since had a series of
additional updates (which you can
listen for), generate the minimal set
of changes to turn one list into the
other.
External Reading
What I need is an equivalent of this ArrayMonitor by George Tryfonas but generalized to support adding and removing of items (they will never be moved).
NB I would really appreciate someone editing the title of the question if they can think of a better summary.
EDIT - My Solution
The XCeed grid binds the cells directly to the items in the grid whereas the sorting & grouping functionality is driven by the ListChangedEvents raised on the BindingList. This is slightly counter intuitive and ruled out the MontioredBindingList below as the rows would update before the groups.
Instead I wrap the items themselves, catching the Property changed events and storing them in a HashSet as Daniel suggested. This works well for me, I periodically iterate over the items and ask them to notify of any changes.
MonitoredBindingList.cs
Here is my attempt at a binding list which can be polled for update notifications. There are likely some bugs with it as it was not useful to me in the end.
It creates a queue of Add/Remove events and keeps track of changes via a list. The ChangeList has the same order as the underlying list so that after we've notified of the add/remove operations you can raise the changes against the right index.
/// <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++;
}
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
我们在这里讨论两件事:
接口
INotifyCollectionChanged
需要由您的自定义集合实现。接口INotifyPropertyChanged
需要由您的项目实现。此外,PropertyChanged
事件仅告诉您项目的哪个属性已更改,但不告诉您之前的值是什么。这意味着,您的项目需要有一个如下所示的实现:
HashSet
已更改的属性。因为它是一个集合,所以每个属性只能包含一次或零次。PropertyChanged
事件,然后将其清除。您的收藏将有类似的实现。然而,这有点困难,因为您需要考虑在计时器事件之间添加和删除的项目。这意味着,当添加一个项目时,您会将其添加到哈希集“addedItems”中。如果某个项目被删除,则将其添加到“removedItems”哈希集中(如果它尚未在“addedItems”中)。如果它已经在“addedItems”中,请将其从那里删除。我想你明白了。
为了遵守关注点分离和单一职责的原则,最好让您的项目以默认方式实现 INotifyPropertyChanged 并创建一个用于合并事件的包装器。这样做的优点是您的项目不会被不属于该项目的代码弄乱,并且该包装器可以变得通用并用于实现
INotifyPropertyChanged
的每个类。集合也是如此:您可以为所有实现 INotifyCollectionChanged 的集合创建一个通用包装器,并让包装器对事件进行合并。
We are talking about two things here:
The interface
INotifyCollectionChanged
needs to be implemented by your custom collection. The interfaceINotifyPropertyChanged
needs to be implemented by your items. Furthermore, thePropertyChanged
event only tells you which property was changed on an item but not what was the previous value.This means, your items need to have a implementation that goes something like this:
HashSet<string>
that contains the names of all properties that have been changed. Because it is a set, each property can only be contained one or zero times.PropertyChanged
event for all properties in the hash set and clear it afterwards.Your collection would have a similar implementation. It is however a little bit harder, because you need to account for items that have been added and deleted between to timer events. This means, when an item is added, you would add it to a hash set "addedItems". If an item is removed, you add it to a "removedItems" hash set, if it is not already in "addedItems". If it is already in "addedItems", remove it from there. I think you get the picture.
To adhere to the principle of separation of concerns and single responsibility, it would be even better to have your items implement
INotifyPropertyChanged
in the default way and create a wrapper that does the consolidation of the events. That has the advantage that your items are not cluttered with code that doesn't belong there and this wrapper can be made generic and used for every class that implementsINotifyPropertyChanged
.The same goes for the collection: You can create a generic wrapper for all collections that implement
INotifyCollectionChanged
and let the wrapper do the consolidation of the events.