1

I have a button that I want to display a pop-up menu when the user left-clicks on it. I have implemented the pop-up as the button's ContextMenu. The availability of the menu items in the contextmenu needed to be controlled through bindings to various ViewModel properties. As you know, ContextMenus exist on a separate visual tree than the parent window, so I had to do a bit of research to figure out how to set the DataContext of the ContextMenu to be same as the datacontext of the parent control (i.e. the button). Here is how I did that:

<Button Name="AddRangeBtn" Height="50" Width="50" Margin="5" 
    **Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}">**
    <Button.Content>
        <Image Source="{StaticResource DmxPlusIconImage}" 
               HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
               MaxHeight="50" MaxWidth="50" Margin="-10" />
    </Button.Content>
    <Button.ToolTip>
        <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                         Content="{x:Static Localization:StringResources.AddFrequencyRangeTooltip}"/>
    </Button.ToolTip>
    <Button.ContextMenu>
        **<ContextMenu Name="AddRngContextMenu" 
                     DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">**
            <MenuItem Style="{StaticResource localMenuItem}"
                      IsEnabled="{Binding CanAddLinearRange}">
                <MenuItem.ToolTip>
                    <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                             Content="{x:Static Localization:StringResources.ToolTip_AddLinearFrequencyRange}"/>

The trick to setting the datacontext was binding the datacontext of the contextmenu to the PlacementTarget.Tag in the contextmenu and adding the corresponding Tag property in the buttton. With that in place, I could then directly bind properties in the MenuItems (like the IsEnabled="{Binding CanAddLinearRange}") to ViewModel's dependancy properties. This all worked fine when I access the contextmenu by right-clicking on the button, but I want the menu to display with a normal left-click. So, I added the following EventTrigger to the button:

<Button.Triggers>
    <EventTrigger SourceName="AddRangeBtn" RoutedEvent="Button.Click">
        <BeginStoryboard>
            <Storyboard>
                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="AddRngContextMenu" Storyboard.TargetProperty="(ContextMenu.IsOpen)">
                    <DiscreteObjectKeyFrame KeyTime="0:0:0">
                        <DiscreteObjectKeyFrame.Value>
                            <system:Boolean>True</system:Boolean>
                        </DiscreteObjectKeyFrame.Value>
                    </DiscreteObjectKeyFrame>
                </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </BeginStoryboard>
    </EventTrigger>
</Button.Triggers>

With this, the contextmenu does show up with a left-click on the button, but that instance of the contextmenu doesn't appear to be getting the same datacontext as the instance that is displayed with a right-click!!! In the left-click version all the items in the menu are enabled, so obviously, the contextmenu is not seeing the dependency properties from the viewmodel. I don't know how this is even possible that there appears to be two different versions of the contextmenu, one with the correct datacontext and one with the default behavior.

ContextMenu when launched with right-click:

ContextMenu when launched with right-click

ContextMenu when launched with left-click:

ContextMenu when launched with left-click

I am using XAML to create the left-click behavior and in some of the example I've seen where people have used code-behind to implement the left-click context menu, they have rest the datacontext of the contextmenu to the button.datacontext in the button.click event handler. I am guessing that is what I need to do as well, but I am not sure how to do it in the xaml.

Any ideas would be highly appreciated.

2
  • @vatbub - I liked your edits to the question, but I think you picked the wrong link for the 2nd image and I didn't realize it until I accepted your changes. I wasn't allowed to embed the pictures, so they were added as links, but the 2nd picture should show all the items as enabled. Commented Jan 3, 2020 at 21:58
  • Sorry for the mistake, I fixed it now. Unfortunately, I also had to add some .... at the end of the answers to make StackOverflow accept the edit. You should be able to edit those dots away, though. Sorry again 🙈 Commented Jan 5, 2020 at 7:57

2 Answers 2

1

With all due credit to @KeithStein for setting me on the right path. I added the following Attached Behavior class to the project:

public class OpenContextMenuOnLeftClickBehavior : DependencyObject
{

    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    /// <summary>
    /// The DependencyProperty that backs the IsEnabled property.
    /// </summary>
    public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
        "IsEnabled", typeof(bool), typeof(OpenContextMenuOnLeftClickBehavior), new FrameworkPropertyMetadata(false, IsEnabledProperty_Changed));

    /// <summary>
    /// The property changed callback for the IsEnabled dependency property.
    /// </summary>
    /// <param name="dpobj">
    /// The dependency object whose property changed.
    /// </param>
    /// <param name="args">
    /// The event arguments.
    /// </param>
    private static void IsEnabledProperty_Changed(DependencyObject dpobj, DependencyPropertyChangedEventArgs args)
    {
        FrameworkElement f = dpobj as FrameworkElement; 
        if (f != null)
        {
            bool newValue = (bool)args.NewValue;

            if (newValue)
                f.PreviewMouseLeftButtonUp += Target_PreviewMouseLeftButtonUpEvent;
            else
                f.PreviewMouseLeftButtonUp -= Target_PreviewMouseLeftButtonUpEvent;
        }
    }

    protected static void Target_PreviewMouseLeftButtonUpEvent(object sender, RoutedEventArgs e)
    {
        FrameworkElement f = sender as FrameworkElement;

        if (f?.ContextMenu != null)
        {
            f.ContextMenu.PlacementTarget = f;
            f.ContextMenu.IsOpen = true;
        }

        e.Handled = true;
    }
}

I then referenced the new property in xaml for the Button that has the ContextMenu:

<Button Name="AddRangeBtn" Height="50" Width="50" Margin="5" 
                    Tag="{Binding DataContext, RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}}"
                    controls:OpenContextMenuOnLeftClickBehavior.IsEnabled="True">
                <Button.Content>
                    <Image Source="{StaticResource DmxPlusIconImage}" 
                           HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
                           MaxHeight="50" MaxWidth="50" Margin="-10" />
                </Button.Content>
                <Button.ToolTip>
                    <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                             Content="{x:Static Localization:StringResources.AddFrequencyRangeTooltip}"/>
                </Button.ToolTip>
                <Button.ContextMenu>
                    <ContextMenu Name="AddRngContextMenu" 
                                 DataContext="{Binding Path=PlacementTarget.Tag, RelativeSource={RelativeSource Self}}">
                        <MenuItem Style="{StaticResource localMenuItem}"
                                  IsEnabled="{Binding CanAddLinearRange}">
                            <MenuItem.ToolTip>
                                <ToolTip Style="{DynamicResource DMXToolTipStyle}"
                                     Content="{x:Static Localization:StringResources.ToolTip_AddLinearFrequencyRange}"/>
                            </MenuItem.ToolTip>
                            <MenuItem.Header>
                                <Button>
                                    <Image Source="{StaticResource DMXAddLinFreqRngTypeIconImage}" MinHeight="37" MinWidth="37"/>
                                </Button>
                            </MenuItem.Header>
                        </MenuItem>

This makes the left-click `ContextMenu" behave just like the right-click version.

Sign up to request clarification or add additional context in comments.

1 Comment

That's not bad but actually if you go this way you could also assign the DataContext directly instead of binding it.
0

I haven't found a way to do this using only XAML, but I have used an attached behavior to help keep it clean.

The problem is that, apparently, PlacementTarget doesn't get set until you right-click. I assume the WPF framework sets it as part of the default handling for context menus. But when you set IsOpen manually, PlacementTarget is still null. And, of course, PlacementTarget needs to be set before your binding can work.

The solution I use involves a LeftClickContextMenuBehavior class with an attached dependency property IsEnabled. When IsEnabled is set to true, I add a handler to the target's PreviewMouseLeftButtonUpEvent which looks like this:

    protected static void Target_PreviewMouseLeftButtonUpEvent(object sender, RoutedEventArgs e) {
        FrameworkElement f = sender;

        if (f.ContextMenu != null) {
            f.ContextMenu.PlacementTarget = f;
            f.ContextMenu.IsOpen = true;
        }

        e.Handled = true;
    }

4 Comments

your insight on when PlacementTarget gets set is very useful and it explains the behavior that I am seeing - thank you for that. While I will hold out some hope that someone can propose a xaml solution, I am going to start implementing the "attached behavior" idea. it is a bit more complicated than adding a button.click event handler, but it is far more elegant and reusable and we have other contextmenus in our application where we can use this. I upvoted your answer, but as I am a lowly new user with no reputation to speak of, my vote doesn't show :-(
after implementing the fix that you suggested, the issue with the ContextMenu picking up the DataContext was resolved, but somehow the MenuItems inside the ContextMenu don't respond to left-click anymore. not even if I put a Click event handler in the code behind for each individual MenuItem. However, they respond to a Right-Click. Any idea if this is side effect of the Attached Behavior? Using SnoopWPF, I can see that the menuitems have the correct data context, but I can't figure out why the clicking events have switched!!
I'm not sure what's causing that problem. I just did a test with mine using nested menu items and everything work as expected. There must be something else interfering in your project or something different with your implementation of the behavior (I'd bet on the former).
just to close this out for anyone reading this thread, the issue with the left-click not registering had to do with what I had put into the ContextMenu's MenuItems. Each menu item was a button, but I was associating the command or the event handler with the menu item and not the button inside the menu item. So that button would swallow the click events and make it appear that the menu item wasn't responding the left-click. Moved the command bindings to the buttons inside the menu items and now everything works as expected.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.