10

The default behavior of a WPF ContextMenu is to display it when the user right-clicks. I want the ContextMenu to show when the user left-clicks. It seems like this should be a simple property on ContextMenu, but it is not.

I rigged it, so that I handle the LeftMouseButtonDown event in the code-behind and then display the context menu.

I'm using MVVM in my project which means I'm using DataTemplates for the items that have the context menus. It would be much more elegant to get rid of the code-behind and find a way to display the context menu using triggers or properties in the XAML.

Any ideas or solutions to this issue?

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
timothymcgrath
  • 1,318
  • 1
  • 9
  • 19
  • It's a departure from the standard in Windows, do you have good justification for doing this? – Adam Ralph Feb 17 '09 at 08:43
  • That is a good point, maybe I should be using something other than the ContextMenu to get this done. It is basically a drop-down menu that appears when you click on the item, not a button, but kind of buttony. ContextMenu seemed like an obvious choice, but maybe that is wrong. – timothymcgrath Feb 20 '09 at 03:39
  • See my answer which uses Expression Blend Triggers here: http://stackoverflow.com/a/4917707/87912 – Flatliner DOA Aug 21 '12 at 04:57
  • Unfortunately I don't think there is a XAML only solution to this one. The context-menu behavior is baked in to the FrameworkElement. – Micah Feb 17 '09 at 01:55

5 Answers5

16

I've just written and tested this based on HK1's answer (you can also read about attached properties in Attached Properties Overview) :

public static class ContextMenuLeftClickBehavior
{
    public static bool GetIsLeftClickEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsLeftClickEnabledProperty);
    }

    public static void SetIsLeftClickEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsLeftClickEnabledProperty, value);
    }

    public static readonly DependencyProperty IsLeftClickEnabledProperty = DependencyProperty.RegisterAttached(
        "IsLeftClickEnabled", 
        typeof(bool), 
        typeof(ContextMenuLeftClickBehavior), 
        new UIPropertyMetadata(false, OnIsLeftClickEnabledChanged));

    private static void OnIsLeftClickEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        var uiElement = sender as UIElement;

        if(uiElement != null) 
        {
            bool IsEnabled = e.NewValue is bool && (bool) e.NewValue;

            if(IsEnabled)
            {
                if(uiElement is ButtonBase)
                    ((ButtonBase)uiElement).Click += OnMouseLeftButtonUp;
                else
                    uiElement.MouseLeftButtonUp += OnMouseLeftButtonUp;
            }
            else
            {
                if(uiElement is ButtonBase)
                    ((ButtonBase)uiElement).Click -= OnMouseLeftButtonUp;
                else
                    uiElement.MouseLeftButtonUp -= OnMouseLeftButtonUp;
            }
        }
    }

    private static void OnMouseLeftButtonUp(object sender, RoutedEventArgs e)
    {
        Debug.Print("OnMouseLeftButtonUp");
        var fe = sender as FrameworkElement;
        if(fe != null)
        {
            // if we use binding in our context menu, then it's DataContext won't be set when we show the menu on left click
            // (it seems setting DataContext for ContextMenu is hardcoded in WPF when user right clicks on a control, although I'm not sure)
            // so we have to set up ContextMenu.DataContext manually here
            if (fe.ContextMenu.DataContext == null)
            {
                fe.ContextMenu.SetBinding(FrameworkElement.DataContextProperty, new Binding { Source = fe.DataContext });
            }

            fe.ContextMenu.IsOpen = true;
        }
    }

}

...

<Button Content="Do All" local:ContextMenuLeftClickBehavior.IsLeftClickEnabled="True" >
    <Button.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Make everything awesome" />
            <MenuItem Header="Control the World" />
        </ContextMenu>
    </Button.ContextMenu>
</Button>

(note the comment inside the OnMouseLeftButtonUp() method)

nightcoder
  • 13,149
  • 16
  • 64
  • 72
  • 3
    Excellent solution, to solve the binding issue referenced in your comments, you can set the placement target: `fe.ContextMenu.PlacementTarget = fe` then `DataContext="{Binding Path=PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">` You can then use the ContextMenuService properties like Placement and Horizontal/VerticalOffset to position it. – DoubleJ May 09 '16 at 16:36
  • 2
    I did not want it to open again when clicking the button while the contextmenu was already shown. So below IsOpen=true I added these 2 lines: `fe.ContextMenu.Closed += ((s, a) => { fe.IsEnabled = true; }); fe.IsEnabled = false;` –  Aug 16 '19 at 16:52
  • @user1552075 It is recommended to add `ContextMenuService.IsEnabled="false"` to xaml to avoid the need for an extra click to switch operation when `IsOpen=True`. – CodingNinja Oct 06 '21 at 01:53
9

What I would suggest doing is making a new static class with attached DependencyProperty. Call the class LeftClickContextMenu and the property Enabled (just ideas). When your registering the DependencyProperty add an on changed callback. Then in the property changed callback if Enabled is set to true then add a handler to the LeftMouseButtonDown event and do your stuff there. If Enabled is set to false remove the handler. This sould allow you to set it like a property on anything by simply using the following in your xaml.

<Border namespace:LeftClickContextMenu.Enabled="True" />

This technique is called an attached behavior and you can read more about it in this code project article: http://www.codeproject.com/KB/WPF/AttachedBehaviors.aspx

Caleb Vear
  • 2,637
  • 2
  • 21
  • 20
  • I think this solution is easier: http://uxpassion.com/blog/old-blog/how-to-enable-and-show-context-menu-on-left-click-in-wpf – Sonhja Feb 25 '13 at 10:02
  • It is definitely an easier approach and if you only need it one spot I would recommend just putting the code in the code behind. However if you need this behaviour in a few places the attached behavior method I have suggested is nicer IMO. – Caleb Vear Mar 08 '13 at 04:33
  • Oh and if you wanted an even easier solution with a button you can just use a ToggleButton and bind it's IsChecked property to the context menu's IsOpen property. – Caleb Vear Mar 08 '13 at 04:35
4

While Caleb's answer is correct, it doesn't include working code. I setup an example using VB.NET (sorry) so I'm posting it here.

<Window x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:AttachedBehaviorTest.AttachedBehaviorTest"
    Title="MainWindow" Height="350" Width="525">
    <Grid>
        <StackPanel>
            <TextBlock local:ContextMenuLeftClickBehavior.IsLeftClickEnabled="True">Some Text Goes Here
                <TextBlock.ContextMenu>
                    <ContextMenu>
                        <MenuItem Header="Test1" />
                    </ContextMenu>
                </TextBlock.ContextMenu>            
            </TextBlock>

        </StackPanel>
    </Grid>
</Window>
Namespace AttachedBehaviorTest

    Public NotInheritable Class ContextMenuLeftClickBehavior

        Private Sub New()
        End Sub

        Public Shared Function GetIsLeftClickEnabled(obj As DependencyObject) As Boolean
            Return CBool(obj.GetValue(IsLeftClickEnabled))
        End Function

        Public Shared Sub SetIsLeftClickEnabled(obj As DependencyObject, value As Boolean)
            obj.SetValue(IsLeftClickEnabled, value)
        End Sub

        Public Shared ReadOnly IsLeftClickEnabled As DependencyProperty = _
            DependencyProperty.RegisterAttached("IsLeftClickEnabled", GetType(Boolean), GetType(ContextMenuLeftClickBehavior), New UIPropertyMetadata(False, AddressOf OnIsLeftClickEnabled))

        Private Shared Sub OnIsLeftClickEnabled(sender As Object, e As DependencyPropertyChangedEventArgs)
            Dim fe As FrameworkElement = TryCast(sender, FrameworkElement)
            If fe IsNot Nothing Then
                Dim IsEnabled As Boolean = CBool(e.NewValue)
                If IsEnabled = True Then
                    AddHandler fe.MouseLeftButtonUp, AddressOf OnMouseLeftButtonUp
                    Debug.Print("Added Handlers")
                Else
                    RemoveHandler fe.MouseLeftButtonUp, AddressOf OnMouseLeftButtonUp
                    Debug.Print("RemovedHandlers")
                End If 
            End If
        End Sub

        Private Shared Sub OnMouseLeftButtonUp(sender As Object, e As RoutedEventArgs)
            Debug.Print("OnMouseLeftButtonUp")
            Dim fe As FrameworkElement = TryCast(sender, FrameworkElement)
            If fe IsNot Nothing Then
                'Next Line is Needed if Context Menu items are Data Bound
                'fe.ContextMenu.DataContext = fe.DataContext
                fe.ContextMenu.IsOpen = True
            End If
        End Sub

    End Class

End Namespace
HK1
  • 11,941
  • 14
  • 64
  • 99
1

This answer does exactly the same job as the answer from @nightcoder (thanks for the inspiration!). It uses a Blend-style behavior which is a more modern approach compared to an attached property.

using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Interactivity;

/// <summary>
/// Add this to any button menu allow a left click to open the context menu as well as the right.
/// </summary>
public class ContextMenuLeftClickBehavior : Behavior<ButtonBase>
{
    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.Loaded += this.OnWindowLoaded;
        this.AssociatedObject.Unloaded += this.OnWindowUnloaded;
    }

    private void OnWindowLoaded(object sender, RoutedEventArgs e)
    {
        this.AssociatedObject.Click += OnMouseLeftButtonUp;
    }

    private void OnWindowUnloaded(object sender, RoutedEventArgs e)
    {
        this.AssociatedObject.Click -= OnMouseLeftButtonUp; // Cannot override OnDetached(), as this is not called on Dispose. Known issue in WPF.
    }

    private static void OnMouseLeftButtonUp(object sender, RoutedEventArgs e)
    {
        if (sender is ButtonBase fe && fe.ContextMenu != null)
        {
            if (fe.ContextMenu != null)
            {
                // If we use binding in our context menu, then it's DataContext won't be set when we show the menu on left click. It
                // seems setting DataContext for ContextMenu is hardcoded in WPF when user right clicks on a control? So we have to set
                // up ContextMenu.DataContext manually here.
                if (fe.ContextMenu?.DataContext == null)
                {
                    fe.ContextMenu?.SetBinding(FrameworkElement.DataContextProperty, new Binding { Source = fe.DataContext });
                }

                fe.ContextMenu.IsOpen = true;
            }
        }
    }
}

Then add the behavior to the button:

<Button>
    <i:Interaction.Behaviors>
        <attachedProperties:ContextMenuLeftClickBehavior/>
    </i:Interaction.Behaviors>                                                
<Button>

Elements such as an Ellipse or a Rectangle do not have an OnClick event, which means nothing really works very well for anything interactive. So wrap everything in a button to get that OnClick event. Might as well hint that the area is clickable by changing the mouse cursor to a hand on mouseover.

<Button.Style>
    <Style TargetType="{x:Type Button}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Ellipse Fill="{TemplateBinding Background}" Width="16" Height="16"/>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <!--  Bind to custom color in ViewModel -->
                <Setter Property="Background" Value="{Binding CustomBrush}"/>
                <Setter Property="Cursor" Value="Hand"/>
            </Trigger>
        </Style.Triggers>
    </Style>
</Button.Style>
Contango
  • 76,540
  • 58
  • 260
  • 305
  • This works for me, but some things have changed since this answer was posted. `System.Windows.Interactivity` no longer seems to exist and `Behavior` is now part of the `Microsoft.Xaml.Behaviors` namespace in the NuGet package `Microsoft.Xaml.Behaviors.WPF` (there are other packages for other frameworks). Also worth noting is that the `attachedProperties` namespace is a `clr-namespace` pointing to the namespace that contains the behavior subclass. In my case I already had a local namespace in my XAML for this, so it was just ``. – Charles A. Oct 01 '21 at 00:27
-1

Forget the "only xaml" thing. This can be solved nicely when you wrap it into attached behavior.

Here is a way to show context menu on left-click:

Create a new left button handler on the Border element:

<Border x:Name="Win"
        Width="40"
        Height="40"
        Background="Purple"
        MouseLeftButtonUp="UIElement_OnMouseLeftButtonUp">

and then add this:

private void UIElement_OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
    e.Handled = true;

    var mouseDownEvent =
        new MouseButtonEventArgs(Mouse.PrimaryDevice,
            Environment.TickCount,
            MouseButton.Right)
        {
            RoutedEvent = Mouse.MouseUpEvent,
            Source = Win,
        };


    InputManager.Current.ProcessInput(mouseDownEvent);
}

What it does, it basically maps the left-click into right-click. For reusability, you can wrap this into an attached behavior.

Erti-Chris Eelmaa
  • 25,338
  • 6
  • 61
  • 78