こんにちは、働くC#プログラマーのさんさめです。
WPFでListBoxやListViewを使うとき、
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への代入はしていません。
こちらも、実際に挙動を確かめてみましょう。
ボタンを押すたびに、
フォーカスが再度当てられて
背景色が濃くなっていることが分かります。
仮想化のせいで見つからないことがあることに注意
さて、これで大体以上なのですが、
最後にこのメソッドを使って、
子のUI要素を取得する時の注意点を書いておきます。
それは、
「UIの仮想化を行っているときに
表示外のUI要素を取得しようとすると、
nullが返ってしまう可能性がある」
という問題です。
先の例で、試しに領域外の要素を指定してみましょう。
存在するはずの要素に、
フォーカスしてくれる気配がありません。
見えている範囲内の数値を入力した場合は反応しています。
これは、
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で同じことをしようとすると、
セルにフォーカスする必要があります。
その場合は以下の記事をご覧ください。
コメント