QA@IT

[Livet]MenuItemに対するCommandのバインドが働きません

7274 PV

C#でLivetを使ってViewを作成しています。
そこで質問ですが、ViewModelで以下のようなExportコマンドの定義があるとします。(Livetのテンプレどおりです)

#region ExportCommand
private ViewModelCommand _ExportCommand;

public ViewModelCommand ExportCommand
{
    get
    {
        if (_ExportCommand == null)
        {
            _ExportCommand = new ViewModelCommand(Export, CanExport);
        }
        return _ExportCommand;
    }
}

public bool CanExport()
{
    //実行可能判定
}

public void Export()
{
    using (var sfd = new CommonSaveFileDialog())
    {
        //エクスポート処理
    }
}
#endregion

これに対してView側のXAMLでは

<Window.Resources>
    <ItemsControl x:Key="MenuItems" x:Shared="False">
        <MenuItem Header="エクスポート...(_E)" Command="{Binding ExportCommand}" />
    </ItemsControl>
</Window.Resources>

...

<Menu>
    <MenuItem Header="ファイル(_F)" ItemsSource="{Binding Items, Source={StaticResource MenuItems}}"/>
</Menu>

...

<ListView ItemsSource="{Binding ListViewItems}">
    <ListView.Resources>
        <ContextMenu x:Key="ListViewItemContextMenu" ItemsSource="{Binding Items, Source={StaticResource MenuItems}}"/>
    </ListView.Resources>
    <ListView.ItemContainerStyle>
        <Style TargetType="{x:Type ListViewItem}">
            <Setter Property="ContextMenu" Value="{StaticResource ResourceKey=ListViewItemContextMenu}"/>
        </Style>
    </ListView.ItemContainerStyle>
    ...
</ListView>

といったようにメニューバーとListViewのコンテキストメニューでMenuItemを共有しています。
このようにすると、メニューをクリックしてもコマンドが実行されませんし、実行可能判定も反映されません。

またWindow.ResouceでのMenuItemの共有をやめ、MenuとContextMenuにベタ書きした場合ではMenuの方は正常に動作するものの、どうしてもContextMenuでは思い通りに動きません。

どなたか解決方法をご存知でしたらご教示お願い致します。

追記

またWindow.ResouceでのMenuItemの共有をやめ、MenuとContextMenuにベタ書きした場合ではMenuの方は正常に動作するものの、どうしてもContextMenuでは思い通りに動きません。

こちらに関してはListViewのItemSourceがDataContextとなっていたのが原因でしたので、RelativeSourceでWindowをたどる事で解決しました。

回答

うーん、リソースで ItemsControl/MenuItem がインスタンス化されてしまうと DataContext が伝播してこない、という感じでしょうか… (>_<)

もうコマンドバインディングは諦めてイベントハンドラで、というのも何だか悔しいので、実現方法をひねり出してみました。

(ちなみに Livet については存じておりませんでしたが、とりあえず普通に ICommand をバインドするのと同じだろうということで…)

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="300" Width="300">
    <Window.Resources>
        <CompositeCollection x:Key="MenuItems" x:Shared="False">
            <MenuItem Header="エクスポート" Command="{Binding ExportCommand}"/>
        </CompositeCollection>
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <Menu DockPanel.Dock="Top" DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}">
            <MenuItem Header="ファイル" ItemsSource="{Binding Source={StaticResource MenuItems}}"/>
        </Menu>
        <ListView>
            <ContextMenuService.ContextMenu>
                <ContextMenu DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Mode=Self}}" 
                             ItemsSource="{Binding Source={StaticResource MenuItems}}"/>
            </ContextMenuService.ContextMenu>
        </ListView>
    </DockPanel>
</Window>

方針としては DataContext が届かないなら無理やりにでも設定してやる!という感じで、変更点は、

  • リソースの ItemsControlCompositeCollection に変更
  • それに伴い、ItemsSource へのバインディングで Items パスの指定は不要に
  • Menu、ContextMenu それぞれの DataContext に何とかしてそれらしきものをバインド

といったところです。

ItemsControl を CompositeCollection に変更したのは、そうしないと動かなかったから(^^; なのですが… CompositeCollection が FrameworkElement ではないので、MenuItem がリソース内でインスタンス化されても親が設定されず、実際にメニューのアイテムにバインドされた段階でうまく DataContext が設定されるのかな?と考えています。

一方その DataContext の設定についてですが、Menu の場合は適当に先祖をたどればいいだろうということで Window の DataContext をセットしていますが、ContextMenu のほうは同じ方法ではダメだったので、PlacementTarget プロパティで親を参照してやることで解決しました。

奥が深いというか、ややこしいというか… 自分がまだ WPF についてよくわかってないからだと思いますが、なかなか手ごわいですね。

ともあれ、多少なりとも参考になれば幸いです。

編集 履歴 (1)
  • お世話様です。CompositeCollectionとPlacementTargetですか…また初めて聞くものが出て来ました。難しい(@_@;)
    うーむ、惜しいです。何が惜しいかというとこの方法でやってみたところ確かにコマンドがちゃんと効くようにはなりました。しかしながら実行不能の場合にグレー表示にならないという謎の状態になります…(クリックしても反応は無い)
    そちらは大丈夫でしたでしょうか?
    -
  • あれれ…仰る通りですね…。すみません、確認不足でした。一度メニューを表示させた後、コマンドの CanExecuteChanged イベントを発動させると、その後は CanExecute() に応じてグレー表示になるようですが…。ぐぬぬぬ -
ウォッチ

この質問への回答やコメントをメールでお知らせします。