※前からWPFやってる人には常識なんだろうなあとは思うけどそういうことは気にしない
問題
ListViewの選択項目をプログラムから操作するには、VMにIsSelectedプロパティを用意して、ListViewItem.IsSelected にバインドすればよい
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}" />
</Style>
</ListView.ItemContainerStyle>
という記述がぐぐるとよく見つかるのだけど、これは正しくない。
ListViewはデフォルトで仮想化されるため、見えている範囲+αの分しかListViewItemのインスタンスは生成されない。ListViewItemが存在しなければバインディングも働かないので、範囲外の項目に対してはVとVMでの同期は行われない。
実際どういう動きになるのか、簡単なテストプログラムで確認してみた。
テストプログラム
説明
public class Item : INotifyPropertyChanged
{
public string Name { get; }
public bool IsSelected { get; set; }
}
こんな感じのViewModelがあって、そのコレクション(要素数50、Name=00~49)が左上のListViewと右のItemsControlのItemsSourceに指定されている。
ListViewの方では ListViewItem.IsSelected と Item.IsSelected が双方向でBindingされていて、ItemsControlの方では Item.IsSelected がtrueなら背景色が変わるようになっている。
ソースは こちら
動かしてみる
ListView上で適当なアイテムを選択してみると、ViewModel側のIsSelectedもtrueになる。

Ctrl+AでListView上のアイテムを全選択してみる。
ListView.SelectedItems には50項目すべてちゃんと追加されるけれど、ViewModel側のIsSelectedは、画面に見えているもの+αしか変わっていない。

そのままListViewをスクロールしていくと、それに合わせてViewModel側のIsSelectedも順番にtrueに変わっていく。

すべてのViewModelのIsSelectedがtrueになったところで、またListView上の適当な項目をクリックしてみる。
全選択が解除されてその項目だけが選択された状態になるので SelectedItems.Count は1になるけれど、画面に見えていない部分のViewModelのIsSelectedはtrueのまま。

ViewModel側で IsSelected を操作した時の挙動も確認する。
ListView上に見えているItem-01のIsSelectedをtrueに変更すると、SelectedItems にも反映されるが、

見えていないItem-49のIsSelectedをtrueにしても SelectedItems には反映されない。

解決策
-
ListViewItem.IsSelectedに対するバインディングはOneWay(VM -> Vのみ)にする -
ListView.SelectedItemsの内容は一切信用しない - VからVMへの反映は、
SelectionChangedイベントで自力でやる
void lvw_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in e.RemovedItems.Cast<Item>())
item.IsSelected = false;
foreach (var item in e.AddedItems.Cast<Item>())
item.IsSelected = true;
}
とのこと。
IsSelectedに対するバインディングをしないならSelectedItemsを見ても大丈夫っぽいけど。
