ContextMenuへのバインディング(1of3)

Posted by 技術ブログ by Strawhat.net on Sunday, June 26, 2011

Windows Phone ToolkitのContextMenuのサンプルコードでは

#!XML
<toolkit:ContextMenuService.ContextMenu>
    <toolkit:ContextMenu>
        <toolkit:MenuItem Header="Item1"/>
        <toolkit:MenuItem Header="Item2"/>
        <toolkit:MenuItem Header="Item3"/>
    </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

というように、MenuItemで明示的にメニュー項目を定義している場合をよく見ます。

今回、以下の条件を満たすためにContextMenuのItemsSourceを使う方法を試していましたが、 すっかりはまっていたので、その過程をまとめてみます。

  • ListBoxの各要素をTap&Holdすることでコンテキストメニューを表示させる
  • 複数の画面から利用されるため、ListBoxと付随するコントロールをUserControlとしてまとめる
  • コンテキストメニューを画面によって変更する必要がある

以下では、話を単純にするために、ListBoxだけを含むWindowsPhoneページで説明していきます。

1st Step) 単純にMenuItemのリストをItemsSourceにバインド

まず初めに考えたのがContextMenuを構成するMenuItemをViewModelで定義して、 そのリストをContextMenu.ItemsSourceに設定するもの。コードは以下のようになります。

■XAML

#!XML
<phone:PhoneApplicationPage
    x:Class="WP7ContextMenuWithMenuItem.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:toolkit="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls.Toolkit"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    shell:SystemTray.IsVisible="True">
 
    <Grid x:Name="LayoutRoot" Background="Transparent">
        <ListBox Grid.Row="0" x:Name="ListBox" ItemsSource="{Binding ListItems}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Margin="0" TextWrapping="Wrap" Text="{Binding Text}" Style="{StaticResource PhoneTextLargeStyle}">
                        <toolkit:ContextMenuService.ContextMenu>
                            <toolkit:ContextMenu ItemsSource="{Binding MenuItems}" IsZoomEnabled="False"/>
                        </toolkit:ContextMenuService.ContextMenu>
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</phone:PhoneApplicationPage>

■コードビハインド&ViewModel

#!C#
public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            LayoutRoot.DataContext = new ViewModel();
        }
    }
 
    public class ViewModel
    {
        public ObservableCollection<ListItem> ListItems { get; private set; }
 
        public ViewModel()
        {
            this.ListItems = new ObservableCollection<ListItem>(){
                new ListItem() { Text="Item 1", MenuItems=this.CreateMenuDefs(), },
                new ListItem() { Text="Item 2", MenuItems=this.CreateMenuDefs(), },
                new ListItem() { Text="Item 3", MenuItems=this.CreateMenuDefs(), },
            };
        }
 
        private ObservableCollection<MenuItem> CreateMenuDefs()
        {
            ObservableCollection<MenuItem> menuDefs
                = new ObservableCollection<MenuItem>() {
                new MenuItem() { Header="Menu 1" },
                new MenuItem() { Header="Menu 2" },
            };
            foreach (var item in menuDefs)
            {
                // イベントハンドラをここで設定する
                item.Click += new RoutedEventHandler(MenuItem_Click_VM);
            }
 
            return menuDefs;
        }
 
        private void MenuItem_Click_VM(object sender, RoutedEventArgs e)
        {
            MenuItem menuItem = sender as MenuItem;
            return;
        }
    }
 
    public class ListItem
    {
        public string Text { get; set; }
        public ObservableCollection<MenuItem> MenuItems { get; set; }
    }

補足

ItemsSourceにバインドしたMenuItemsですが、ItemTemplateのなかで定義しているため、 ListBoxのItemsSourceにバインドしたリストの要素、つまりListItemクラスに定義する必要があります。

実行結果

ViewModelで定義したコンテキストメニューが表示されて、項目を選択するとイベントハンドラが呼ばれました。

実行結果

ただ、ViewModelにコントロールであるMenuItemが入り込むため、 ViewModelとViewの分離の観点からこの方法はよくないと考えています。

このため、ContextMenu.ItemsSourceにはPOCOのリストを設定することができないか試してみました。 その過程・結果は次の記事に回します。