WPF:将ContextMenu绑定到MVVM命令

Hei*_*nzi 25 c# data-binding wpf xaml mvvm

假设我有一个带有返回命令的属性的窗口(实际上,它是一个带有ViewModel类中的Command的UserControl,但让我们尽可能简单地重现问题).

以下作品:

<Window x:Class="Window1" ... x:Name="myWindow">
    <Menu>
        <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
    </Menu>
</Window>
Run Code Online (Sandbox Code Playgroud)

但以下不起作用.

<Window x:Class="Window1" ... x:Name="myWindow">
    <Grid>
        <Grid.ContextMenu>
            <ContextMenu>
                <MenuItem Command="{Binding MyCommand, ElementName=myWindow}" Header="Test" />
            </ContextMenu>            
        </Grid.ContextMenu>
    </Grid>
</Window>
Run Code Online (Sandbox Code Playgroud)

我得到的错误信息是

System.Windows.Data错误:4:无法找到引用'ElementName = myWindow'的绑定源.BindingExpression:路径= mycommand的; 的DataItem = NULL; target元素是'MenuItem'(Name =''); target属性是'Command'(类型'ICommand')

为什么?我该如何解决这个问题?使用DataContext不是一个选项,因为这个问题发生在可视化树的下方,其中DataContext已经包含正在显示的实际数据.我已经尝试过使用{RelativeSource FindAncestor, ...},但是会产生类似的错误消息.

小智 43

问题是ContextMenu它不在可视化树中,所以你基本上必须告诉Context菜单关于使用哪个数据上下文.

查看这篇博客文章,其中包含一个非常好的Thomas Levesque解决方案.

他创建了一个继承Freezable的类Proxy,并声明了一个Data依赖属性.

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));
}
Run Code Online (Sandbox Code Playgroud)

然后它可以在XAML中声明(在可视化树中已知正确的DataContext的位置):

<Grid.Resources>
    <local:BindingProxy x:Key="Proxy" Data="{Binding}" />
</Grid.Resources>
Run Code Online (Sandbox Code Playgroud)

并在可视树外的上下文菜单中使用:

<ContextMenu>
    <MenuItem Header="Test" Command="{Binding Source={StaticResource Proxy}, Path=Data.MyCommand}"/>
</ContextMenu>
Run Code Online (Sandbox Code Playgroud)

  • 这是一个非常好的解决方案。我使我的绑定代理强类型化(数据属性和依赖属性不是 typeof(object) 而是 typeof(MyViewModel))。这样有更好的智能感知,我必须通过代理进行绑定。 (3认同)

N_A*_*N_A 16

Hurray for 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>
Run Code Online (Sandbox Code Playgroud)

重要的部分是按钮上的标签(尽管您可以轻松设置按钮的DataContext).这将存储对父窗口的引用.ContextMenu能够通过它的PlacementTarget属性访问它.然后,您可以通过菜单项向下传递此上下文.

我承认这不是世界上最优雅的解决方案.但是,它在背后的代码中胜过设置.如果有人有更好的方法来做到这一点,我很乐意听到它.


nrj*_*one 8

我发现它不适合我,因为菜单项是嵌套的,这意味着我必须遍历一个额外的"Parent"才能找到PlacementTarget.

更好的方法是找到ContextMenu本身作为RelativeSource,然后绑定到它的放置目标.此外,由于标记是窗口本身,并且您的命令在viewmodel中,因此您还需要设置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>
Run Code Online (Sandbox Code Playgroud)

这意味着如果你最终得到一个带有子菜单等的复杂上下文菜单,你不需要继续为每个级别的命令添加"父".

- 编辑 -

还提出了这个替代方法,在每个ListBoxItem上设置一个标签,该标签绑定到Window/Usercontrol.我最终这样做是因为每个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>
Run Code Online (Sandbox Code Playgroud)


Hei*_*nzi 7

根据HCLs answer,这就是我最终使用的:

<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>
Run Code Online (Sandbox Code Playgroud)


HCL*_*HCL 6

请参阅Justin Taylor的这篇文章以获得解决方法.

更新
可悲的是,引用的博客不再可用.我试图用另一个SO答案来解释这个过程.它可以在这里找到.