[WPF/UWP]ListView内のTextBoxがクリックされたときに選択項目を切り替えるには?:.NET TIPS
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)
掲載したサンプルコードに基づいて作成した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>
この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>
先のコードとよく似ているが、トリガーがなく、代わって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
ListViewコントロールのItemsSourceプロパティには、「SampleData」というクラスのコレクション(IList<SampleData>型)がバインドされているものとする。
つい「クリックされたコントロールのインスタンスはListView内のどれだろうか?」という発想になりがちだが、このようにデータ中心に考えるとよい。
なお、C#のコードではC# 7の新機能を使っている。その詳細は「特集:C# 7の新機能詳説:第3回 型による分岐の改良」をご覧いただきたい。
この方法では、フォーカスを受け取ったコントロールごとに異なる処理を行うことも可能だ(例えば、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.