Explanation of the problem
You asked for an explanation of what is going wrong as well as instructions how to fix it. So far nobody has explained the problem. I will do so.
In ListBox with a VirtualizingWrapPanel there are five separate data structures that track items, each in different ways:
- ItemsSource: The original collection (in this case ObservableCollection)
- CollectionView: Keeps a separate list of sorted/filtered/grouped items (only if any of these features are in use)
- ItemContainerGenerator: Tracks the mapping between items and containers
- InternalChildren: Tracks containers that are currently visible
- WrapPanelAbstraction: Tracks which containers appear on which line
When an item is removed from ItemsSource, this removal must be propagated through all data structures. Here is how it works:
- You call Remove() on the ItemsSource
- ItemsSource removes the item and fires its CollectionChanged which is handled by the CollectionView
- CollectionView removes the item (if sorting/filtering/grouping is in use) and fires its CollectionChanged which is handled by the ItemContainerGenerator
- ItemContainerGenerator updates its mapping, fires its ItemsChanged which is handled by VirtualizingPanel
- VirtualizingPanel calls its virtual OnItemsChanged method which is implemented by VirtualizingWrapPanel
- VirtualizingWrapPanel discards its WrapPanelAbstraction so it will be built, but it never updates InternalChildren
Because of this, the InternalChildren collection is out of sync with the other four collections, leading to the errors that were experienced.
Solution to the problem
To fix the problem, add the following code anywhere within VirtualizingWrapPanel's OnItemsChanged method:
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;
}
This keeps the InternalChildren collection in sync with the other data structures.
Why AddInternalChild/InsertInternalChild is not called here
You may wonder why there are no calls to InsertInternalChild or AddInternalChild in the above code, and especially why handling Replace and Move don't require us to add a new item during OnItemsChanged.
The key to understanding this is in the way ItemContainerGenerator works.
When ItemContainerGenerator receives a remove event it handles everything immediately:
- ItemContainerGenerator immediately removes the item from its own data structures
- ItemContainerGenerator fires the ItemChanged event. The panel is expected to immediately remove the container.
- ItemContainerGenerator "unprepares" the container by removing its DataContext
On the other hand, ItemContainerGenerator learns that an item is added everything is typically deferred:
- ItemContainerGenerator immediately adds a "slot" for the item in its data structure but does not create a container
- ItemContainerGenerator fires the ItemChanged event. The panel calls InvalidateMeasure() [this is done by the base class - you do not have to do it]
- Later when MeasureOverride is called, Generator.StartAt/MoveNext is used to generate the item containers. Any newly-generated containers are added to InternalChildren at that time.
Thus, all removals from the InternalChildren collection (including ones that are part of a Move or Replace) must be done inside OnItemsChanged, but additions can (and should) be deferred until the next MeasureOverride.
与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…