WPF:将 ContextMenu 绑定到 MVVM 命令

Posted

技术标签:

【中文标题】WPF:将 ContextMenu 绑定到 MVVM 命令【英文标题】:WPF: Binding a ContextMenu to an MVVM Command 【发布时间】:2011-04-04 18:04:54 【问题描述】:

假设我有一个带有返回 Command 的属性的 Window(实际上,它是 ViewModel 类中带有 Command 的 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”进行绑定的源。绑定表达式:路径=我的命令;数据项=空;目标元素是'MenuItem'(名称='');目标属性是“命令”(输入“ICommand”)

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

【问题讨论】:

+1 用于编辑您的解决方案,您应该将其作为单独的答案 【参考方案1】:

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

查看this blogpost,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));

然后可以在 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>

【讨论】:

在我尝试了大约 10 种不同的方法(来自 SO 和其他地方)之后,这终于起作用了。非常感谢这个干净且非常简单但非常棒的答案! :) 这是最佳解决方案 这是一个非常好的解决方案。我将绑定代理设置为强类型(数据属性和依赖属性不是 typeof(object) 而是 typeof(MyViewModel)。这样我必须通过代理绑定的地方有更好的智能感知。【参考方案2】:

为web.archive.org 万岁!这里是the missing blog post:

绑定到 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 属性。然后,您可以传递此上下文 向下浏览您的菜单项。

我承认这不是世界上最优雅的解决方案。 但是,它胜过在后面的代码中设置东西。如果有人有 更好的方法,我很想听听。

【讨论】:

奇怪的是,我设置了DataContextMenuItem,但它不起作用。正如您所描述的,一旦我将其更改为设置在ContextMenu 上,它就开始工作了。感谢您发布此内容。【参考方案3】:

我发现它对我不起作用,因为菜单项是嵌套的,这意味着我必须遍历一个额外的“父级”才能找到 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>

【讨论】:

【参考方案4】:

基于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>

【讨论】:

这真的有效吗?我一直试图让这个工作,并且使用 snoop 似乎命令被评估一次并且从未真正更新过。 PlacementTarget 在上下文菜单被实际激活之前为空,此时 Parent.PlacementTarget.Tag 是有效的,但命令永远不会动态更新(从我在 Snoop 中可以看到) 这实际上是唯一对我有用的东西,我已经尝试了来自整个网站的 10-15 条建议。【参考方案5】:

如果(像我一样)你讨厌丑陋的复杂绑定表达式,这里有一个简单的代码隐藏解决方案来解决这个问题。这种方法仍然允许您在 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;
         
    

【讨论】:

【参考方案6】:

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/

【讨论】:

以上是关于WPF:将 ContextMenu 绑定到 MVVM 命令的主要内容,如果未能解决你的问题,请参考以下文章

WPF ContextMenu:MenuItem 图标可见性绑定错误

[WPF]解决模板中ContextMenu绑定CommandParameter的问题

WPF ContextMenu 在MVVM模式中无法绑定 Command的解决办法

WPF ContextMenu 在MVVM模式中绑定 Command及使用CommandParameter传参

WPF中ContextMenu怎么控制显示与不显示

将 ContextMenu 的 MenuItem 可见性绑定到 ListView 选择