Пользовательские ObservableCollection или BindingList с поддержкой периодических уведомлений

резюме

У меня есть большой быстро меняющийся набор данных, который я хочу привязать к UI (Datagrid с группировкой). Изменения происходят на двух уровнях;

  • элементы часто добавляются или удаляются из коллекции (500 в секунду в каждую сторону)
  • каждый элемент имеет 4 свойства, которые будут меняться до 5 раз в своей жизни

характеристики данных следующие;

  • есть ~5000 предметы в коллекции
  • элемент может быть добавлен в течение секунды, затем иметь 5 изменений свойств, а затем удален.
  • элемент также может оставаться в некотором промежуточном состоянии в течение некоторого времени и должен отображаться пользователю.

ключевое требование, с которым у меня возникают проблемы;

  • пользователь должен иметь возможность сортировки данных по любому свойству объекта

что я хотел бы сделай;

  • обновить UI только каждый N секунд
  • поднять только соответствующие NotifyPropertyChangedEvents

если элемент 1 имеет состояние свойства, которое перемещение из A - > B - > C - > D в интервал мне нужно / нужно только одно изменение состояния события, в->д.

Я ценю, что пользователю не нужно обновлять пользовательский интерфейс тысячи раз в секунду. если элемент добавлен, его состояние изменилось и удаляется все в окне N секунд между обновлениями пользовательского интерфейса, он никогда не должен попадать в DataGrid.

DataGrid с

DataGrid-это компонент, который я использую для отображения данных. В настоящее время я использую xCeeD DataGrid, поскольку он обеспечивает динамическую группировку тривиально. Я не эмоционально инвестировал в него, фондовый DataGrid был бы в порядке, если бы я мог предоставить некоторые параметры динамической группировки (которые включают свойства, которые изменяются часто.)

узкое место в моей системе в настоящее время в время, необходимое для повторной сортировки когда свойства элемента изменяются

это занимает 98% процессора в Профилировщике YourKit.

другой способ сформулировать вопрос

учитывая два экземпляра BindingList / ObservableCollection которые изначально были идентичны, но первый список с тех пор имел серию дополнительные обновления (которые вы можете слушайте), сгенерируйте минимальный набор изменений, чтобы превратить один список в другой.

Внешнее Значение

то, что мне нужно, эквивалентно этому ArrayMonitor Джордж Трифонас, но обобщенный для поддержки добавления и удаления элементов (они никогда не будут перемещены).

NB я был бы очень признателен, если бы кто-то редактировал название вопроса, если бы они могли подумать о лучшем резюме.

EDIT-мое решение

сетка XCeed связывает ячейки непосредственно с элементами сетки, в то время как функция сортировки и группировки управляется listchangedevents, поднятыми в BindingList. Это немного противоречит интуиции и исключает MontioredBindingList ниже, поскольку строки будут обновляться перед группами.

вместо этого я обертываю сами элементы, ловя измененные события свойства и сохраняя их в Для поиска HashSet как Даниил предложил. Это хорошо работает для меня, я периодически перебираю элементы и прошу их уведомлять о любых изменениях.

MonitoredBindingList.cs

вот моя попытка списка привязки, который может быть опрошен для уведомлений об обновлении. Есть, вероятно, некоторые ошибки с ним, как это не было полезно для меня в конце концов.

он создает очередь событий добавления / удаления и отслеживает изменения через список. Список изменений имеет тот же порядок, что и базовый список, чтобы после уведомления об операциях добавления/удаления вы могли вызвать изменения по правильному индексу.

/// <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 ответов


мы говорим здесь о двух вещах:

  1. изменения в коллекции. Это вызывает событие INotifyCollectionChanged.Collectionchanged в
  2. изменения свойств элементов. Это вызывает событие INotifyPropertyChanged.PropertyChanged, как

интерфейс INotifyCollectionChanged должен быть реализован вашей пользовательской коллекцией. Интерфейс INotifyPropertyChanged должен быть реализован на ваши вопросы. Кроме того,PropertyChanged событие только сообщает вам, какое свойство было изменено на элементе, но не то, что было предыдущим значением.
Это означает, что ваши элементы должны иметь реализацию, которая идет примерно так:

  • есть таймер, который запускается каждые N секунд
  • создать HashSet<string>, содержащий имена всех свойств, которые были изменены. Поскольку это набор, каждое свойство может содержать только один или ноль раз.
  • при изменении свойства добавьте его имя в хэш-набор, если оно еще не находится в нем.
  • когда таймер истекает, поднять PropertyChanged событие для всех свойств в хэш-наборе и очистите его после этого.

ваша коллекция будет иметь аналогичную реализацию. Однако это немного сложнее, ведь нужно учитывать элементы, которые были добавлены и удалены между событиями таймера. Это означает, что при добавлении элемента необходимо добавить это к хэш-набору "addedItems". Если элемент удален, вы добавляете его в хэш-набор" removedItems", если он еще не находится в"addedItems". Если он уже в "addedItems", удалить его оттуда. Думаю, вы поняли.

чтобы придерживаться принципа разделения забот и единой ответственности, было бы еще лучше, чтобы ваши элементы реализовали INotifyPropertyChanged по умолчанию и создайте оболочку, которая выполняет консолидацию событий. Это имеет то преимущество, что ваши товары не загромождены кодом, который там не принадлежит, и эта оболочка может быть сделана универсальной и использоваться для каждого класса, который реализует INotifyPropertyChanged.
То же самое касается коллекции: вы можете создать универсальную оболочку для всех коллекций, которые реализуют INotifyCollectionChanged и пусть обертка выполняет консолидацию событий.