概要
ListBoxは指定されたアイテムのコレクションを表示することが目的のコントロールだが、そのアイテムの指定には二通りの方法がある。
- 直接ListBoxItemを子要素として配置する方法
- ItemsSourceプロパティにバインドしたコレクションを、DataTemplateで指定したコンテナに格納して配置させる方法
データバインディングを用いるなら後者の方法を使いたいが、実際に生成された子要素にアクセスするのはなかなか難しい。実際にコーディングする上でいくつか罠に引っかかってしまったので、これ以上不幸な犠牲者を出さないためにも、ここに覚え書きを残しておくことにする。
注:もしかすると下記の目標を達成するためにはもっとスマートな方法があるのかも知れないが、今後似たようなことをする機会もあるかも知れないので大目に見てほしい。
目標
ListBoxのアイテムを選択不可にする
具体的な手段
ListBox.SelectionChangedイベントで、各アイテムのSelector.IsSelected添付プロパティをすべてfalseに設定することで、ハンドラが処理を終えた時点ですべてのアイテムが非選択状態になる。
コード
ListBoxItemを子要素に格納する場合は、Itemsプロパティから取得したアイテムのコレクションのIsSelected添付プロパティ(実態はSelectorクラスが持つ)を書き換えれば良い。ListBoxの仕組みについて、以下の記事が非常に参考になった。
一方で、ItemsSourceを使う場合だと、ItemsでもSelectedItemsでもItemsSourceに設定したコレクションが持っているオブジェクトへの参照が帰ってくるだけで、実際にテンプレートが適用された子要素が返ってくるわけではない。これは正直理解しがたい仕様であるが、これに立ち向かう手段については、MSDNの以下の記事が詳しい。
方法 : DataTemplate によって生成された要素を検索する
なかなか狂気を感じる内容だが、要約すると以下の通りである。
- 対象のListBoxのItemContainerGeneratorのContainerFromItemメソッドを使って、ほしいアイテムから子要素の実態(ListBoxItem)が取得できる
- ListBoxItemの中身が見たい場合は、ListBoxItemのContentPresenterが持ってるDataTemplateのFindNameを使って引っ張る
- ContentPresenterを取り出すのはVisualTreeHelperで頑張れ
実際のコードはこんな感じになる。XAML側は普通にItemsSourceにコレクションをバインドしているだけなので省略する。
private void MyListBox_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
foreach (var item in MyListBox.Items)
{
var container = MyListBox.ItemContainerGenerator.ContainerFromItem(item);
Selector.SetIsSelected(container, false);
}
}