記事のカテゴリー: C#、.NET9、WPF
仮想化したListViewは画面に表示する範囲の項目コンテナ(ListViewItem)しか作りません。その動作によりパフォーマンスを向上しているわけですが、MVVMでは問題が起こります。
項目の選択状態をViewModelで扱う
ListViewの項目の選択状態をViewModelで扱う場合、ListViewItemのIsSelectedプロパティと、1対1で対応するViewModelクラスのプロパティをバインドします。このときViewModelはコンテナが未作成の項目の選択状態を扱うこともあり、ViewとViewModelで選択状態をうまく同期できません。
C#:
// ViewModel の基本クラス public abstract class ViewModel : INotifyPropertyChanged { protected void NotifyPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler? PropertyChanged; } // 項目に対応する ViewModel クラス public class ItemViewModel : ViewModel { private bool isSelected = false; public bool IsSelected { get { return isSelected; } set { if (value == isSelected) { return; } isSelected = value; NotifyPropertyChanged(); } } }
XAML:
<ListView ItemsSource="{Binding Items}"> <ListView.View> <GridView /> </ListView.View> <ListView.ItemContainerStyle> <!-- IsSelected プロパティに双方向でバインドする --> <Style TargetType="ListViewItem"> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" /> </Style> </ListView.ItemContainerStyle> </ListView>
問題が起こるシナリオ
例えば次の場合に問題が起こります。
- ListViewにたくさんの項目を追加する
- ViewModelですべての項目を選択する
- UIでクリックして1つの項目を選択する(他の項目は選択解除される)
- ListViewを下にスクロールする
- 画面外にあった項目がなぜか選択されている
この問題は3で他の項目を選択解除したにもかかわらず、画面外の項目についてはコンテナが存在しないためViewからViewModelに選択状態を反映しないことから生じます。その結果、4で初めてコンテナを作って、ViewModelの値を受け取り選択状態になります。
回避策
例えコンテナが存在しない状態でもViewとViewModelで項目の選択状態を同期するには、ViewからViewModelへの反映を自前で実装する必要があります。ListViewの項目の選択状態が変わったときに発生するSelectionChangedイベントを使い、発生原因がユーザーの入力の場合のみにViewModelへ反映します。ただし、SelectionChangedイベントの引数では発生原因がバインディングかユーザーの入力かは分からないので、マウスやキーボードの状態を確認します。
XAML:
<ListView ItemsSource="{Binding Items}"> <ListView.View> <GridView /> </ListView.View> <ListView.ItemContainerStyle> <!-- (View から ViewModel は自前で実装するので) 一方向でバインドするように修正 --> <Style TargetType="ListViewItem"> <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=OneWay}" /> </Style> </ListView.ItemContainerStyle> </ListView>
C#:
// 適当な添付ビヘイビアの中で private static void ListView_OnSelectionChanged(object sender, SelectionChangedEventArgs e) { // 発生原因がユーザーの入力かどうかを判断する static bool FromView(ListView listView) { if (listView.IsKeyboardFocusWithin && (Keyboard.IsKeyDown(Key.PageUp) || Keyboard.IsKeyDown(Key.PageDown) || Keyboard.IsKeyDown(Key.End) || Keyboard.IsKeyDown(Key.Home) || Keyboard.IsKeyDown(Key.Up) || Keyboard.IsKeyDown(Key.Down) || (Keyboard.IsKeyDown(Key.A) && Keyboard.Modifiers == ModifierKeys.Control))) { return true; } else if (listView.IsMouseCaptureWithin && listView.IsMouseOver && (Mouse.LeftButton == MouseButtonState.Pressed || Mouse.RightButton == MouseButtonState.Pressed)) { return true; } return false; } // 選択状態を ViewModel に反映する static void UpdateViewModel(ListView listView) { foreach (ItemViewModel item in listView.Items.Cast<ItemViewModel>()) { item.IsSelected = listView.SelectedItems.Contains(item); } } if (sender is not ListView listView) { return; } if (FromView(listView)) { UpdateViewModel(listView); } }
0 件のコメント:
コメントを投稿