動的にコンテキストメニューを構成し、ViewModel のコマンドをたたく
サンプルプロジェクト contextmenu_viewmodel.zip
WP7 では Silverlight Toolkit 同梱の ContextMenu クラスを使用することで、コンテキストメニューを使用することが出来ます。
たとえば ListBox の項目を長押ししたらメニューを出す XAML は以下のようにします。
<ListBox x:Name="listbox" ItemsSource="{Binding Items}"> <toolkit:ContextMenuService.ContextMenu> <toolkit:ContextMenu> <toolkit:MenuItem Header="Item1"/> <toolkit:MenuItem Header="Item2"/> <toolkit:MenuItem Header="Item3"/> <toolkit:MenuItem Header="Item4"/> <toolkit:MenuItem Header="Item5"/> </toolkit:ContextMenu> </toolkit:ContextMenuService.ContextMenu> </ListBox>
お分かりと思いますが、この方法には 表示するメニューが固定される ( 選択された項目によって表示するメニューを変えられない) という大きな問題があります。
実際には表示したい項目やソフトの状態によっては「削除」だったり、「名前の変更」だったり「最新の情報に更新」だったりするわけなので、この状態では具合が悪いです。
※また ListBox に 100+ の項目を表示して上記の方法でコンテキストメニューを表示すると、MenuItem.CommandParameter に表示しているアイテムを渡すとずれるという問題があります。(100番目の項目を選択したはずなのに、実際には 10番目が渡ってくるなど)
動的にメニューを構成する
そこでコンテキストメニューを動的に構成し表示するようにしてみましょう。
ではどうやってメニューを作成するか?そうですコンバータです。普段は bool→Visibility などで活躍するコンバータを、ItemsSource→IEnumerable<MenuItem> 変換に使用します。
XAML
まず ContextMenu には ItemsSource プロパティがありますので、ここに MenuItem クラスのコレクションを入れてやればコンテキストメニューが開いたときの項目になります。
XAML 的にはこんな感じになります(コンバータと ListBox.ItemTemplate 部分を抜粋)。
<phone:PhoneApplicationPage.Resources> <local:ContextMenuConverter x:Key="ContextMenuConverter"/> <DataTemplate x:Key="StringTemplate"> <Border> <Grid > <TextBlock Text="{Binding Index}"/> <TextBlock Text="{Binding Name}"/> <toolkit:ContextMenuService.ContextMenu> <toolkit:ContextMenu ItemsSource="{Binding Converter={StaticResource ContextMenuConverter}}"/> </toolkit:ContextMenuService.ContextMenu> </Grid> </Border> </DataTemplate> </phone:PhoneApplicationPage.Resources>
通常ならコンテキストメニューは ListBox 直下として、<ListBox><toolkit:ContextMenuService.ContextMenu>~</ListBox> するのですが、ここでは ListBox に表示される項目のテンプレートの配下にしています。
これは後に解説する Command 実行に関連します。
コンバータ
コンバータはこんな感じです。
value に入ってくるのが ListBox に表示されているアイテム(ここでは DataClass) で、返すのが 表示する MenuItem のリストになります。
※コンテキストメニューは ListBox 直下としない理由がここにあります。ListBox 直下にすると value に DataContext(ViewModel) が渡ってきてしまい、選択された項目が来ないので都合が悪いのです。
public class ContextMenuConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { // 選択されたアイテムを取得する DataClass c = value as DataClass; // メニュー項目用リストを準備 List<MenuItem> tmpList = new List<MenuItem>(); // メニュー項目作成 // それぞれの項目に合わせていろいろやる。 MenuItem i = new MenuItem { Header = c.Name + " の詳細を開く", }; tmpList.Add(i); // 返す return tmpList; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
これでコンテキストメニューを長押しすると、コンバータの Convert メソッドが呼ばれて MenuItem のリストを返すことで、動的にコンテキストメニュー項目を変更・表示する事が出来ます。
コンテキストメニューから ViewModel のコマンドをたたく
ここまででコンテキストメニューの動的構成が出来るようになったのですが、このままでは選択できるだけで何も実行することが出来ません。
次にコンテキストメニューから ViewModel のコマンドを実行してみましょう。
まず実装方針は2つあります。
- ViewModel に IValueConverter を実装し、ViewModel にコンバータを兼ねさせる
- ViewModel と コンバータは別のソース(クラス) にし、コンバータに ViewModel を持たせる
まず最初の実装ですが、これは簡単ですね。
サンプルの ViewModel.cs がそのようになっていますので、参照してください。
次のコンバータに ViewModel を持たせる方法ですが、まずコンバータのプロパティに ViewModel を追加して、MenuItem 生成時に Command と CommandParameter を設定してやります。
public class ContextMenuConverter : IValueConverter { public ViewModel viewModel { get; set; } public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { // 選択されたアイテムを取得する DataClass c = value as DataClass; // メニュー項目用リストを準備 List<MenuItem> tmpList = new List<MenuItem>(); // メニュー項目作成 MenuItem i = new MenuItem { Header = c.Name + " の詳細を開く", Command = this.viewModel.HeaderClickCommand, CommandParameter = value, }; tmpList.Add(i); // 返す return tmpList; } public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { throw new NotImplementedException(); } }
次に XAML で、Converter 定義で viewModel プロパティに ViewModel を設定します。
必ず ViewModel の定義の後にコンバータを定義してください。そうしないとコンバータに設定される ViewModel が null になってしまいます。
<phone:PhoneApplicationPage.Resources> <local:ViewModel x:Key="ViewModel" d:IsDataSource="True"/> <local:ContextMenuConverter viewModel="{StaticResource ViewModel}" x:Key="ContextMenuConverter"/>
サンプルについて
サンプルでは上記の実装方針両方とも実装してありますが、デフォルトは ViewModel にコンバータを兼用させているものです。
ViewModel とコンバータを分ける方を実行する場合は、
- 変更前
<toolkit:ContextMenu ItemsSource="{Binding Converter={StaticResource ViewModel}}"/>
- 変更後
<toolkit:ContextMenu ItemsSource="{Binding Converter={StaticResource ContextMenuConverter}}"/>
としてください。