ListViewに配置したTextBoxやButtonがクリックされたときに、選択状態を変更するにはトリガーを使用する方法と、GotFocusイベントを使用する方法がある。
WPFアプリやUWPアプリなどでは、コレクションの一覧を表示するのにListBoxコントロール/ListViewコントロール/GridViewコントロールなどを使う。データテンプレートを使って個々のデータの表示にさまざまなUIコントロールを利用できるので、表現力のあるUIを構築できて便利だ。TextBoxコントロールやButtonコントロールなども配置できるので、データの一覧を見せるだけでなくエンドユーザーの編集も可能になる。ところが、配置したTextBoxコントロールなどをクリックしても、ListViewコントロールの選択状態は変わらないのである(次の画像)。
本稿では、ListViewコントロールに配置したTextBoxコントロール/Buttonコントロールがフォーカスを受け取ったときにListViewコントロールの選択状態を切り替える方法を紹介する。特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。
なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。掲載したサンプルコードに基づいて作成したWPFアプリの例を次の画像に示す。サンプルコードの全体はGitHubで公開している。
サンプルコードの実施例(WPF)ListViewItemオブジェクト(ListViewコントロールの各項目)のIsKeyboardFocusWithinプロパティの変化によるトリガーを利用する(次のコード)。これにより、TextBoxコントロールやButtonコントロールといったフォーカスを受け取ってしまうコントロールが配置されていても、選択状態が切り替わるようになる。
<ListView ……省略……>
  <ListView.ItemContainerStyle>
    <Style TargetType="ListViewItem">
      <!-- トリガーを使う -->
      <Style.Triggers>
        <Trigger Property="IsKeyboardFocusWithin" Value="true">
          <Setter Property="IsSelected" Value="true" />
        </Trigger>
      </Style.Triggers>
    </Style>
  </ListView.ItemContainerStyle>
  <ListView.ItemTemplate>
    <DataTemplate>
      <Grid ……省略……>
        <Grid.ColumnDefinitions>……省略……</Grid.ColumnDefinitions>
        <TextBlock ……省略…… />
        <TextBox Text="{Binding ……省略……}"
                  Grid.Column="1" />
        <Button Grid.Column="2" Content="Select" />
      </Grid>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>
トリガーを利用する方法はUWPなどでは使えない。汎用的な方法としては、TextBoxコントロールやButtonコントロールなどのGotFocusイベントのハンドラーで、フォーカスを受け取ったコントロールのデータコンテキストを利用する。
例えば次のコードのようにして、TextBoxコントロールとButtonコントロールのGotFocusイベントにハンドラーを結び付ける。
<ListView ……省略……>
  <ListView.ItemTemplate>
    <DataTemplate>
      <Grid ……省略……>
        <Grid.ColumnDefinitions>……省略……</Grid.ColumnDefinitions>
        <TextBlock ……省略…… />
        <TextBox Text="{Binding ……省略……}"
                 GotFocus="Control_GotFocus"
                 Grid.Column="1" />
        <Button GotFocus="Control_GotFocus"
                Grid.Column="2" Content="Select" />
      </Grid>
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>
GotFocusイベントハンドラーでは次のコードのようにして、フォーカスを受け取ったコントロールのデータコンテキストを取り出し、それを使って選択状態を切り替えたり、ListView内でのインデックスを得たりできる。
private void Control_GotFocus(object sender, RoutedEventArgs e)
{
  // フォーカスを受け取ったコントロールのデータコンテキストを得る
  if (sender is Control ctl
      && ctl.DataContext is SampleData data)
  {
    // data(結び付けられているデータコンテキスト)を使って何かする
    // 例:ListViewでこのデータを持っている項目を選択する
    //     =フォーカスを受け取ったコントロールを含む項目が選択される
    this.ListView1.SelectedItem = data;
    // ListView内でのインデックスを得る
    if (this.ListView1.ItemsSource is IList<SampleData> list)
    {
      int index = list.IndexOf(data);
      // index(ListView内でのインデックス)を使って何かする
      Run1.Text = index.ToString(); // 画面下端部、1番下の文字列の「=」より右
    }
  }
}
Private Sub Control_GotFocus(sender As Object, e As RoutedEventArgs)
  ' フォーカスを受け取ったコントロールのデータコンテキストを得る
  Dim ctl = TryCast(sender, Control)
  Dim data = TryCast(ctl?.DataContext, SampleData)
  If (data IsNot Nothing) Then
    ' data(結び付けられているデータコンテキスト)を使って何かする
    ' 例:ListViewでこのデータを持っている項目を選択する
    '     =フォーカスを受け取ったコントロールを含む項目が選択される
    Me.ListView1.SelectedItem = data
    ' ListView内でのインデックスを得る
    Dim list = TryCast(ListView1.ItemsSource, IList(Of SampleData))
    If (list IsNot Nothing) Then
      Dim index As Integer = list.IndexOf(data)
      ' index(ListView内でのインデックス)を使って何かする
      Run1.Text = index.ToString() ' 画面下端部、1番下の文字列の「=」より右
    End If
  End If
End Sub
この方法では、フォーカスを受け取ったコントロールごとに異なる処理を行うことも可能だ(例えば、ButtonコントロールをクリックしたときはTextBoxコントロールの文字列をクリアするなど)。そのような融通が利くので、筆者はWPFでもこちらを使っている。
コレクションの一覧を表示するコントロール(ListBox/ListView/GridViewなど)の中にフォーカスを受け取るコントロール(TextBox/Buttonなど)を配置した場合、フォーカスを受け取ったコントロールが含まれている項目を知るにはコントロールのデータコンテキストを利用する。ただしWPFの場合、その項目を選択状態にするだけならトリガーが利用できる。
利用可能バージョン:.NET Framework 3.5以降
カテゴリ:WPF 処理対象:データバインディング
カテゴリ:WPF/XAML 処理対象:ListViewコントロール
使用ライブラリ:ListViewコントロール(System.Windows.Controls名前空間)
関連TIPS:[WPF/UWP]列挙型をComboBoxにバインドするには?
関連TIPS:WPF/UWP:ラジオボタンを双方向バインディングするには?[C#/VB]
関連TIPS:WPF/UWP:テキストブロックの一部分だけをデータバインディングするには?[XAML]
関連TIPS:WPF:ラジオボタンの選択をバインディングソースに反映させるには?[C#/VB]
関連TIPS:WPF:DataGridやListViewなどに表示しているデータを別スレッドから変更するには?[C#、VB]
関連TIPS:構文:nullチェックを簡潔に記述するには?[C# 6.0]
Copyright© Digital Advantage Corp. All Rights Reserved.