Виртуализация проблемы панели WPF Wrap

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

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

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

спасибо!


резюме вопрос:

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

воспроизведение:

  1. создать списке.
  2. установить виртуализации wrappanel как itemhost в шаблоне listboxpanel.
  3. привязать itemsource listbox к наблюдаемой коллекции.
  4. удалите элемент из резервной наблюдаемой коллекции.

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

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

спасибо,

АО


код:

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

alt текст http://virtualwrappanel.codeplex.com/Project/Download/AttachmentDownload.ashx?ProjectName=virtualwrappanel&WorkItemId=1&FileAttachmentId=138959

3 ответов


метод 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;
    }
}

описание проблемы

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

в 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 не синхронизирована с другими четырьмя коллекциями, что приводит к возникшим ошибкам.

решение проблема

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

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 и перемещение не требует от нас добавления нового элемента во время OnItemsChanged.

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

когда ItemContainerGenerator получает событие remove, он обрабатывает все немедленно:

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

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

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

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


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

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

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

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

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

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

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

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

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