WPF:将 ContextMenu 绑定到 MVVM 命令

问题描述 投票:0回答:7

假设我有一个带有返回命令属性的窗口(事实上,它是一个在 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;数据项=空;目标元素是“MenuItem”(名称=“”);目标属性是“Command”(类型“ICommand”)

为什么?我该如何解决这个问题?使用

DataContext
不是一个选项,因为此问题发生在 DataContext 已包含正在显示的实际数据的可视化树中。我已经尝试使用
{RelativeSource FindAncestor, ...}
代替,但这会产生类似的错误消息。

c# wpf data-binding xaml mvvm
7个回答
76
投票

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

查看这篇博文,其中包含 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>

18
投票

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 属性。然后你可以传递这个上下文 向下浏览菜单项。

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


12
投票

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

这意味着,如果您最终得到一个带有子菜单等的复杂上下文菜单……您不需要继续向每个级别的命令添加“父级”。

-- 编辑 --

还提出了这个替代方案,在绑定到窗口/用户控件的每个 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>

9
投票

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

3
投票

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

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/


0
投票

我的情况有点不同,因为我为 ListView 设置

ItemContainerStyle
并希望每个
ListViewItem
有一个上下文菜单。但它的工作原理与 nrjohnstone 的答案相同:

<ListView.ItemContainerStyle>
    <Style TargetType="{x:Type ListViewItem}">
        <Setter Property="Tag" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:MyMainWindow}}}"/>
        <Setter Property="ContextMenu">
            <Setter.Value>
                <ContextMenu>
                    <MenuItem Header="Hello! 😉" InputGestureText="Ctrl+H" Command="{Binding PlacementTarget.Tag.DataContext.MyCommand, RelativeSource={RelativeSource AncestorType={x:Type ContextMenu}}}"/>
                </ContextMenu>
            </Setter.Value>
        </Setter>
    </Style>
</ListView.ItemContainerStyle>
© www.soinside.com 2019 - 2024. All rights reserved.