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

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

TOC

前回の続きです。

MenuItemのIEnumerableをContextMenuのItemsSourceに バインドすることで一応狙った通りに動作したのですが、 ViewModelにコントロールのMenuItemが入るのはどうも気持ち悪いので、 違う方法を考えてみます。

まず、メニュー項目を定義したPOCO(ContextMenuItemDefinitionクラス)の IEnumerableをContextMenuのItemsSourceにバインドします。 ContextMenuItemDefinitionクラスは、とりあえず項目名(Text)を定義しておきます。

#!C#
public partial class MainPage : PhoneApplicationPage
    {
        public MainPage()
        {
            InitializeComponent();
            LayoutRoot.DataContext = new ViewModel();
        }
 
        private void MenuItem_Click(object sender, RoutedEventArgs e)
        {
            MenuItem menuItem = sender as MenuItem;
            return;
        }
    }
 
    public class ContextMenuItemDefinition
    {
        public string Text { get; set; }
    }
     
    public class ViewModel
    {
        public ObservableCollection<ListItem> ListItems { get; private set; }
        public ObservableCollection<ContextMenuItemDefinition> MenuDefs { get; private set; }
 
        public ViewModel()
        {
            this.MenuDefs = new ObservableCollection<ContextMenuItemDefinition>()
            {
                new ContextMenuItemDefinition() {Text="Menu A"},
                new ContextMenuItemDefinition() {Text="Menu B"},
                new ContextMenuItemDefinition() {Text="Menu 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 ListItem
    {
        public string Text { get; set; }
        public ObservableCollection<ContextMenuItemDefinition> MenuItems { get; set; }
    }

XAMLでは、ContextMenuのItemTemplateにメニュー項目の表示内容を定義します。 まず、メニューの各項目にはMenuItemが含まれるのだからと思って、次のように変更してみました(これは間違いだったのですが)。

#!XML
<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:ContextMenu.ItemTemplate>
                                    <DataTemplate>
                                        <toolkit:MenuItem Header="{Binding Text}" Click="MenuItem_Click"/>
                                    </DataTemplate>                                    
                                </toolkit:ContextMenu.ItemTemplate>
                            </toolkit:ContextMenu>
                        </toolkit:ContextMenuService.ContextMenu>
                    </TextBlock>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>

■実行結果

実行したところ、コンテキストメニューが表示されるのですが、なんか空間が空いていておかしな表示です。

実行結果

また、メニューを選択するとイベントハンドラが呼ばれるのですが、その後にコンテキストメニューが閉じられません。

実行結果

原因を調べたところ、Windows Phone ToolkiのIssueTrackerに 同じ問題が登録されていました。 その回答によると、

It’s incorrect to have a MenuItem in the ContextMenu’s DataTemplate. Either one shouldn’t specify MenuItems at all (and the ItemsControl base class will create them automatically), or one should specify them via the Items property (and they’ll be used as-is). What it looks like from the sample code is that there will be a nested MenuItem inside each real MenuItem and I can totally imagine that interfering with the ContextMenu’s close behavior.

となっていて、MenuItemをDataTemplateのなかで使うなとあります。 そして、「MenuItemを指定せずに(データを与えて)、ItemsControlにMenuItemを生成させる」か 「Itemsプロパティを経由してMenuItemを指定して、そのまま使用させる」のどちらかとあります。

後者は前回にやったことなので、前者の方法で実装する必要があります。それを試してみた過程・結果は次の記事に回します。