複数のバインディング・ターゲットで選択を同期するには?[Win 8/WP 8]:WinRT/Metro TIPS
CollectionViewSourceクラスを使って、複数のコントロール間で選択されている項目を同期する方法を説明する。
powered by Insider.NET
複数のリスト・ボックスなどを画面に配置しているときに、選択されている項目をそれらのコントロールの間で同期したいと思ったことはないだろうか? Windowsストア・アプリでよくあるケースは、通常のビューではGridViewコントロールを表示し、スナップ・ビューのときはListViewコントロールに切り替える場合だ。この場合には、ビューを切り替えても同じ項目が選択されているべきである。本稿では、コレクションをコントロールにバインドして表示する場合に、CollectionViewSourceクラスを利用して、そのような選択の同期を実現する方法を説明する。本稿のサンプルは「Windows Store app samples:MetroTips #41(Windows 8版)」と「Windows Store app samples:MetroTips #41(WP 8版)」からダウンロードできる。
なお、掲載しているコードは特記なき場合はWindowsストア・アプリとWindows Phone 8(以降、WP 8)アプリで共通である。
●事前準備
Windows 8(以降、Win 8)向けのWindowsストア・アプリを開発するには、Win 8とVisual Studio 2012(以降、VS 2012)が必要である。これらを準備するには、第1回のTIPSを参考にしてほしい。本稿では64bit版Win 8 ProとVS 2012 Express for Windows 8を使用している。
WP 8向けのアプリを開発するには、SLAT対応CPUを搭載したPC上の64bit版Win 8 Pro以上とWindows Phone SDK 8.0(無償)が必要となる。
●選択が同期しないアプリ
普通のコレクションをバインドしただけでは、複数のコントロール間で選択された項目が同期することはない。それを確認するためのアプリをまず作ってみよう。
バインディング・ソースにするコレクションは、「WinRT/Metro TIPS:データ・コレクションをバインドするには?[Win 8/WP 8]」で作ったSampleDataCollectionクラスをそのまま使うことにしよう。これはSampleDataというクラスのオブジェクトを格納しているコレクションで、SampleDataクラスは文字列型のTitleプロパティとUrlプロパティを持っている。
次に、ユーザーが選択できるコントロールを複数配置した画面を用意する(次のコード)。Windowsストア・アプリにはComboBoxコントロールと2つのListBoxコントロールを配置した。WP 8アプリはComboBoxコントロールが利用できないので、2つのListBoxコントロールだけを配置した。それらのコントロールを収めたGridコントロール(名称は「grid1」)のDataContextプロパティに、前述したSampleDataCollectionクラスのオブジェクトが実行時にセットされるものとして、データ・バインディングを設定してある。さらに、デザイン時にもデータが表示されるようにした*1。
まずは、Windowsストア・アプリ用のコードを示す。
……省略……
<Grid x:Name="grid1" Grid.Row="1" Margin="120,10,80,80"
d:DataContext="{d:DesignInstance Type=local:SampleDataCollection,IsDesignTimeCreatable=True}" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<ComboBox ItemsSource="{Binding}" DisplayMemberPath="Title"
FontSize="30" VerticalAlignment="Top" Width="300" />
<ListBox Grid.Column="1" ItemsSource="{Binding}" DisplayMemberPath="Title"
FontSize="30" Margin="40,0,0,0" />
<ListBox Grid.Column="2" ItemsSource="{Binding}" DisplayMemberPath="Url"
FontSize="30" Margin="40,0,0,0" />
</Grid>
……省略……
次がWP 8用のコードだ。
<phone:PhoneApplicationPage
……省略……
xmlns:local="clr-namespace:MetroTips041CS"
>
……省略……
<Grid x:Name="grid1" Grid.Row="1" Margin="12,0,12,0"
d:DataContext="{d:DesignInstance Type=local:SampleDataCollection,IsDesignTimeCreatable=True}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListBox ItemsSource="{Binding}" DisplayMemberPath="Title"
FontSize="30" Margin="0,10,0,0" />
<ListBox Grid.Row="1" ItemsSource="{Binding}" DisplayMemberPath="Url"
FontSize="30" Margin="0,10,0,0" />
</Grid>
……省略……
ComboBoxコントロールがない以外は、基本的にWindowsストア・アプリ版と同じである。なお、ページの開始タグ内にある「xmlns:local=……」に記述する名前空間は、適宜改めてほしい。
*1 デザイン時にデータを表示させる方法については、「WinRT/Metro TIPS:デザイン画面でデータをバインドするには?[Win 8/WP 8]」を参照してほしい。上記のコードでは、「d:」接頭辞を使って、GridコントロールのDataContextプロパティにデザイン時のみSampleDataCollectionオブジェクトが設定されるようにしている。コードビハインドからデータ・コンテキストを設定しない限り、実行時にはデータが表示されない。
そうしたら、「WinRT/Metro TIPS:データ・コレクションをバインドするには?[Win 8/WP 8]」で行ったのと同様に、コードビハインドでSampleDataCollectionオブジェクトを作成してGridコントロールのDataContextプロパティにセットする(次のコード)。
// grid1に表示するデータを作ってバインド
var sampleCollection = new SampleDataCollection();
sampleCollection.Add(new SampleData()
{
Title = "Metroスタイル・アプリの開発者が知るべき3つのこと",
Url = "http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html",
});
sampleCollection.Add(new SampleData()
{
Title = "デザイン・ガイドラインに従って画面を作成するには?[Win 8]",
Url = "http://www.atmarkit.co.jp/ait/articles/1208/23/news131.html",
});
sampleCollection.Add(new SampleData()
{
Title = "メニューの代わりにアプリ・バーを使うには?[Win 8]",
Url = "http://www.atmarkit.co.jp/ait/articles/1208/30/news149.html",
});
// SampleDataCollectionをDataContextに与えれば表示される。
// ただし、各コントロールでは項目をバラバラに選択できてしまう。
this.grid1.DataContext = sampleCollection;
' grid1に表示するデータを作ってバインド
Dim sampleCollection = New SampleDataCollection()
sampleCollection.Add(New SampleData() With _
{
.Title = "Metroスタイル・アプリの開発者が知るべき3つのこと",
.Url = "http://www.atmarkit.co.jp/fdotnet/chushin/readyforwin8app_01/readyforwin8app_01_01.html"
})
sampleCollection.Add(New SampleData() With _
{
.Title = "デザイン・ガイドラインに従って画面を作成するには?[Win 8]",
.Url = "http://www.atmarkit.co.jp/ait/articles/1208/23/news131.html"
})
sampleCollection.Add(New SampleData() With _
{
.Title = "メニューの代わりにアプリ・バーを使うには?[Win 8]",
.Url = "http://www.atmarkit.co.jp/ait/articles/1208/30/news149.html"
})
' SampleDataCollectionをDataContextに与えれば表示される。
' ただし、各コントロールでは項目をバラバラに選択できてしまう。
Me.grid1.DataContext = sampleCollection
このコードは、Windowsストア・アプリではLoadStateメソッド(LayoutAwarePageクラスを継承してページを作った場合)、またはOnNavigatedToメソッド(Pageクラスを直接継承してページを作った場合)に記述する。WP 8では、コンストラクタの末尾に追記すればよい。
これで実行してみよう。コントロールごとに異なる項目が選択できるはずだ(次の画像)。つまり、選択の同期が取れていないということだ。
●選択を同期させるにどうしたらよいだろう?
一案としては、コントロールのSelectionChangedイベントで、ほかのコントロールの選択を変更してやれば、全てのコントロールで同じ項目が常に選択されるようにできるだろう。しかし、例えば上記のWindowsストア・アプリでは、3つのイベント・ハンドラごとに、ほかの2つのコントロールを制御するコードを書かねばならない。面倒なうえに間違いも入り込みやすいだろう。
せっかくデータ・バインディングを使っているのだから、双方向バインディングを使ってユーザーの選択をほかのコントロールに伝播(でんぱ)させるようにすれば、きれいに作れそうだ。例えば次のようにして、コントロールのSelectedIndexプロパティ同士を直接バインドしても実現できる(Win 8のコードのみ掲載する)。
……省略……
<ComboBox x:Name="comboBox1" ItemsSource="{Binding}" DisplayMemberPath="Title"
FontSize="30" VerticalAlignment="Top" Width="300" />
<ListBox Grid.Column="1" ItemsSource="{Binding}" DisplayMemberPath="Title"
FontSize="30" Margin="40,0,0,0"
SelectedIndex="{Binding SelectedIndex, ElementName=comboBox1, Mode=TwoWay}" />
<ListBox Grid.Column="2" ItemsSource="{Binding}" DisplayMemberPath="Url"
FontSize="30" Margin="40,0,0,0"
SelectedIndex="{Binding SelectedIndex, ElementName=comboBox1, Mode=TwoWay}" />
……省略……
このコードは、以降では使わない。順を追ってコードを試している場合は、動作を確認できたら元に戻しておいてほしい。
しかしこれでは、コントロールごとにバインディングの指定を記述するのがやはり面倒だし、変更があってバインディング・ソース(上のコードでは「comboBox1」コントロール)を削除することになった場合には全てのバインディング指定を書き直さねばならない。だが、次に述べるように、もっと簡単に双方向バインディングによる選択の同期を実現できる方法があるのだ。
●CollectionViewSourceクラス
そのような目的に利用できるのがICollectionViewインターフェイス(Windows.UI.Xaml.Data名前空間)だ。ICollectionViewインターフェイスには、現在選択されている項目を表すCurrentItemプロパティやCurrentPositionプロパティがあり、それが変わったときに発火するイベントもある。
そして、ICollectionViewインターフェイスの実装を持つクラスが用意されている。それがCollectionViewSourceクラス(Windowsストア・アプリではWindows.UI.Xaml.Data名前空間、WP 8ではSystem.Windows.Data名前空間)である。
CollectionViewSourceクラスをバインディング・ソースに使うと、コレクションをバインドするだけでなく、選択されている項目もバインドできるのだ。さらにうれしいことには、コレクションを表示するために用意されているListBoxコントロールなどは、選択されている項目のバインドを自動的に行ってくれる。次で説明するように、コントロールのItemsSourceプロパティにCollectionViewSourceオブジェクトをバインドするだけでよいのだ。
●選択を同期するには?
バインドするコレクションを上記のCollectionViewSourceクラスにすればよい。
具体的には、CollectionViewSourceクラスのインスタンスを作り、バインドしたいコレクションをそのSourceプロパティにセットして使うだけだ。コードビハインドでもXAMLコードでも書けるが、ここではコードビハインドでやってみよう。次に示すコードのように、とても簡単だ。
// SampleDataCollectionをDataContextに与えれば表示される。
// ただし、各コントロールでは項目をバラバラに選択できてしまう。
// this.grid1.DataContext = sampleCollection;
// ↓
// DataContextに与えるコレクションをCollectionViewSourceでラップする。
var cvs = new CollectionViewSource() { Source = sampleCollection, };
this.grid1.DataContext = cvs;
' SampleDataCollectionをDataContextに与えれば表示される。
' ただし、各コントロールでは項目をバラバラに選択できてしまう。
' Me.grid1.DataContext = sampleCollection
' ↓
' DataContextに与えるコレクションをCollectionViewSourceでラップする。
Dim cvs = New CollectionViewSource() With {.Source = sampleCollection}
Me.grid1.DataContext = cvs
WP 8では、ファイル冒頭でSystem.Windows.Data名前空間を取り込む必要がある。
これで完成だ。実行して、どのコントロールの選択を変えても残りのコントロールの選択が同期して変化することを確認してほしい(次の画像)。
常に同じ項目が選択されているようになった(上:Win 8、下:WP 8)
Windowsストア・アプリ版のComboBoxコントロールでは、開いた状態のままカーソルキーで選択を変えてもほかのListBoxコントロールの選択が同期する。
●プロジェクト・テンプレートの秘密
Windowsストア・アプリのプロジェクト・テンプレートのうち、「グリッド アプリケーション (XAML)」と「分割アプリケーション (XAML)」では、自動生成されたXAMLコードでCollectionViewSourceクラスが使われている。
具体的には、自動生成されたXAMLコードの冒頭付近にPage.Resources要素があって、その中でCollectionViewSourceオブジェクトを生成している。そして、それをページ内のコントロールにバインドして使っている。このようにすることで、スナップ・ビューに切り替えても選択している項目が変わらないようにしてあるのだ。
●まとめ
CollectionViewSourceクラスを使うと、複数のコントロール間で選択されている項目を同期できる。これは、Windowsストア・アプリのプロジェクト・テンプレートでも多用されている。
なお、CollectionViewSourceクラスの詳細については、次のドキュメントを参照してほしい。
- MSDN:CollectionViewSource Class(Windowsストア・アプリ)
- MSDN:CollectionViewSource クラス(Windows Phone)
Copyright© Digital Advantage Corp. All Rights Reserved.