views:

1614

answers:

5

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?

+2  A: 

menu = sender as MenuItem will be null if the sender is not a MenuItem or a derived class thereof. Subsequently trying to dereference menu will blow up.

It's likely that your sender is a Menu or ContextMenu or a ToolStripMenuItem or some other form of menu item, rather than specifically being a MenuItem object. Use a debugger breakpoint to stop the code here and examine the sender object to see exactly what class it is.

Jason Williams
I used a debugger breakpoint on this line and it says about "sender" Type as follows: "sender {System.Windows.Controls.MenuItem Header:Change status Items.Count:0} object {System.Windows.Controls.MenuItem}"
rem
It is possible that you get that event from several items, some of which are MenuItems (like the one you've caught in the debugger) and some of which are not (like the one causing your crash). If you use if (menu != null) around your processing code, you can stop it trying to process any events from non-MenuItem objects, which may help. Or it is actually crashing on the next line, and it is menu.DataContext that is not a MyCustControl object. Just single step through with the debugger and look at each value till you find out which one is null.
Jason Williams
A: 

Shouldn't you be checking RoutedEventArgs.Source instead of sender?

Vijay Patel
+2  A: 

For RoutedEventArgs

  • RoutedEventArgs.source is the reference to the object that raised the event
  • RoutedEventArgs.originalSource is the reporting source as determined by pure hit testing, before any possible Source adjustment by a parent class.

So .Sender should be the answer. But this depends on how the menuitems are added and bound

See this answer collection and choose the method that will work for you situation!

TFD
Thanks for "answer collection" link. Certianly many ways to skin the cat. You think Microsoft would have made this cleaner by now!
+1  A: 

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

qntmfred
Thanks! The problem is that in the end (I mean your code example) we get "g"-the reference to Grid (where my Context Menu XAML declaration is placed), but I need the reference to clicked object which is inside the Grid (inside the Grid I have hundreds of similar objects, each of them can be right-clicked to get a context menu).
rem
instead of putting the contextmenu on the grid itself, put it on the Grid's children.
qntmfred
Yes, it works. Thank you!
rem
Performance note: Readers should note that the `CommandParameter` binding is not actually necessary here, since `e.Source` can be used instead. The only essential requirement for this to work is that each element have its own `ContextMenu`. Although `CommandParameter` is significantly less efficient than using `e.Source` because of the bindings, you could easily argue that it is more elegant and therefore worth the lower efficiency.
Ray Burns
+1  A: 

You had two different problems. Both problems resulted in the same exception, but were otherwise unrelated:

First problem

In your first approach your code was correct and ran well except for the problem here:

SCurrent.Status = MyCustControl.Status.Sixth;

The name "Status" is used both as a static member and as an instance member. I think you cut-and-pasted the code incorrectly into your question.

It may also be necessary to add the following after MenuItem menu = sender as MenuItem;, depending on your exact situation:

  if(menu==null) return;

Second problem

In your second approach you used "sender" instead of "e.Source". The following code works as desired:

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

Final Note

Note: There is no reason at all to bind CommandParameter for this if you use the commanding approach. It is significantly slower and takes more code. e.Source will always be the source object so there is no need to use CommandParameter, so use that instead.

Ray Burns
Ray, in case of using your last piece of code I get a debugger error: Unable to cast object of type 'System.Windows.Controls.Grid' to type 'MyCustControl'. It seems that e.sourse points not to the clicked object but to the Grid (where my Context Menu XAML declaration is placed).
rem
Interesting. I remember I actually cut-and-pasted your code into a project and tried it. I think I may have pasted the ContextMenu into a template without thinking about it instead of attaching it to the Grid, since it obviously wouldn't be possible for it to work as desired if the ContextMenu were attached to the Grid.
Ray Burns