web-dev-qa-db-ja.com

WPF:ContextMenuをMVVMコマンドにバインドする

コマンドを返すプロパティを持つウィンドウがあるとしましょう(実際、それはViewModelクラスのコマンドを持つUserControlですが、問題を再現するためにできる限り単純にしましょう)。

次の作品:

<Window x:Class="Window1" ... x:Name="myWindow">
    <Menu>
        <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
    </Menu>
</Window>

ただし、以下は機能しません。

<Window x:Class="Window1" ... x:Name="myWindow">
    <Grid>
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
            </ContextMenu>            
        </Grid.ContextMenu>
    </Grid>
</Window>

私が得るエラーメッセージは

System.Windows.Dataエラー:4:参照 'ElementName = myWindow'のバインディングのソースが見つかりません。 BindingExpression:Path = MyCommand; DataItem = null;ターゲット要素は 'MenuItem'(Name = '');です。ターゲットプロパティは 'Command'(タイプ 'ICommand')です

どうして?そして、どうすればこれを修正できますか? DataContextの使用はオプションではありません。この問題は、表示されている実際のデータがDataContextに既に含まれているビジュアルツリーのずっと下で発生するためです。代わりに{RelativeSource FindAncestor, ...}を使用してみましたが、同様のエラーメッセージが表示されます。

29
Heinzi

問題は、ContextTreeがビジュアルツリーにないことです。そのため、基本的には、使用するデータコンテキストについてコンテキストメニューに通知する必要があります。

このブログ投稿 をチェックして、Thomas Levesqueの非常に素晴らしいソリューションを確認してください。

彼はFreezableを継承し、Data依存プロパティを宣言するクラスProxyを作成します。

public class BindingProxy : Freezable
{
    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxy();
    }

    public object Data
    {
        get { return (object)GetValue(DataProperty); }
        set { SetValue(DataProperty, value); }
    }

    public static readonly DependencyProperty DataProperty =
        DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}

次に、XAMLで宣言できます(ビジュアルツリー内の正しいDataContextがわかっている場所)。

<Grid.Resources>
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>

そして、ビジュアルツリーの外のコンテキストメニューで使用されます。

<ContextMenu>
    <MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>
42
Daniel

万歳 web.archive.org !これが 行方不明のブログ投稿 です。

WPFコンテキストメニューのMenuItemへのバインド

2008年10月29日水曜日— jtango18

WPFのContextMenuは、ページ/ウィンドウ/コントロール自体のビジュアルツリー内に存在しないため、データバインディングは少し難しい場合があります。私はこれについてウェブ全体で高低を検索しましたが、最も一般的な答えは「コードビハインドでそれを行うだけ」のようです。違う! XAMLのすばらしい世界に戻って、コードの背後でコードを実行することに戻りませんでした。

これが、ウィンドウのプロパティとして存在する文字列にバインドできるようにするための私の例です。

public partial class Window1 : Window
{
    public Window1()
    {
        MyString = "Here is my string";
    }

    public string MyString
    {
        get;
        set;

    }
}

    <Button Content="Test Button" Tag="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}">
        <Button.ContextMenu>
            <ContextMenu DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}" >
                <MenuItem Header="{Binding MyString}"/>
            </ContextMenu>
        </Button.ContextMenu>
    </Button>

重要な部分はボタンのタグです(ボタンのDataContextを設定するのと同じくらい簡単にできます)。これは、親ウィンドウへの参照を格納します。 ContextMenuは、PlacementTargetプロパティを介してこれにアクセスできます。次に、このコンテキストをメニュー項目に渡すことができます。

これは世界で最もエレガントなソリューションではないことを認めます。ただし、コードビハインドでの設定に勝るものはありません。これを行うためのより良い方法を誰かが持っているなら、私はそれを聞いてみたいです。

16
mydogisbox

メニュー項目が入れ子になっているために機能していないことがわかりました。つまり、PlacementTargetを見つけるために余分な「親」を上に移動する必要がありました。

より良い方法は、ContextMenu自体をRelativeSourceとして見つけ、その配置ターゲットにバインドすることです。また、タグはウィンドウ自体であり、コマンドはビューモデルにあるため、DataContextも設定する必要があります。

私はこのようなものになってしまいました

<Window x:Class="Window1" ... x:Name="myWindow">
...
    <Grid Tag="{Binding ElementName=myWindow}">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding PlacementTarget.Tag.DataContext.MyCommand, 
                                            RelativeSource={RelativeSource Mode=FindAncestor,                                                                                         
                                                                           AncestorType=ContextMenu}}"
                          Header="Test" />
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>

つまり、サブメニューなどを含む複雑なコンテキストメニューが表示される場合、各レベルのコマンドに「親」を追加し続ける必要はありません。

-編集-

また、Window/UsercontrolにバインドするすべてのListBoxItemにタグを設定するためのこの代替案も思い付きました。各ListBoxItemは独自のViewModelで表されていたため、これを行うことになりましたが、コントロールのトップレベルのViewModelを介して実行するメニューコマンドが必要でしたが、リストのViewModelをパラメーターとして渡しました。

<ContextMenu x:Key="BookItemContextMenu" 
             Style="{StaticResource ContextMenuStyle1}">

    <MenuItem Command="{Binding Parent.PlacementTarget.Tag.DataContext.DoSomethingWithBookCommand,
                        RelativeSource={RelativeSource Mode=FindAncestor,
                        AncestorType=ContextMenu}}"
              CommandParameter="{Binding}"
              Header="Do Something With Book" />
    </MenuItem>>
</ContextMenu>

...

<ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListBoxItem}">
        <Setter Property="ContextMenu" Value="{StaticResource BookItemContextMenu}" />
        <Setter Property="Tag" Value="{Binding ElementName=thisUserControl}" />
    </Style>
</ListView.ItemContainerStyle>
8
nrjohnstone

回避策については、Justin Taylorの this の記事を参照してください。

更新
残念ながら、参照されたブログはもう利用できません。私は別のSO-answerで手続きを説明しようとしました。それは here で見つかります。

6
HCL

HCLの回答 に基づいて、これは私が最終的に使用したものです:

<Window x:Class="Window1" ... x:Name="myWindow">
    ...
    <Grid Tag="{Binding ElementName=myWindow}">
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding Parent.PlacementTarget.Tag.MyCommand, 
                                            RelativeSource={RelativeSource Self}}"
                          Header="Test" />
            </ContextMenu>
        </Grid.ContextMenu>
    </Grid>
</Window>
4
Heinzi

(私のように)醜い複雑なバインディング式が嫌いな場合、この問題の簡単なコードビハインドソリューションを次に示します。このアプローチでも、XAMLでコマンドの宣言を簡潔に保つことができます。

XAML:

<ContextMenu ContextMenuOpening="ContextMenu_ContextMenuOpening">
    <MenuItem Command="Save"/>
    <Separator></Separator>
    <MenuItem Command="Close"/>
    ...

コードビハインド:

private void ContextMenu_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{
    foreach (var item in (sender as ContextMenu).Items)
    {
        if(item is MenuItem)
        {
           //set the command target to whatever you like here
           (item as MenuItem).CommandTarget = this;
        } 
    }
}
2
Tom Makin