如何使用父按钮中的 属性 让 CommandParameter 为 ContextMenu 中的 MenuItem 工作?

How do you get CommandParameter working for a MenuItem in a ContextMenu using a property from the parent Button?

结果 我弄清楚自己发生了什么事。我有一种奇怪的组合,所以这对其他人可能有用也可能没用。

我将继续在答案条目中记录解决方案。我的解决方案涉及少量代码隐藏。我一般不喜欢代码隐藏。它仅与视图相关,因此不会破坏 MVVM。如果有人给我一个仅 XAML 的解决方案,而且还不算太远,我会很乐意将其用作可接受的答案。

问题 我有一个 WPF 应用程序 (.NET6),它有一个代表谨慎对象的图像网格。我希望每个人都有一个相同的上下文菜单。每个都需要发送一个唯一的 CommandParameter 以便视图模型可以识别要操作的对象。

为了给自己添加标识符的方法,我将 Button 子类化如下:

public class PartButton : Button
{
    public int PartPosition { get; set; }
}

我知道有关上下文菜单不在可视化树中的问题,因此不会从顶级控件(在本例中为 UserControl)继承 DataContext。诀窍通常只是使用 PlacementTarget 获取父级,并使用其 DataContext.

让左键单击和右键单击一样有效也是一个目标。为此,我对 Click 使用 RoutedEvent,并使用 Storyboard 将按钮上的 ContextMenu.IsOpen 设置为 true。如果您对 XAML 更有经验,您可能已经发现了问题。

因为有 30 个按钮,最终目标是通过一种样式添加菜单,以便可以使用尽可能少的代码声明按钮。为了让一个工作正常,我试了一下:

<jb:PartButton
    Grid.Row="2" Grid.Column="2"
    IsEnabled="{ Binding Path=PartStatuses[1] }"
    PartPosition="1">
    <Image Source="../Resources/Part.png" />
    <jb:PartButton.ContextMenu>
        <ContextMenu DataContext="{Binding Path=PlacementTarget, RelativeSource={RelativeSource Self}}">
            <MenuItem Header="Test Part"
                CommandParameter="{Binding Path=PartPosition}"
                Command="{Binding Source={StaticResource Proxy}, Path=Data.TestPartCmd}"
                />
        </ContextMenu>
    </jb:PartButton.ContextMenu>
</jb:PartButton>

右键单击效果很好。但是,当使用上面的 RoutedEvent 技巧时,左键单击会导致 PlacementTarget 始终为 null.

            <ContextMenu>
                <MenuItem Header="Test Part" CommandParameter="{Binding RelativeSource={RelativeSource Mode=Self}}" Command="{Binding TestPartCmd}"/>
            </ContextMenu>

和像这样的 ViewModel

    public RelayCommand<System.Windows.Controls.MenuItem> TestPartCmd
    {
        get
        {
            return new RelayCommand<System.Windows.Controls.MenuItem>((menuItem) =>
            {
                int partPosition = ((PartButton)((System.Windows.Controls.Primitives.Popup)((System.Windows.FrameworkElement)menuItem.Parent).Parent).PlacementTarget).PartPosition;
            });
        }
    }

可能有更好的方法,但我还没想到。

按照承诺,这是我目前的解决方案:

<ContextMenu>
    <MenuItem Header="Bootloader Test"
        CommandParameter="{Binding Path=PlacementTarget.PartPosition, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
        Command="{Binding Path=PlacementTarget.DataContext.BootloaderTestCmd, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
        />

要工作,它需要 code-behind:

public void OnPartButtonPressed(object sender, RoutedEventArgs e)
{
    if (sender is Button btn)
    {
        btn.ContextMenu.PlacementTarget = btn;
        btn.ContextMenu.IsOpen = true;
    }
}

现在,这已完全实现为一种样式,因此每个按钮真正需要的是这样的:

<jb:PartButton
    PartPosition="1"
    Grid.Row="2" Grid.Column="2" />

在实际项目中,我还在对象处声明了按钮的图像,因为我一直在努力通过样式来完成它。该图像取决于必须在 view-model 中解析的索引 (PartPosition)。很难使用样式从 view-model 使用必须从对象查询的值来获取 属性。不过,这是不同的问题,我以后可能会担心也可能不会担心。