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

Posted by 技術ブログ by Strawhat.net on Monday, June 27, 2011

前回ではContextMenuのItemTemplateにMenuItemを含めると、 項目を選択してもコンテキストメニューが閉じない問題がありました。

MenuItemでなくTextBlockを使うと表示にも選択時の動作も問題ないのですが、 メニュー項目を選択したときのイベントハンドラ、もしくはCommandを設定できる 場所がありません…ないはずです。もし私の思い違いならご指摘ください。

#!XML
<toolkit:ContextMenuService.ContextMenu>
    <toolkit:ContextMenu ItemsSource="{Binding MenuItems}" IsZoomEnabled="False">
        <toolkit:ContextMenu.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Text}"/>
            </DataTemplate>                                    
        </toolkit:ContextMenu.ItemTemplate>
    </toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>

これまでをまとめると、以下の問題をクリアしつつContextMenuへのバインドを実現する必要があるのです。

  • ContextMenuのItemsSourceにバインドするViewModelのプロパティに、MenuItemは使わない。
  • ContextMenuのItemTempalteでMenuItemは使わない。でもMenuItem以外の要素を使うと、選択時のイベント・コマンドを定義できない。→つまり、ItemTemplateは使えない。

これを実現する方法にしばらく悩んでいたのですが、結局は以下の方針で対応することにしました。

  • ViewModelではPOCOのIEnumerableを定義する。
  • ContextMenuのItemTemplateは使用しない。
  • ContextMenuのItemsSourceにMenuItemのIEnumerableがバインドされるように、コンバーターを使ってPOCOからMenuItemへと変換する。

最初はコンバーターを持ち出すのはどうかと思ったのですが、 Viewに属するコンバーターであればUIコントロールのMenuItemが出てきても問題ないですし、 ContextMenuのItemTemplateを使ったときにイベントハンドラ・コマンドを定義できるように、 ContextMenuが改善されるまでは、この方法でもよいかと考えています。

ソースコードの関係する箇所は以下の通りです。 コンバーターでの処理の都合で、イベントハンドラでなく、コマンドを使っています。

■XAML

#!XML
<phone:PhoneApplicationPage.Resources>
    <local:MenuItemConverter x:Key="MenuItemConverter"/>
</phone:PhoneApplicationPage.Resources>
 
<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, Converter={StaticResource MenuItemConverter}}"
                            IsZoomEnabled="False">
                        </toolkit:ContextMenu>
                    </toolkit:ContextMenuService.ContextMenu>
                </TextBlock>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</Grid>

■コードビハインド&ViewModel

#!C#
public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            LayoutRoot.DataContext = new ViewModel();
        }
    }
 
    public class ListItem
    {
        public string Text { get; set; }
        public ObservableCollection<ContextMenuItemDefinition> MenuItems { get; set; }
    }
 
    public class ContextMenuItemDefinition
    {
        public string Text { get; set; }
        public ICommand Command { get; set; }
        public string CommandParameter { get; set; }
    }
 
    public class ViewModel
    {
        public ObservableCollection<ListItem> ListItems { get; private set; }
        public ObservableCollection<ContextMenuItemDefinition> MenuDefs { get; private set; }
        public ICommand AlwaysCommand { get; private set; }
 
        public ViewModel()
        {
            this.AlwaysCommand = new AlwaysICommand();
 
            this.MenuDefs = new ObservableCollection<ContextMenuItemDefinition>()
            {
                new ContextMenuItemDefinition() {Text="Menu A", Command=AlwaysCommand, CommandParameter="A"},
                new ContextMenuItemDefinition() {Text="Menu B", Command=AlwaysCommand, CommandParameter="B"},
                new ContextMenuItemDefinition() {Text="Menu C", Command=AlwaysCommand, CommandParameter="C"},
            };
 
            this.ListItems = new ObservableCollection<ListItem>(){
                new ListItem() { Text="Item 1", MenuItems=MenuDefs, },
                new ListItem() { Text="Item 2", MenuItems=MenuDefs, },
                new ListItem() { Text="Item 3", MenuItems=MenuDefs, },
            };
        }
    }
 
    public class AlwaysICommand : ICommand
    {
        public bool CanExecute(object parameter)
        {
            return true;
        }
        public event EventHandler CanExecuteChanged;
        public void Execute(object parameter)
        {
            return;
        }
    }

■コンバーター

#!C#
public class MenuItemConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            ObservableCollection<MenuItem> menuItems
                = new ObservableCollection<MenuItem>();
 
            try
            {
                ObservableCollection<ContextMenuItemDefinition> defs
                    = (ObservableCollection<ContextMenuItemDefinition>)value;
 
                foreach (var def in defs)
                {
                    menuItems.Add(new MenuItem()
                    {
                        Header = def.Text,
                        Command = def.Command,
                        CommandParameter = def.CommandParameter,
                    });
                }
            }
            catch (InvalidCastException)
            {
                // pass
            }
 
            return menuItems;
        }
 
        public object ConvertBack(
            object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

■実行結果

実行するとメニューの表示、選択したときの動作に問題はありません。

実行結果

■まとめ

今回問題であったItemTemplateでメニュー項目を定義した場合に選択された メニュー項目を知る方法がないことは、Silverlight for Windows Phone Toolkitが 改良されていく過程で、たとえばContextMenuが適切なイベントを出すなどして解決されるかと思います。

それまでは、POCOで定義したメニュー項目のIEnumerableを、コンバーターを使って MenuItemのIEnumerableに変換する方法を用いることで、ViewとViewModelを 適切に独立させたままContextMenuのItemsSourceへのバインドによってメニュー項目を 定義することができるのではないでしょうか。

なお、もっとよい方法があったり、私の思い違いがありましたら、是非ご指摘ください。