※前から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を見ても大丈夫っぽいけど。