Пользовательская коллекция ObservableCollection <T>или BindingList <T>с поддержкой периодических уведомлений - PullRequest
10 голосов
/ 15 марта 2011

Резюме

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

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

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

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

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

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

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

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

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

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

DataGrid

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

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

Это занимает 98% процессорного времени в YourKit Profiler.

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

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

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

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

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

РЕДАКТИРОВАТЬ - Мое решение

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

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

MonitoredBindingList.cs

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

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

/// <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 голосов
/ 15 марта 2011

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

  1. Изменения в коллекции. Это вызывает событие INotifyCollectionChanged.CollectionChanged
  2. Изменения в свойствах предметов. Это вызывает событие INotifyPropertyChanged.PropertyChanged

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

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

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

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

...