Bindingを使っているListBoxのListBoxItemにフォーカスするには

C#

こんにちは、働くC#プログラマーのさんさめです。

WPFでListBoxListViewを使うとき、
MVVMパターンで作るのならば、
ItemsSourceプロパティにVMのコレクションを
バインディングするのが定石です。

ところが、
このような普通の使い方をしている時に、
「n番目の項目にフォーカスを当てたい」
と思うと、一筋縄ではいきません。

なぜなら、
BindingをしているListBoxでは、
ItemsからもItemsSourceからも、
VMのコレクションが取得できてしまうため、
実際にフォーカスを当てたい、
「ListBoxItem」には素直にアクセスできないからです。

結論から言うと、
こういう時には、
ItemContainerGeneratorプロパティ
を使うことでListBoxItemへの
フォーカスを実現できます。

本記事では、
このプロパティを通じて、

  • 要素のインデックスから
    ListBoxItemにフォーカスさせる方法
  • 最後に選択していた
    ListBoxItemに再フォーカスさせる方法

の2点について解説します。

スポンサーリンク

ItemContainerGeneratorプロパティを使う

さて、そもそも
ItemsContainerGeneratorプロパティ
とはいったい何をするものでしょうか。

ItemsContainerGeneratorは、
1つの独立したクラスのようです。

Microsoftの公式ドキュメントによると、

ItemsControl など、ホストに代わってユーザー インターフェイス (UI) を生成します。

とあります。

さんさめ
さんさめ

…抽象的でよく分からない…

よくよく読んでみると、
どうやら、保有クラスの型によって、
実際に生成される子要素のUIを作ったり、
管理したりを司るクラスのようです。

たとえば、

  • ListBoxならListBoxItem
  • ComboBoxならComboBoxItem

生成削除などの管理を行います。

また、実際にListBoxに設定された
ItemsSource(Items)とUI要素の紐づけも、
このクラスが担っているようです。

…ということは、つまり、

このクラスにインデックスや
ItemsSourceの要素を渡せば、
対応するUI要素(今回はListBoxItem)を
教えてくれそう
な気がしますね。

要素のインデックスからListBoxItemを取得

はたしてその機能を持つメソッドが、
ContainerFromIndexです。

以下のように使います。

var dObj = MainListBox
    .ItemContainerGenerator
    .ContainerFromIndex(index);
if (dObj is ListBoxItem target)
{
    target.Focus();
    MainListBox.SelectedItem = MainListBox.Items[index];
}

戻り値の型はDependencyObjectなので、
そのままではFocusメソッドを呼べません。

そのため、一度ListBoxItemにキャストしてから、
Focusメソッドを呼んでいます。

では、実際に挙動を確かめてみます。

ボタンを押した瞬間に、
ちゃんと入力された番号の要素に
フォーカスしていますね。

最後に選択していたListBoxItemに再フォーカスさせる

次は、要素からListBoxItemを取得する例です。

これまたずばりなメソッドがあります。

具体的には、
ContainerFromItemを使用します。

これを用いた、
最後に選択していたListBoxItemに再フォーカスさせる
実装は次のようになります。

var dObj = MainListBox
    .ItemContainerGenerator
    .ContainerFromItem(MainListBox.SelectedItem);
if (dObj is ListBoxItem target)
{
    target.Focus();
}

MainListBox.SelectedItemを引数に渡すことで、
最後に選択していた要素に対応する
ListBoxItemを取得しています。

後の流れはほとんど先の実装と同じですね。

しいて言えば、
SelectedItemへの代入はしていません。

こちらも、実際に挙動を確かめてみましょう。

一度TextBoxをクリックしているのはフォーカスを外すためです

ボタンを押すたびに、
フォーカスが再度当てられて
背景色が濃くなっていることが分かります。

仮想化のせいで見つからないことがあることに注意

さて、これで大体以上なのですが、

最後にこのメソッドを使って、
子のUI要素を取得する時の注意点を書いておきます。

それは、

「UIの仮想化を行っているときに
表示外のUI要素を取得しようとすると、
nullが返ってしまう可能性がある」

という問題です。

先の例で、試しに領域外の要素を指定してみましょう。

90番目の要素はありまぁす!

存在するはずの要素に、
フォーカスしてくれる気配がありません。
見えている範囲内の数値を入力した場合は反応しています。

これは、
UIの仮想化によりまだUI要素自体が存在せず、
ContainerFromIndexがnullを返しているためです。

この問題を回避するためには、

  • 仮想化を切る
  • 一度表示してからフォーカスを当てる

のどちらかの手段を取る必要があります。

前者の、「仮想化を切る」は、
動作が重くなる原因になるため
おススメできません。

というわけで、
一度表示範囲内に含めてから、
あらためてフォーカスを当てる方法を取りましょう。

具体的には、次のようなコードになります。

var dObj = MainListBox
    .ItemContainerGenerator
    .ContainerFromIndex(index);
if (dObj is ListBoxItem target)
{
    target.Focus();
    MainListBox.SelectedItem = MainListBox.Items[index];
}
else if (index < MainListBox.Items.Count)
{
    MainListBox.ScrollIntoView(MainListBox.Items[index]);
    var dObj2 = MainListBox
        .ItemContainerGenerator
        .ContainerFromIndex(index);
    if (dObj2 is ListBoxItem target2)
    {
        target2.Focus();
        MainListBox.SelectedItem = MainListBox.Items[index];
    }
}

重要なのは、else if より後のコードです。

ScrollIntoViewメソッドを使って、
指定されたインデックスの要素を、
表示範囲内に収めています。

そのあとに、改めて
ContainerFromIndexメソッドを呼び出して、
UI要素を取得します。

このように段階を踏むことで、
仮想化されたListBoxでも、
任意の要素にフォーカスを当てることができます。

まとめ

まとめです。

  • ItemContainerGeneratorを使えば、
    Bindingを使っているListBoxでも
    ListBoxItemを取得できる
  • ListBoxItemを取得できれば、
    任意の要素にフォーカスを当てられる
  • 仮想化されている場合
    一度表示内に収めるなどの工夫が必要

最後までお読みいただき、ありがとうございました。

関連記事

DataGridで同じことをしようとすると、
セルにフォーカスする必要があります。
その場合は以下の記事をご覧ください。

コメント

タイトルとURLをコピーしました