QA@IT

メニューバー(Menu)と他のコントロールのコンテキストメニューでMenuItemを共有できますか?

8141 PV

WPFでXAMLの書き方について質問です。

例としてMenuに「編集」ヘッダを作り、編集に関するMenuItemが表示されるようになっているとき、
同じウィンドウ内にあるListViewの行を右クリックで表示できるコンテキストメニューでも、それと同じ内容のMenuItemを表示させたいと思っています。

両方にベタ書きすれば一番簡単ではありますが、それ以外にテンプレートを共有するなどの方法で共通化を図ることはできますでしょうか?
C#言語を使用していますが、なるべくXAMLで完結する効率的な方法を探しています。
よろしくお願いします。

回答

はたしてこれがまっとうな方法なのかどうか、自分でもよくわからないのですが(^_^;)
以下の様な感じで実現はできました。

<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>
        <ItemsControl x:Key="EditMenus" x:Shared="false">
            <MenuItem Header="foo" Click="MenuItem_Click"/>
            <MenuItem Header="bar" Click="MenuItem_Click"/>
            <MenuItem Header="baz" Click="MenuItem_Click"/>
        </ItemsControl>
    </Window.Resources>
    <DockPanel LastChildFill="True">
        <Menu DockPanel.Dock="Top">
            <MenuItem Header="編集" ItemsSource="{Binding Source={StaticResource EditMenus}, Path=Items}"/>
        </Menu>
        <ListView>
            <ContextMenuService.ContextMenu>
                <ContextMenu ItemsSource="{Binding Source={StaticResource EditMenus}, Path=Items}"/>
            </ContextMenuService.ContextMenu>
            <Ellipse Width="100" Height="100" Fill="Red"/>
        </ListView>
    </DockPanel>
</Window>

リソースディクショナリに ItemsControl を用意して要素として MenuItem を追加しておき、それをソースとしてバインディングする、というわけですが、ItemsControl の属性に x:Shared="false" を指定するのがミソで、これがないと MenuItem の親がメニューかコンテキストメニューどちらか一つになってしまいますね。

x:Shared 属性やその他の XAML の言語機能についての詳細は http://msdn.microsoft.com/ja-jp/library/vstudio/aa970778.aspx から辿ってご一読いただければと思います。

なるべく XAML で、ということで ItemsControl を利用しましたが、何か MenuItem を要素とするコレクション型を定義してそれを使ってもよいかもしれません(試していないので想像だけですが)。

何か他によい方法があれば、私もぜひ知りたいです。

編集 履歴 (0)
  • x:Shared知りませんでした、別インスタンスだけど共有できるわけですね参考になります。そしてMenuItem単品の話と思っていた私 orz。質問者じゃないのに便乗ですがこれでアイテムの中身をC#側で動的に変えたい場合(Headerをhogeにする等)はResourcesからいじって両方Bindしなおすんですか? -
  • ありがとうございます!これぞ自分が求めていた答えです。
    答えから逆に導くとItemsControlの存在が知りたかったということになります。
    (flied_onionさん、上手く伝わってなかったのならごめんなさい)
    自分もコレクションを使うべきなのかという考えはありました。
    それにしてもx:Sharedは同じく初耳です…(エディタの補完で出て来ませんね)
    -
  • 確かに IntelliSense の候補にも出ないので存在に気づきにくいですよね…(自分もごく最近知りました)。動的にアイテムをいじるとなると、 flied_onion さんの仰る通り何かのタイミングでリソースそのものをいじるか、メニューが開くイベントで MenuItem を辿って手を加えるか… しかしそこまで行くともう素直に ViewModel を用意してバインドする方が良いような気もしますね〜 -

CommandBindingsを利用すれば可能だと思います。
必要に応じてStyleも作成して適用するといいのかもしれません。

イベントハンドラはcs側に以下があるものとして

  private void OnEdit(object sender, ExecutedRoutedEventArgs e)
  {
    MessageBox.Show("OnEdit");
  }

以下の様にXAMLを記述します。
Commandの定義(RoutedCommandの定義)は csに書くこともできますが、要望にありますのでXAMLで定義します。

<Window x:Class="QaAtItDupMenuItemWpf.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <!--既存のコマンドではなく自作した-->
    <RoutedCommand x:Key="MyEditRouted" />

    <Style x:Key="MyEditStyle" TargetType="MenuItem">
      <Setter Property="Header" Value="編集"/>
    </Style>
  </Window.Resources>
  <!-- 作成したRoutedCommand(自作のCommand)とイベントハンドラを結びつける-->
  <Window.CommandBindings>
    <CommandBinding Command="{StaticResource MyEditRouted}" Executed="OnEdit" />
  </Window.CommandBindings>
  <Grid>
    <Menu Height="24" VerticalAlignment="Bottom" Margin="0,0,0,290">
      <MenuItem Style="{StaticResource MyEditStyle}" Command="{StaticResource MyEditRouted}" />
      <MenuItem Style="{StaticResource MyEditStyle}" Command="{StaticResource MyEditRouted}" />
      <MenuItem Style="{StaticResource MyEditStyle}" Command="{StaticResource MyEditRouted}" />
      <MenuItem Header="へんしゅう"  Command="{StaticResource MyEditRouted}" />
    </Menu>
  </Grid>
</Window>

このサンプルでは4つのMenuItemが全て同じイベントハンドラに紐づいています。
また、一番右のMenuItem以外は同じStyleが適用されています。
Styleまたはハンドラが変更される場合はBindingCommandやStyleの定義を変更するだけで済みます。

CommandBindingの属性Commandに設定するCommand(RoutedCommand)は面倒なので作成してしまいました。
もともと存在する ApplicationCommands.Newコマンドを使用した場合は以下の様に短くかけますね。
<CommandBinding Command="New" Executed="MyHandler" />

コマンドになにがあるか知りたければ以下のキーワードで検索するといいと思います。
System.Windows.Input.ApplicationCommands

編集 履歴 (0)
  • 例だと同じメニュー内にしてますが、コンテキストメニューとメニューでも行けると思います。 -
  • ご回答ありがとうございます。
    ですがCommandの共有に関しては存じておりまして、
    私が求めているものは、MenuItemそのもののリソースをMenuとListViewのコンテキストメニューで共有できるかどうかという事です。
    今のところ検索してもそれっぽいものは出てこないので、やはりできないのかなとは感じています。
    -
  • 同じインスタンスを複数個所にって意味ですね。多分 C#使っても無理かと思います(そもそも親を二つもてないため)。Window.Resourcesに置いといてメニューが開くイベントで動的に追加なら可能かもしれませんがやっていないのでわかりません。 -
  • 余談:WinFormsでToolStripMenuItemを2か所目に追加すると前の親から自動で削除されます。WPFの C#でやると例外がでますね。なんだろうこの地味な違い・・・(しかも XAMLで ContentControlのContentでStaticResource指定の時はWinForms同様自動的に親が解除されるという)。 -
  • 補足ありがとうございます!
    良いか悪いかはさておいて意図してる内容としてはyito様の回答がズバリでした。
    しかしWinForms感覚でやってるとWPFで全然思い通りにできませんね(@@;
    -
ウォッチ

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