WPF:将ContextMenu绑定到MVVM命令

39

假设我有一个窗口,其中包含一个返回命令的属性(实际上,它是一个带有在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 Error: 4 : 找不到引用为'ElementName=myWindow'的绑定源。BindingExpression:Path=MyCommand; DataItem=null; target element is 'MenuItem' (Name=''); target property is 'Command' (type 'ICommand')

为什么会出现这个问题?如何解决?不能使用DataContext,因为在可视树的下方,DataContext已经包含了实际显示的数据。我已经尝试使用{RelativeSource FindAncestor, ...},但是会产生类似的错误消息。


+1 对于编辑并提供了解决方案。你应该将其作为独立的答案。 - jan
6个回答

76

问题在于ContextMenu不在可视树中,因此您基本上需要告诉上下文菜单使用哪个数据上下文。

请查看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>

1
这个__finally__终于成功了,我尝试了大约10种不同的方法(来自SO和其他地方)。非常感谢您提供如此干净、简单而又如此棒的答案! :) - Yoda
这是最佳解决方案 - n00b101
3
这是一个非常好的解决方案。我会让我的绑定代理强类型化(数据属性和依赖属性不是 typeof(object),而是 typeof(MyViewModel))。这样,在必须通过代理进行绑定的位置,就可以获得更好的智能感知。 - Michael

18

感谢 web.archive.org!以下是缺失的博客文章: Binding to a MenuItem in a WPF Context Menu:

在WPF上下文菜单中绑定MenuItem

2008年10月29日,星期三 — jtango18

因为WPF上下文菜单并不像页面/窗口/控件一样存在于视觉树中,所以数据绑定可能有点棘手。我在网上搜索了很多,最常见的答案似乎是“只需在代码后台中完成”。错误!我不想回到在代码后台做事情的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属性访问它。然后,您可以通过菜单项向下传递此上下文。

我承认这不是世界上最优雅的解决方案。但是,它比在代码后台中设置东西要好。如果有人有更好的方法,请告诉我。


很奇怪,我已经设置了MenuItemDataContext,但它不起作用。当我按照你所描述的在ContextMenu上进行设置时,它开始工作了。感谢您发布这篇文章。 - Nicholas Miller

11

我发现它对我不起作用是因为菜单项被嵌套,这意味着我必须向上遍历一个额外的 "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>
这意味着,如果你的上下文菜单变得复杂,包含子菜单等等,你不需要在每个级别的命令中都添加"Parent"。
-- 编辑 --
此外,我想到了一种替代方法,即在每个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>

8

根据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>

1
这真的能用吗?我一直在尝试让它工作,使用Snoop似乎命令只被评估一次,从未实际更新。PlacementTarget为空,直到上下文菜单实际被激活,此时Parent.PlacementTarget.Tag是有效的,但是从我在Snoop中看到的情况来看,命令从未得到动态更新。 - nrjohnstone
这实际上是我尝试了来自该网站各处的10-15个建议中唯一有效的方法。 - UяošKoт

2

2020年的答案:

我将这个答案留在这里,供其他谷歌搜索此问题的人参考,因为这是第一个显示出来的搜索结果。 这对我有用,比其他建议的解决方案更简单:

<MenuItem Command="{Binding YourCommand}" CommandTarget="{Binding Path=PlacementTarget, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>

如此描述:

https://wpf.2000things.com/2014/06/19/1097-getting-items-in-context-menu-to-correctly-use-command-binding/

这篇文章介绍了如何在上下文菜单中正确使用命令绑定。

2
如果(像我一样)您对丑陋的复杂绑定表达式感到反感,那么这里有一个简单的代码后台解决方案。这种方法仍然允许您在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;
        } 
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接