ListBoxの選択状態の変更

以前からの疑問が解消したのでメモ。

背景知識の説明

まずWPFでのListBox(というか親クラスのItemsControl)とリスト(IListとか)の関係を整理する。ListBox.ItemsSourceにリストをぶち込むとListBoxにリスト項目の一覧が表示される。単純明快。

ただし、舞台裏では少々複雑なことが行われている。ItemsSouceに渡したリストが自動的にItemCollectionに変換され、これがListBoxで参照される。ItemsCollectionはリストのラッパーでListBoxやItemsCollectionに不可欠な選択状態(現在選択中の項目への参照)やソート結果(並び順の状態)を保持するために使われる。つまり内部ではListBoxが直接リストを参照していないことになる。お陰で特殊なクラスから派生する必要も無く普通のIListや配列を渡すだけで、WPFのリッチな機能(ソートやグループ化)が使えるのだ。なのでこれらの機能をコントロールする場合にはItemCollectionオブジェクトの取得が必要になる。これにはCollectionViewSource.GetDefaultViewメソッドで取得できる。

余談ながらCollectionViewSourceクラスとの関係を言うと、ItemsCollectionはXAML上では使えずC#などコードからの利用が前提となる。このためXAML上でItemCollectionを生成してListBox.ItemsSourceに食わせるのにCollectionViewSourceを使うことになる。

ListBoxの選択状態

それで本題。以前から分からなかったことが一つあった。ListBoxのように複数選択するタイプのItemsControlでの選択状態の制御だ。ListBox上で選択された要素が知りたければListBox.SelectedItemsプロパティでリストとして取得できる。

ただ選択状態をコードで制御するのがよく分からない。読取り専用なので設定できない。単一項目版のSelectedItemプロパティの方は設定もできるのだが、複数項目選択の変更に困る。ListBoxの内部では状態を持っているのは間違いないのだが、ListBoxのAPIにはそれらしいものが見当たらない。ソート機能のようにItemsCollectionが担当しているかと想像したがここにも該当するものが無い。ListBox専用の特殊な子クラスがあるのかと思っていたがこれも見当たらない。これが長らくの謎だった。

チェックボックスリスト

別件でチェックボックスリストのサンプルを探していた。するとこんなのがあった。
WPF/コンポーネント/コントロール/チェックボックスリストボックス - プログラミング図書館・本館 - アットウィキ

ここではItemsTemplateの中のチェックボックスコントロールをListBoxの選択状態にバインディングしている。なるほど。バインディング先は(Selector.IsSelected)となっている。これはSelectorクラス(ListBoxの親クラス)が持っている添付プロパティだ。

謎の解明

添付プロパティの詳細説明は省略するが、任意のクラス(正確にはDependencyObjectのサブクラス)に後付けできる仮想的なプロパティのようなものと言えば良いか。具体的にはDockPanelの表示位置を指定する際に使うDockPanel.Dockというプロパティが添付プロパティだ。Dockプロパティ自体はDockPanelが実際に保有しているが、貼り付け先のオブジェクトに関連づけて持たせる事ができるので、さも貼り付け先のオブジェクトが持っているかのようにXAML上では扱える。

なので選択状態を持っているのは誰だという疑問はぐるっと回ってやはりListBoxが持っていたのであった。なるほど。上の説明になっているか分からないけどちょっとサンプルコードを書いておく。

 <ListBox Name="MyListBox" SelectionMode="Multiple" >
     <ListBoxItem Content="いち" Selector.IsSelected="True" />
     <ListBoxItem Content="にぃ" />
     <ListBoxItem Content="さん" Selector.IsSelected="True"  />
     <ListBoxItem Content="よん" />
 </ListBox>

Selector.IsSelectedを設定した項目だけ選択状態にめでたくなっているのが分かる。

なおC#コードからは次のように書ける。

Selector.SetIsSelected((DependencyObject) MyListBox.Items[1], true);