Виртуализация WPF Wrap Panel Проблема - PullRequest
8 голосов
/ 28 июля 2010

Существует не так много вариантов виртуализированной панели переноса для использования в WPF.По той или иной причине MS решила не отправлять ее в стандартную библиотеку.

Если кто-то может быть настолько смелым, чтобы предоставить ответ (и объяснение) толпы на первый рабочий элемент в следующем проекте codeplex,Я был бы очень признателен:

http://virtualwrappanel.codeplex.com/workitem/1

Спасибо!


Краткое описание проблемы:

Я недавно пытался использовать виртуализациюWrappanel из этого проекта и обнаружили ошибку.

Шаги для воспроизведения:

  1. Создать список.
  2. Установить виртуализированную оболочку как элемент в шаблоне списка.
  3. Привязать источник элементовсписок для наблюдаемой коллекции.
  4. Удалить элемент из резервной наблюдаемой коллекции.

Сбой Debug.Assert (Debug.Assert (child == _children [childIndex], ""Был создан неправильный дочерний элемент ");) в MeasureOverride, и продолжение выполнения приводит к нулевому исключению в методе очистки [см. Прилагаемый снимок экрана].

Пожалуйста, дайте мне знать, если вы можете это исправить.

Спасибо,

AO


Код:

http://virtualwrappanel.codeplex.com/SourceControl/list/changesets#

альтернативный текст http://virtualwrappanel.codeplex.com/Project/Download/AttachmentDownload.ashx?ProjectName=virtualwrappanel&WorkItemId=1&FileAttachmentId=138959

Ответы [ 3 ]

8 голосов
/ 19 августа 2010

Объяснение проблемы

Вы попросили объяснить, что происходит, а также инструкции, как это исправить. Пока что никто не объяснил проблему. Я сделаю это.

В ListBox с VirtualizingWrapPanel есть пять отдельных структур данных, которые отслеживают элементы, каждая по-разному:

  1. ItemsSource: исходная коллекция (в данном случае ObservableCollection)
  2. CollectionView: хранит отдельный список отсортированных / отфильтрованных / сгруппированных элементов (только если используются какие-либо из этих функций)
  3. ItemContainerGenerator: отслеживает сопоставление между элементами и контейнерами
  4. InternalChildren: отслеживает контейнеры, которые в данный момент видны
  5. WrapPanelAbstraction: отслеживает, какие контейнеры появляются в какой строке

Когда элемент удаляется из ItemsSource, это удаление должно распространяться на все структуры данных. Вот как это работает:

  1. Вы вызываете Remove () на элементе ItemsSource
  2. ItemsSource удаляет элемент и запускает его CollectionChanged, который обрабатывается CollectionView
  3. CollectionView удаляет элемент (если используется сортировка / фильтрация / группировка) и запускает его CollectionChanged, который обрабатывается ItemContainerGenerator
  4. ItemContainerGenerator обновляет свое отображение, запускает его ItemsChanged, который обрабатывается VirtualizingPanel
  5. VirtualizingPanel вызывает свой виртуальный метод OnItemsChanged, который реализуется VirtualizingWrapPanel
  6. VirtualizingWrapPanel отбрасывает свою WrapPanelAbstraction, поэтому она будет построена, но никогда не обновляет InternalChildren

Из-за этого коллекция InternalChildren не синхронизирована с остальными четырьмя коллекциями, что приводит к ошибкам.

Решение проблемы

Чтобы устранить проблему, добавьте следующий код в метод OnItemsChanged VirtualizingWrapPanel:

switch(args.Action)
{ 
    case NotifyCollectionChangedAction.Remove: 
    case NotifyCollectionChangedAction.Replace: 
        RemoveInternalChildRange(args.Position.Index, args.ItemUICount); 
        break; 
    case NotifyCollectionChangedAction.Move: 
        RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount); 
        break; 
} 

Это обеспечивает синхронизацию коллекции InternalChildren с другими структурами данных.

Почему AddInternalChild / InsertInternalChild здесь не вызывается

Вы можете задаться вопросом, почему в приведенном выше коде нет вызовов InsertInternalChild или AddInternalChild, и особенно, почему при обработке Replace и Move не требуется добавлять новый элемент во время OnItemsChanged.

Ключ к пониманию этого в том, как работает ItemContainerGenerator.

Когда ItemContainerGenerator получает событие удаления, он обрабатывает все сразу:

  1. ItemContainerGenerator немедленно удаляет элемент из своих собственных структур данных
  2. ItemContainerGenerator запускает событие ItemChanged. Ожидается, что панель немедленно снимет контейнер.
  3. ItemContainerGenerator «освобождает» контейнер, удаляя его DataContext

С другой стороны, ItemContainerGenerator узнает, что элемент добавлен, все обычно откладывается:

  1. ItemContainerGenerator немедленно добавляет «слот» для элемента в его структуру данных, но не создает контейнер
  2. ItemContainerGenerator запускает событие ItemChanged. Панель вызывает InvalidateMeasure () [это делается базовым классом - вам не нужно это делать]
  3. Позже, когда вызывается MeasureOverride, Generator.StartAt / MoveNext используется для генерации контейнеров элементов. Все вновь созданные контейнеры добавляются в InternalChildren в это время.

Таким образом, все удаления из коллекции InternalChildren (включая те, которые являются частью Move или Replace) должны выполняться внутри OnItemsChanged, но добавления могут (и должны) откладываться до следующего MeasureOverride.

4 голосов
/ 18 августа 2010

Метод OnItemsChanged должен правильно обрабатывать параметры args. Пожалуйста, смотрите этот вопрос для получения дополнительной информации. Скопировав код из этого вопроса, вам нужно обновить OnItemsChanged следующим образом:

protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args) {
    base.OnItemsChanged(sender, args);
    _abstractPanel = null;
    ResetScrollInfo();

    // ...ADD THIS...
    switch (args.Action) {
        case NotifyCollectionChangedAction.Remove:
        case NotifyCollectionChangedAction.Replace:
            RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
            break;
        case NotifyCollectionChangedAction.Move:
            RemoveInternalChildRange(args.OldPosition.Index, args.ItemUICount);
            break;
    }
}
0 голосов
/ 18 августа 2010

Во-первых, имейте в виду, что в целом, если вы удаляете объект из коллекции и у вас нет его ссылки, этот объект мертв в момент удаления. Поэтому, по крайней мере, вызов RemoveInternalChildRange является недопустимым после удаления, но это не основная проблема.

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

В-третьих, проверьте на нулевое значение после:

UIElement child = _generator.GenerateNext(out newlyRealized) as UIElement;

и для первой пробной версии измените код, чтобы получить корректный выход, что в данном случае означает, что gracefull continue - необходимо использовать для цикла и приращений в цикле, чтобы вообще иметь возможность продолжить.

Также проверьте InternalChildren, когда вы видите, что null, чтобы увидеть, дает ли этот путь доступа тот же результат, что и ваши _children (как в размере, внутренние данные, null в том же месте).

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

Кроме того, опубликуйте полностью компилируемый пример проекта, который дает репродукцию (в виде zip-файла) где-нибудь - сокращает случайные предположения и позволяет ppl просто собрать / запустить и посмотреть.

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

...