ContextMenu 中 MenuItem 的 ElementName 绑定

Posted

技术标签:

【中文标题】ContextMenu 中 MenuItem 的 ElementName 绑定【英文标题】:ElementName Binding from MenuItem in ContextMenu 【发布时间】:2010-11-04 01:19:14 【问题描述】:

有没有其他人注意到带有 ElementName 的绑定无法正确解析 ContextMenu 对象中包含的 MenuItem 对象?查看此示例:

<Window x:Class="EmptyWPF.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300"
    x:Name="window">
    <Grid x:Name="grid" Background="Wheat">
        <Grid.ContextMenu>
            <ContextMenu x:Name="menu">
                <MenuItem x:Name="menuItem" Header="Window" Tag="Binding ElementName=window" Click="MenuItem_Click"/>
                <MenuItem Header="Grid" Tag="Binding ElementName=grid" Click="MenuItem_Click"/>
                <MenuItem Header="Menu" Tag="Binding ElementName=menu" Click="MenuItem_Click"/>
                <MenuItem Header="Menu Item" Tag="Binding ElementName=menuItem" Click="MenuItem_Click"/>
            </ContextMenu>
        </Grid.ContextMenu>
        <Button Content="Menu" 
                HorizontalAlignment="Center" VerticalAlignment="Center" 
                Click="MenuItem_Click" Tag="Binding ElementName=menu"/>
        <Menu HorizontalAlignment="Center" VerticalAlignment="Bottom">
            <MenuItem x:Name="anotherMenuItem" Header="Window" Tag="Binding ElementName=window" Click="MenuItem_Click"/>
            <MenuItem Header="Grid" Tag="Binding ElementName=grid" Click="MenuItem_Click"/>
            <MenuItem Header="Menu" Tag="Binding ElementName=menu" Click="MenuItem_Click"/>
            <MenuItem Header="Menu Item" Tag="Binding ElementName=anotherMenuItem" Click="MenuItem_Click"/>
        </Menu>
    </Grid>
</Window>

除了 ContextMenu 中包含的绑定之外,所有绑定都可以正常工作。它们在运行时将错误打印到输出窗口。

任何人都知道任何解决方法?这是怎么回事?

【问题讨论】:

这个问题显然与namescopes有关...... ContextMenus 是否默认定义自己的名称范围? 【参考方案1】:

正如其他人所说,“ContextMenu”不包含在可视化树中,“ElementName”绑定不起作用。仅当上下文菜单未在“DataTemplate”中定义时,才可以按照接受的答案的建议设置上下文菜单的“NameScope”。我已经通过使用x:Reference Markup-Extension 解决了这个问题,它类似于“ElementName”绑定,但以不同方式解析绑定,绕过了可视化树。我认为这比使用“PlacementTarget”更具可读性。这是一个例子:

<Image Source="Binding Image">       
    <Image.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Delete" 
                      Command="Binding Source=x:Reference Name=Root, Path=DataContext.RemoveImage"
                      CommandParameter="Binding" />
        </ContextMenu>
    </Image.ContextMenu>
</Image>

根据 MSDN 文档

x:Reference 是 XAML 2009 中定义的构造。在 WPF 中,您可以使用 XAML 2009 功能,但仅适用于非 WPF 标记编译的 XAML。 标记编译的 XAML 和 XAML 的 BAML 形式目前不支持 支持 XAML 2009 语言关键字和功能。

不管这意味着什么……不过对我有用。

【讨论】:

简单,优雅,第一次尝试就可以:这应该被标记为解决方案(真的比 PlacementTarget 更好) MSDN 文档中的引用似乎向我表明,使用 x:Reference 不允许 Visual Studio(或任何编译 XAML 的工具)将 XAML 编译成二进制形式。我想知道性能影响是什么。 令人印象深刻,它对我有用;)。可能是最简单优雅的解决方案! 如果您的 ContextMenu 位于资源块中,则设计器会抛出“XamlParseException: Unresolved reference 'Root'”。运行程序有效。【参考方案2】:

上下文菜单很难绑定。它们存在于您的控件的可视化树之外,因此它们找不到您的元素名称。

尝试将上下文菜单的数据上下文设置为其放置目标。你必须使用RelativeSource。

<ContextMenu 
   DataContext="Binding PlacementTarget, RelativeSource=RelativeSource Self"> ...

【讨论】:

将 DataContext 设置为 PlacementTarget 会影响 ElementName 绑定吗?我认为 DataContext 仅用于未设置 Source、RelativeSource 或 ElementName 属性的绑定。 设置 ElementName 属性只有在布局管理器可以通过向上导航可视化树找到相关元素时才有效。上下文菜单不存在于添加它们的控件的可视化树中。您必须设置上下文菜单的数据上下文,以便布局管理器可以向上导航其放置目标的可视化树以找到关联的元素。 将 DataContext 添加到上述示例并不能解决问题。我在“输出”窗口中仍然出现以下错误:“System.Windows.Data 错误:4:找不到与引用 'ElementName=window' 进行绑定的源。BindingExpression:(no path); DataItem=null; target element is 'MenuItem ' (Name='menuItem'); 目标属性是 'Tag' (type 'Object')" hmmm...回顾我的代码我只是通过直接在绑定中设置相对源来做到这一点,我认为设置 DataContext 会更简单。这对我有用: CommandTarget="Binding PlacementTarget, RelativeSource=RelativeSource FindAncestor, AncestorType=x:Type ContextMenu" 语法显然会有所不同,但也许将绑定的源属性设置为放置目标会起作用吗? 好的,现在说得通了。您正在通过 ContextMenu 将 ElementName 引用更改为 RelativeSource 引用。感谢您的想法。【参考方案3】:

这是另一个仅限 xaml 的解决方法。 (这也假设您想要 DataContext 中的内容,例如,您正在 MVVMing 它)

选项一,ContextMenu的父元素不在DataTemplate中:

Command="Binding PlacementTarget.DataContext.MyCommand, 
         RelativeSource=RelativeSource AncestorType=ContextMenu"

这适用于 OP 的问题。如果您在 DataTemplate 中,这将不起作用。在这些情况下,DataContext 通常是集合中的一个,而您希望绑定到的 ICommand 是同一 ViewModel 中集合的兄弟属性( Window 的 DataContext)。

在这些情况下,您可以利用 Tag 临时保存包含集合和您的 ICommand 的父 DataContext

class ViewModel

    public ObservableCollection<Derp> Derps  get;set;
    public ICommand DeleteDerp get; set;
 

在 xaml 中

<!-- ItemsSource binds to Derps in the DataContext -->
<StackPanel
    Tag="Binding DataContext, ElementName=root">
    <StackPanel.ContextMenu>
        <ContextMenu>
            <MenuItem
                Header="Derp"                       
                Command="Binding PlacementTarget.Tag.DeleteDerp, 
                RelativeSource=RelativeSource 
                                    AncestorType=ContextMenu"
                CommandParameter="Binding PlacementTarget.DataContext, 
                RelativeSource=RelativeSource AncestorType=ContextMenu">
            </MenuItem>

【讨论】:

我认为您在这里提出的相关观点是您可以使用标签和相对源绑定来获取可视树中另一个位置的数据。 这实际上与 MVVM 无关。当我试图在 VM 之外将两个与视图相关的控件绑定在一起时,我只使用 ElementName 绑定。这是将上下文菜单项绑定到 VM 上的命令的一个很好的解决方案。一个不错的选择是使用绑定到 VM 的路由命令。 Josh Smith 的 CommandSink 类就是一个很好的例子。【参考方案4】:

我不确定为什么要使用魔术来避免事件处理程序中的一行代码用于您已经处理的鼠标单击:

    private void MenuItem_Click(object sender, System.Windows.RoutedEventArgs e)
    
        // this would be your tag - whatever control can be put as string intot he tag
        UIElement elm = Window.GetWindow(sender as MenuItem).FindName("whatever control") as UIElement;
    

【讨论】:

这样做不会允许菜单项根据绑定命令自动禁用。因此,虽然它可以执行,但您必须添加更多代码以在加载时相应地禁用/启用菜单项。并不是说这很糟糕,它只是让很多人想起了 WinFoms UI 代码意大利面条的糟糕回忆。【参考方案5】:

我找到了一个更简单的解决方案。

在 UserControl 后面的代码中:

NameScope.SetNameScope(contextMenu, NameScope.GetNameScope(this));

【讨论】:

它也适用于 4.5。惊人的解决方案,效果很好。谢谢。 也适用于 4.5.2。小心...它使用“ElementName”修复绑定,但不使用“RelativeSource FindAncestor”修复绑定。 Source=x:Reference Name=Root 来自 Marc 的答案要好得多顺便说一句,因为它不需要后面的代码并且可以在资源字典中使用【参考方案6】:

经过一番试验,我发现了一种解决方法:

使***Window/UserControl实现INameScope并将NameScopeContextMenu设置为***控件。

public class Window1 : Window, INameScope

    public Window1()
    
        InitializeComponent();
        NameScope.SetNameScope(contextMenu, this);
    

    // Event handlers and etc...

    // Implement INameScope similar to this:
    #region INameScope Members

    Dictionary<string, object> items = new Dictionary<string, object>();

    object INameScope.FindName(string name)
    
        return items[name];
    

    void INameScope.RegisterName(string name, object scopedElement)
    
        items.Add(name, scopedElement);
    

    void INameScope.UnregisterName(string name)
    
        items.Remove(name);
    

    #endregion

这允许上下文菜单在Window 中查找命名项目。还有其他选择吗?

【讨论】:

以上是关于ContextMenu 中 MenuItem 的 ElementName 绑定的主要内容,如果未能解决你的问题,请参考以下文章

如何在 ContextMenu 中使用 MenuItem.setIntent()

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

menuitem和contextmenu crossbrowser兼容性

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

求助高手,WPF ContextMenu MenuItem的icon设置成透明,或者去掉icon

WPF ContextMenu 和 MenuItem 无法在默认样式 WPF 中设置 OverridesDefaultStyle 属性