views:

1069

answers:

3

Interesting problem related to firing commands from context menu items...

I want to fire a command to insert a row in my control, InsertRowCmd. This command needs to know where to insert the row.

I could use Mouse.GetPosition(), but that would get me the position of the mouse currently, which would be over the menu item. I want to get the origin of the context menu instead.

Does any one have any suggestions on how to pass the origin of the context menu as a parameter to the command?

Sample code:

<UserControl x:Name="MyControl">
<!--...-->
        <ContextMenu x:Name="menu">
            <MenuItem Header="Insert Row" Command="{x:Static customCommands:MyCommands.InsertRowCmd}" CommandParameter="?"/>
        </ContextMenu>
</UserControl>

My current ideas are as follows:

-Use click handler instead so that I can find the origin in code. The problem is that I would then have to handle enabling/disabling.

-Handle click event and save the origin of the context menu. Pass this saved information into the command. I have verified that click events fire before the command is executed.

Any ideas?

EDIT:

I'm using Josh Smith's CommandSinkBinding to route the command handling into my ViewModel class. So the code that handles the command execution knows nothing about the view.

+3  A: 

You'll need to use TranslatePoint to translate the top-left (0, 0) of the ContextMenu to a coordinate in the containing grid. You could do so by binding the CommandParameter to the ContextMenu and use a converter:

CommandParameter="{Binding IsOpen, ElementName=_menu, Converter={StaticResource PointConverter}}"

Another approach would be an attached behavior that automatically updates an attached readonly property of type Point whenever the ContextMenu is opened. Usage would look something like this:

<ContextMenu x:Name="_menu" local:TrackBehavior.TrackOpenLocation="True">
    <MenuItem Command="..." CommandParameter="{Binding Path=(local:TrackBehavior.OpenLocation), ElementName=_menu}"/>
</ContextMenu>

So the TrackOpenLocation attached property does the work of attaching to the ContextMenu and updating a second attached property (OpenLocation) whenever the ContextMenu is opened. Then the MenuItem can just bind to OpenLocation to get the location at which the ContextMenu was last opened.

HTH, Kent

Kent Boogaart
I'm thinking you mean "CommandParameter" at the beginning not "ConverterParameter"?
Josh G
Would you care to elaborate on the attached behavior idea?
Josh G
Yes, thanks - fixed and elaborated.
Kent Boogaart
I think I'll try the attached behavior. Thanks for the suggestions.
Josh G
+1  A: 

In addition to Kent's answer, think about a "standard way". F.e. when a ListBox has a ContextMenu, you do not need menu's position, because the selected item is set before the menu popped up. So, if your control would have something that gets "selected" on the right click...

Sergey Aldoukhov
Hmmm... This has some possibility.
Josh G
I ended up using both of your suggestions. There are times when the click is not on a child control, so I need Kent's suggestion on the Point translation with attached behavior. When an item was selected, I used that item instead. Thanks!
Josh G
+1  A: 

Following on from Kent's answer, I used his attached property suggestion and ended up with this (using Josh Smith's example for attached behaviors):

public static class TrackBehavior
{
 public static readonly DependencyProperty TrackOpenLocationProperty = DependencyProperty.RegisterAttached("TrackOpenLocation", typeof(bool), typeof(TrackBehavior), new UIPropertyMetadata(false, OnTrackOpenLocationChanged));

 public static bool GetTrackOpenLocation(ContextMenu item)
 {
  return (bool)item.GetValue(TrackOpenLocationProperty);
 }

 public static void SetTrackOpenLocation(ContextMenu item, bool value)
 {
  item.SetValue(TrackOpenLocationProperty, value);
 }

 public static readonly DependencyProperty OpenLocationProperty = DependencyProperty.RegisterAttached("OpenLocation", typeof(Point), typeof(TrackBehavior), new UIPropertyMetadata(new Point()));

 public static Point GetOpenLocation(ContextMenu item)
 {
  return (Point)item.GetValue(OpenLocationProperty);
 }

 public static void SetOpenLocation(ContextMenu item, Point value)
 {
  item.SetValue(OpenLocationProperty, value);
 }

 static void OnTrackOpenLocationChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
 {
  var menu = dependencyObject as ContextMenu;
  if (menu == null)
  {
   return;
  }

  if (!(e.NewValue is bool))
  {
   return;
  }

  if ((bool)e.NewValue)
  {
   menu.Opened += menu_Opened;

  }
  else
  {
   menu.Opened -= menu_Opened;
  }
 }

 static void menu_Opened(object sender, RoutedEventArgs e)
 {
  if (!ReferenceEquals(sender, e.OriginalSource))
  {
   return;
  }

  var menu = e.OriginalSource as ContextMenu;
  if (menu != null)
  {
   SetOpenLocation(menu, Mouse.GetPosition(menu.PlacementTarget));
  }
 }
}

and then to use in the Xaml, you just need:

<ContextMenu x:Name="menu" Common:TrackBehavior.TrackOpenLocation="True">
 <MenuItem Command="{Binding SomeCommand}" CommandParameter="{Binding Path=(Common:TrackBehavior.OpenLocation), ElementName=menu}" Header="Menu Text"/>
</ContextMenu>

However, I also needed to add:

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

to the constructor of my view, otherwise the binding for the CommandParameter couldn't lookup ElementName=menu.

Wilka
I think I had the same problem with the name scope. Thanks for the post.
Josh G