如何在WPF上下文菜单项点击事件处理程序中引用右键单击的对象?

16

在WPF应用程序中,有一个包含多个对象的Grid(网格),它们都是从自定义控件派生而来。我想使用上下文菜单对它们中的每一个执行一些操作:

   <Grid.ContextMenu>
     <ContextMenu>
       <MenuItem  Name="EditStatusCm" Header="Change status" Click="EditStatusCm_Click"/>
     </ContextMenu>                   
   </Grid.ContextMenu> 

但是在事件处理程序中,我无法知道哪个对象被右键单击了:

    private void EditStatusCm_Click(object sender, RoutedEventArgs e)
    {
        MyCustControl SCurrent = new MyCustControl();
        MenuItem menu = sender as MenuItem;
        SCurrent = menu.DataContext as MyCustControl; // here I get a run-time error
        SCurrent.Status = MyCustControl.Status.Sixth;
    }
在被注释的那一行代码上,调试器显示:对象引用未设置为对象的实例。 请帮忙看看,我的代码哪里出错了? 编辑(添加): 我尝试使用Command方法来实现同样的功能:
我声明了一个名为DataCommands的类,其中包含RoutedUICommand Requery,然后使用了Window.CommandBindings
<Window.CommandBindings>
  <CommandBinding Command="MyNamespace:DataCommands.Requery" Executed="RequeryCommand_Executed"></CommandBinding>
</Window.CommandBindings>

现在 MenuItem 的 XAML 看起来像这样:

<Grid.ContextMenu>
 <ContextMenu>
  <MenuItem  Name="EditStatusCm" Header="Change status"  Command="MyNamespace:DataCommands.Requery"/>
 </ContextMenu>                   
</Grid.ContextMenu>

事件处理程序的样子:

    private void RequeryCommand_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        IInputElement parent = (IInputElement)LogicalTreeHelper.GetParent((DependencyObject)sender);
        MyCustControl SCurrent = new MyCustControl();
        SCurrent = (MuCustControl)parent;
        string str = SCurrent.Name.ToString();// here I get the same error
        MessageBox.Show(str);
    }

但调试器显示了相同的运行时错误:对象引用未设置为对象的实例。

我的两种方法缺少什么?

在WPF上下文菜单项点击事件处理程序中,我应该如何引用右键单击的对象?


我尝试使用命令方式,因为它更符合WPF的风格,但是出现了相同的错误。我编辑了我的问题,并添加了我尝试使用命令方式的步骤。在这两种情况下,我都没有完全理解如何获取所点击对象的引用。 - rem
8个回答

33
请注意CommandParameter。
<Grid Background="Red" Height="100" Width="100">
    <Grid.ContextMenu>
        <ContextMenu>
            <MenuItem 
                Header="Change status" 
                Click="EditStatusCm_Click"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent}" />
        </ContextMenu>
    </Grid.ContextMenu>
</Grid>

并在处理程序中使用它来确定它是哪个网格

    private void EditStatusCm_Click(object sender, RoutedEventArgs e)
    {
        MenuItem mi = sender as MenuItem;
        if (mi != null)
        {
            ContextMenu cm = mi.CommandParameter as ContextMenu;
            if (cm != null)
            {
                Grid g = cm.PlacementTarget as Grid;
                if (g != null)
                {
                    Console.WriteLine(g.Background); // Will print red
                }
            }
        }
    }
更新:
如果您希望菜单项处理程序访问Grid的子元素而不是Grid本身,请使用以下方法。
<Grid Background="Red" Height="100" Width="100">
    <Grid.Resources>
        <ContextMenu x:Key="TextBlockContextMenu">
            <MenuItem 
                Header="Change status" 
                Click="EditStatusCm_Click"
                CommandParameter="{Binding RelativeSource={RelativeSource Self}, Path=Parent}" />
        </ContextMenu>

        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="ContextMenu" Value="{StaticResource TextBlockContextMenu}" />
        </Style>
    </Grid.Resources>

    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>

    <TextBlock Text="Row0" Grid.Row="0" />
    <TextBlock Text="Row1" Grid.Row="1" />
</Grid>

只需用你自定义的对象类型替换TextBlocks即可。然后在事件处理程序中,将Grid g = cm.PlacementTarget as Grid替换为TextBlock t = cm.PlacementTarget as TextBlock(或替换为你自定义的对象类型)。


谢谢!问题是最后(我的意思是你的代码示例)我们得到了“g”-对Grid的引用(其中放置了我的上下文菜单XAML声明),但我需要对点击的对象的引用,该对象位于Grid内部(在Grid内部,我有数百个类似的对象,每个对象都可以右键单击以获取上下文菜单)。 - rem
将上下文菜单放在Grid的子元素上,而不是放在Grid本身上。 - kenwarner
3
性能注意:读者应该注意到这里实际上并不需要使用CommandParameter绑定,因为可以使用e.Source代替。这项工作的唯一必要条件是每个元素都有自己的上下文菜单。尽管由于绑定,CommandParameter显著不如使用e.Source高效,但你可以轻松地认为它更加优雅,因此值得较低的效率。 - Ray Burns

6
通过在XAML中绑定数据上下文,如下所示:
ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource=    {RelativeSource Self}}">

你可以这样做:

private void Context_MenuClick(object sender, RoutedEventArgs e)
{
   var menuItem = e.Source as MenuItem;

   MyDoStuffFunction(menuItem.DataContext);
}

数据上下文将绑定到打开ContextMenu的对象。我从这个链接的codeproject文章中得到了它:

http://www.codeproject.com/Articles/162784/WPF-ContextMenu-Strikes-Again-DataContext-Not-Upda


2
如果发送者不是 MenuItem 或其派生类,则 menu = sender as MenuItem 将为 null。随后尝试取消引用 menu 将导致错误。
很可能您的发送者是菜单(Menu)、上下文菜单(ContextMenu)、工具栏菜单项(ToolStripMenuItem)或其他形式的菜单项,而不是特定的 MenuItem 对象。使用调试器断点在此处停止代码并检查发送者对象,以确定它属于哪个类。

我在这一行上使用了调试器断点,并且它关于“sender”类型的信息如下:“sender {System.Windows.Controls.MenuItem Header:Change status Items.Count:0} object {System.Windows.Controls.MenuItem}”。 - rem
有可能你从多个项中获取该事件,其中一些是菜单项(例如在调试器中捕获的那个),而另一些则不是(例如导致崩溃的那个)。如果在处理代码周围使用 if (menu != null),您可以阻止它尝试处理来自非菜单项对象的任何事件,这可能会有所帮助。或者它实际上是在下一行崩溃,而 menu.DataContext 不是 MyCustControl 对象。只需使用调试器逐步执行并查看每个值,直到找出哪个值为 null 即可。 - Jason Williams
menu = sender as MenuItem 不起作用,因为默认情况下 MenuItem 是 System.Windows.Forms 中的一个类, 但它可以像这样工作 sender as System.Windows.Controls.MenuItem; - horiatu

2

对于 RoutedEventArgs

  • RoutedEventArgs.source 是引发事件的对象的引用
  • RoutedEventArgs.originalSource 是通过纯命中测试确定的报告源,在任何可能的父类源调整之前。

所以 .Sender 应该是答案。但这取决于菜单项是如何添加和绑定的。

请参见此答案集合,选择适合您情况的方法!


感谢提供“答案收集”链接。肯定有很多方法可以解决这个问题。你认为微软现在应该把这个做得更简洁! - user73993

1

你遇到了两个不同的问题。这两个问题导致了相同的异常,但是它们本质上是无关的:

第一个问题

在你的第一种方法中,你的代码是正确的并且运行良好,除了这里的问题:

SCurrent.Status = MyCustControl.Status.Sixth;

名称 "Status" 既用作静态成员,也用作实例成员。我认为您在将代码复制到问题中时出错了。
根据您的具体情况,可能还需要在 MenuItem menu = sender as MenuItem;之后添加以下内容:
  if(menu==null) return;

第二个问题

在您的第二种方法中,您使用了“sender”而不是“e.Source”。以下代码按预期工作:

private void RequeryCommand_Executed(object sender, ExecutedRoutedEventArgs e)    
{    
    IInputElement parent = (IInputElement)LogicalTreeHelper.GetParent((DependencyObject)e.Source);
      // Changed "sender" to "e.Source" in the line above
    MyCustControl SCurrent = new MyCustControl();    
    SCurrent = (MuCustControl)parent;    
    string str = SCurrent.Name.ToString();// Error gone
    MessageBox.Show(str);    
}

最后说明

注意:如果您使用命令方式,绑定CommandParameter毫无意义。它的速度明显较慢,代码也更多。e.Source始终是源对象,因此无需使用CommandParameter,请改用它。


Ray,如果使用你的最后一段代码,我会得到一个调试器错误:无法将类型为“System.Windows.Controls.Grid”的对象强制转换为类型“MyCustControl”。看起来e.source指向的不是被点击的对象,而是Grid(我的上下文菜单XAML声明所在的位置)。 - rem
有趣。我记得我曾经将你的代码剪切并粘贴到一个项目中进行尝试。我想我可能会在没有考虑的情况下将ContextMenu粘贴到模板中,而不是将其附加到Grid上,因为如果ContextMenu附加到Grid上,它显然无法按预期工作。 - Ray Burns

1

你应该检查 RoutedEventArgs.Source 而不是 sender,不是吗?


1
这对我有效:
XAML:
<DataGrid.ContextMenu>
<ContextMenu x:Name="AddColumnsContextMenu" MenuItem.Click="AddColumnsContextMenu_Click">
</ContextMenu>

添加菜单项的方法如下:-
foreach (String s in columnNames)
{
var item = new MenuItem { IsCheckable = true, IsChecked = true ,Header=s};
AddColumnsContextMenu.Items.Add(item);
}

这里是监听器:-
private void AddColumnsContextMenu_Click(object sender, RoutedEventArgs e)
{
    MenuItem mi = e.Source as MenuItem;
    string title = mi.Header.ToString();
    MessageBox.Show("Selected"+title);
}

感谢...


0
在我的情况下,我能够使用:
private void MenuItem_Click(object sender, RoutedEventArgs e)
{    
    MenuItem menuItem        = e.Source as MenuItem;
    ContextMenu parent       = menuItem.Parent as ContextMenu;
    ListBoxItem selectedItem = parent.PlacementTarget as ListBoxItem;
}

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