[WPF/UWP]ListView内のTextBoxがクリックされたときに選択項目を切り替えるには?.NET TIPS

ListViewに配置したTextBoxやButtonがクリックされたときに、選択状態を変更するにはトリガーを使用する方法と、GotFocusイベントを使用する方法がある。

» 2017年11月15日 05時00分 公開
[山本康彦BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載「.NET TIPS」

 WPFアプリやUWPアプリなどでは、コレクションの一覧を表示するのにListBoxコントロール/ListViewコントロール/GridViewコントロールなどを使う。データテンプレートを使って個々のデータの表示にさまざまなUIコントロールを利用できるので、表現力のあるUIを構築できて便利だ。TextBoxコントロールやButtonコントロールなども配置できるので、データの一覧を見せるだけでなくエンドユーザーの編集も可能になる。ところが、配置したTextBoxコントロールなどをクリックしても、ListViewコントロールの選択状態は変わらないのである(次の画像)。

 本稿では、ListViewコントロールに配置したTextBoxコントロール/Buttonコントロールがフォーカスを受け取ったときにListViewコントロールの選択状態を切り替える方法を紹介する。特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。掲載したサンプルコードに基づいて作成したWPFアプリの例を次の画像に示す。サンプルコードの全体はGitHubで公開している。

サンプルコードの実施例(TextBoxコントロールやButtonコントロールを置いただけの場合)
サンプルコードの実施例(GotFocusイベントを利用してListView内のTextBoxがクリックされた場合に項目が選択されるようにした場合) サンプルコードの実施例(WPF)
掲載したサンプルコードに基づいて作成したWPFアプリの例。
上: TextBoxコントロールやButtonコントロールを置いただけの場合。クリックしてフォーカスを与えても選択されない。この画像では3番目の項目にあるTextBoxコントロールの文字列を編集しているのだが、ListViewコントロールは選択状態になっていない。このままでは左端のTextBlockコントロール(ピンク色の部分)をクリックしないと選択状態を変えられないので不便である
下: 本稿の2番目で説明する方法を組み込んだ。ListViewコントロールの選択状態が、データバインディングで下部のTextBlockコントロールに表示されている。TextBoxコントロールをクリックすると、ListViewコントロールでそれを含んでいる項目が選択状態になる。右端のButtonコントロールをクリックしても同様である

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>

WPFでトリガーを使う例(XAML)
このListViewコントロールの各項目には、TextBoxコントロールとButtonコントロールが配置されている。TextBoxコントロール/Buttonコントロールがフォーカスを受け取ると、それを含んでいるListViewコントロールの項目のListViewItemオブジェクトのIsKeyboardFocusWithinプロパティがtrueに変わる。するとトリガーが起動されて、そのListViewItemオブジェクトのIsSelectedプロパティをtrueに変える(=その項目が選択状態になる)。

汎用的にはコードビハインドでコントロールのデータコンテキストを利用する

 トリガーを利用する方法は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>

WPFの画面でGotFocusイベントにハンドラーを結び付ける(XAML)
先のコードとよく似ているが、トリガーがなく、代わってTextBoxコントロールとButtonコントロールのGotFocusイベントにハンドラーとしてControl_GotFocusメソッドが設定してある。

 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

GotFocusイベントハンドラーでデータコンテキストを使う例(上:C#、下:VB)
ListViewコントロールのItemsSourceプロパティには、「SampleData」というクラスのコレクション(IList<SampleData>型)がバインドされているものとする。
つい「クリックされたコントロールのインスタンスはListView内のどれだろうか?」という発想になりがちだが、このようにデータ中心に考えるとよい。
なお、C#のコードではC# 7の新機能を使っている。その詳細は「特集:C# 7の新機能詳説:第3回 型による分岐の改良」をご覧いただきたい。

 この方法では、フォーカスを受け取ったコントロールごとに異なる処理を行うことも可能だ(例えば、ButtonコントロールをクリックしたときはTextBoxコントロールの文字列をクリアするなど)。そのような融通が利くので、筆者はWPFでもこちらを使っている。

まとめ

 コレクションの一覧を表示するコントロール(ListBox/ListView/GridViewなど)の中にフォーカスを受け取るコントロール(TextBox/Buttonなど)を配置した場合、フォーカスを受け取ったコントロールが含まれている項目を知るにはコントロールのデータコンテキストを利用する。ただしWPFの場合、その項目を選択状態にするだけならトリガーが利用できる。

「.NET TIPS」のインデックス

.NET TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。