Search code examples
c#wpfevent-handlingcontextmenu

How to reference right-clicked object in WPF Context Menu item click event handler?


In WPF application there is a Grid with a number of objects (they are derived from a custom control). I want to perform some actions on each of them using context menu:

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

But in the event handler I cannot get know which of the objects was right-clicked:

    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;
    }

On that commented line Debugger says: Object reference not set to an instance of an object.

Please help, what is wrong in my code?

Edited (added):

I tried to do the same, using Command approach:

I declared a DataCommands Class with RoutedUICommand Requery and then used Window.CommandBindings

<Window.CommandBindings>
  <CommandBinding Command="MyNamespace:DataCommands.Requery" Executed="RequeryCommand_Executed"></CommandBinding>
</Window.CommandBindings>

XAML of MenuItem now looks like:

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

And event handler looks like:

    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);
    }

But debugger shows the same run-time error: Object reference not set to an instance of an object.

What is missing in my both approaches?

How I should reference right-clicked object in WPF Context Menu item click event handler?


Solution

  • note the 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>
    

    and use it in the handler to figure out which Grid it is

        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
                    }
                }
            }
        }
    

    Update:
    If you want the menuitem handler to get to the Grid's children instead of the Grid itself, use this approach

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

    Just replace the TextBlocks with whatever your custom object type is. Then in the event handler, replace Grid g = cm.PlacementTarget as Grid with TextBlock t = cm.PlacementTarget as TextBlock (or whatever your custom object type is).