前回では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へのバインドによってメニュー項目を 定義することができるのではないでしょうか。
なお、もっとよい方法があったり、私の思い違いがありましたら、是非ご指摘ください。