WP TIPS に戻る

動的にコンテキストメニューを構成し、ViewModel のコマンドをたたく

サンプルプロジェクト contextmenu_viewmodel.zip

WP7 では Silverlight Toolkit 同梱の ContextMenu クラスを使用することで、コンテキストメニューを使用することが出来ます。
たとえば ListBox の項目を長押ししたらメニューを出す XAML は以下のようにします。

contextmenu-01.jpg
<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つあります。

  1. ViewModel に IValueConverter を実装し、ViewModel にコンバータを兼ねさせる
  2. 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}}"/>

としてください。