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

Резюме

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

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

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

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

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

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

Что я хотел бы сделать,

  • Обновление пользовательского интерфейса только каждые N секунд
  • Поднимите только соответствующие NotifyPropertyChangedEvents

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

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

DataGrid

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

Узкое место в моей системе в настоящее время во время повторного сортировки при изменении свойств элемента

Это занимает 98% процессора в профиле пользователя MyKit.

Другой способ выражения вопроса

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

Внешнее чтение

Мне нужен эквивалент этого ArrayMonitor от George Tryfonas, но обобщенный для поддержки добавления и удаления элементов (они никогда не будут перемещены).

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

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

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

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

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

Ответ 1

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

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

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

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

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