問題の説明
何がうまくいかないかの説明と、それを修正する方法の指示を求めました。これまでのところ、誰も問題を説明していません。そうします。
VirtualizingWrapPanel を使用した ListBox には、アイテムを追跡する 5 つの個別のデータ構造があり、それぞれが異なる方法で追跡されます。
- ItemsSource: 元のコレクション (この場合は ObservableCollection)
- CollectionView: ソート/フィルター/グループ化されたアイテムの個別のリストを保持します (これらの機能のいずれかが使用されている場合のみ)
- ItemContainerGenerator: アイテムとコンテナ間のマッピングを追跡します
- InternalChildren: 現在表示されているコンテナを追跡します
- WrapPanelAbstraction: どのコンテナーがどの行に表示されるかを追跡します
アイテムが ItemsSource から削除された場合、この削除はすべてのデータ構造に反映される必要があります。仕組みは次のとおりです。
- ItemsSource で Remove() を呼び出します
- ItemsSource はアイテムを削除し、CollectionView によって処理されるその CollectionChanged を起動します
- CollectionView はアイテムを削除し (並べ替え/フィルタリング/グループ化が使用されている場合)、ItemContainerGenerator によって処理されるその CollectionChanged を起動します。
- ItemContainerGenerator はそのマッピングを更新し、VirtualizingPanel によって処理されるその ItemsChanged を起動します
- VirtualizingPanel は、VirtualizingWrapPanel によって実装される仮想 OnItemsChanged メソッドを呼び出します。
- VirtualizingWrapPanel はその WrapPanelAbstraction を破棄するためビルドされますが、InternalChildren は更新されません。
このため、InternalChildren コレクションは他の 4 つのコレクションと同期されず、発生したエラーにつながります。
問題の解決策
この問題を解決するには、次のコードを 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 と Move を処理するために OnItemsChanged 中に新しい項目を追加する必要がない理由を不思議に思うかもしれません。
これを理解する鍵は、ItemContainerGenerator の動作にあります。
ItemContainerGenerator が remove イベントを受け取ると、すぐにすべてを処理します。
- ItemContainerGenerator は、独自のデータ構造からアイテムをすぐに削除します
- ItemContainerGenerator は ItemChanged イベントを発生させます。パネルはすぐにコンテナを削除することが期待されています。
- ItemContainerGenerator は、その DataContext を削除することによってコンテナーを「準備解除」します
一方、ItemContainerGenerator は、アイテムが追加されると、通常はすべてが延期されることを学習します。
- ItemContainerGenerator は、そのデータ構造にアイテムの「スロット」をすぐに追加しますが、コンテナは作成しません
- ItemContainerGenerator は ItemChanged イベントを発生させます。パネルは InvalidateMeasure() を呼び出します [これは基本クラスによって行われます - 行う必要はありません]
- 後で MeasureOverride が呼び出されると、Generator.StartAt/MoveNext を使用してアイテム コンテナーが生成されます。新しく生成されたコンテナは、その時点で InternalChildren に追加されます。
したがって、InternalChildren コレクションからのすべての削除 (Move または Replace の一部であるものを含む) は OnItemsChanged 内で行う必要がありますが、追加は次の MeasureOverride まで延期することができます (また、延期する必要があります)。