19

I have a button with an Image as its content in a toolbar. I would like this button to open a menu beneath it when clicked. How?

<Toolbar>
            <Button>
                <Button.Content>
                    <Image  Source="../Resources/help.png"></Image>
                </Button.Content>
            </Button>
</Toolbar>

Thanks!!

4 Answers4

43

Instead of using a subclassed Button, you can use Attached Properties or a Behavior to implement the drop down button functionality, for a more WPF-like approach and so you don't impact the button style:

using System.Windows.Interactivity;

public class DropDownButtonBehavior : Behavior<Button>
{
    private bool isContextMenuOpen;

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.AddHandler(Button.ClickEvent, new RoutedEventHandler(AssociatedObject_Click), true);
    }

    void AssociatedObject_Click(object sender, System.Windows.RoutedEventArgs e)
    {
        Button source = sender as Button;
        if (source != null && source.ContextMenu != null)
        {
            if (!isContextMenuOpen)
            {
                // Add handler to detect when the ContextMenu closes
                source.ContextMenu.AddHandler(ContextMenu.ClosedEvent, new RoutedEventHandler(ContextMenu_Closed), true);
                // If there is a drop-down assigned to this button, then position and display it 
                source.ContextMenu.PlacementTarget = source;
                source.ContextMenu.Placement = PlacementMode.Bottom;
                source.ContextMenu.IsOpen = true;
                isContextMenuOpen = true;
            }
        }            
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.RemoveHandler(Button.ClickEvent, new RoutedEventHandler(AssociatedObject_Click));
    }

    void ContextMenu_Closed(object sender, RoutedEventArgs e)
    {
        isContextMenuOpen = false;
        var contextMenu = sender as ContextMenu;
        if (contextMenu != null)
        {
            contextMenu.RemoveHandler(ContextMenu.ClosedEvent, new RoutedEventHandler(ContextMenu_Closed));
        }
    }
}

Usage:

<!-- NOTE: xmlns:i="schemas.microsoft.com/expression/2010/interactivity‌​" -->
<Button>
    <i:Interaction.Behaviors>
        <local:DropDownButtonBehavior/>
    </i:Interaction.Behaviors>
    <Button.Content>
        <StackPanel Orientation="Horizontal">
            <Image Source="/DropDownButtonExample;component/Assets/add.png" SnapsToDevicePixels="True" Height="16" Width="16" />
            <TextBlock Text="Add"/>
            <Separator Margin="2,0">
                <Separator.LayoutTransform>
                    <TransformGroup>
                        <TransformGroup.Children>
                            <TransformCollection>
                                <RotateTransform Angle="90"/>
                            </TransformCollection>
                        </TransformGroup.Children>
                    </TransformGroup>
                </Separator.LayoutTransform>
            </Separator>
            <Path Margin="2" VerticalAlignment="Center" Width="6" Fill="#FF527DB5" Stretch="Uniform" HorizontalAlignment="Right" Data="F1 M 301.14,-189.041L 311.57,-189.041L 306.355,-182.942L 301.14,-189.041 Z "/>
        </StackPanel>
    </Button.Content>
    <Button.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Attribute"/>
            <MenuItem Header="Setting"/>
            <Separator/>
            <MenuItem Header="Property"/>
        </ContextMenu>
    </Button.ContextMenu>
</Button>

Current gist source and example here.

Ryan
  • 7,835
  • 2
  • 29
  • 36
  • 2
    This should be marked as answer, nice neat solution! – Herman Cordes Sep 19 '14 at 10:51
  • I'd like it to open on click and close again on click, like the dropdowns on Visual Studio's split buttons. This implementation opens each time you click. I tried setting IsOpen = !IsOpen, and changing when the event triggers (e.g. on PreviewMouseDown) but it seems the context menu is closed already before it reaches a click event. Can you solve this mystery? I'm not even sure it can be done within the behaviour. – Simon F Aug 31 '15 at 23:12
  • That is a good question. Since you say `IsOpen = !IsOpen` is not working, then you could attach to the ContextMenu `Open` and `Closed` events so you could determine if the ContextMenu is actually open (assuming the referenced ContextMenu instance doesn't change). As far as why this happens, I assume once you press the DropDownButton again, technically the ContextMenu loses focus and closes, so the `!IsOpen` attempt fails by the time the behavior code executes. It would be interesting to see how Visual Studio actually does this. It should be much easier than this. – Ryan Sep 03 '15 at 04:11
  • I figured out a solution. I attached to the Closed event to detect if the ContextMenu is still open. If it is open, then I do re-open the ContextMenu. Since the ContextMenu closes when it loses focus, you do not need to do anything when the button is pressed while the ContextMenu is open. – Ryan Sep 03 '15 at 17:05
  • 2
    Behavior is in 'System.Windows.Interactivity' – CRice Aug 15 '16 at 00:57
  • if you don't have blend installed but have a reference to the assembly 'System.Windows.Interactivity' the xaml markup namespace reference can use xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" as seen in https://stackoverflow.com/questions/3059821/the-tag-interaction-behaviors-does-not-exist-in-vs2010-blend-3 – barrypicker Jul 02 '18 at 23:11
  • Just a quick comment. For clarity, you should name your variable `isContextMenuOpen` to something more indicative of what it's actually doing such as `isBehaviorActive` because I (incorrectly) thought I could just use the `contextMenu.isOpen` property and remove the similarly-named variable thinking it was redundant. I now realize `isOpen` will always be true when entering the handler, and your variable is to stop you immediately re-opening it if you've re-clicked the button while it's open, essentially letting you toggle it closed again. Also... (next comment) – Mark A. Donohoe Sep 02 '18 at 19:24
  • (continued...) if you instead change to `PreviewMouseDown`, not only can you use it on any control which takes a context menu (not just button), but you also get the added benefit of it working like actual menus where you can 'click-drag-release' to open and select an item in one motion, as opposed to using `Click` where it takes two clicks: one to open the menu, and a second to select it. Finally, if you simply expose a property of type Placement, then use that in the handler which opens the menu, you can specify that parameter in your consuming XAML making it even more flexible. – Mark A. Donohoe Sep 02 '18 at 19:29
  • 1
    @MarqueIV thanks for the comments. `isContextMenuOpen` was actually closer to `isHandlerAttached` to avoid reattaching the `Click` (or `PreviewMouseDown`) event handler. I did a quick test with `PreviewMouseDown` and it seems to work exactly as you said. I'll update the answer and gist soon. Thanks again. – Ryan Sep 02 '18 at 22:06
  • Not sure I follow your comment about it being closer to the `isHandlerAttached` regarding the `Click` event because you don't use that variable during the attaching or detaching of that event, nor to stop it from being reattached. According to your code, it's only used for opening/closing the context menu (or blocking thereof). You set it to true when the menu opens, then you set it to false when the menu closes. If in your click handler, it's true, you bail out. Maybe call it `openOnNextClick`? – Mark A. Donohoe Sep 02 '18 at 22:29
  • Also, one thing I didn't consider... changing it to any mouse event negates the keyboard being used. So... in my variant, I actually handle *both* the mouse down event, and, if it's a button, also the click event. I then call a common 'OpenMenu' method that has the former event-handler logic. Seems to handle all cases (and since when a user clicks with the mouse, the mouse events happen before the click, that variable we're discussing has the exact same effect... it stops the logic to open the menu. All in all, I really like the flexibility this provides. – Mark A. Donohoe Sep 02 '18 at 22:33
  • 1
    I had to grab `Microsoft.Xaml.Behaviors.Wpf` from nuget and use `xmlns:i="http://schemas.microsoft.com/xaml/behaviors"` instead. (That's from https://stackoverflow.com/a/37906343/438013.) But otherwise this works great! – dbdkmezz Aug 17 '20 at 10:41
10

If you have the luxury of targeting .NET 4 or newer, the new Ribbon library has a RibbonMenuButton that can do this. In 4.5 it is as easy as referencing System.Windows.Controls.Ribbon in your project:

<RibbonMenuButton x:Name="ExampleMenu" SmallImageSource="/Images/Example.png">
    <RibbonMenuItem x:Name="ExampleMenuItem" Header="Save" />
</RibbonMenuButton>
Chris
  • 448
  • 5
  • 9
  • Ribbon Library for WPF (includes a download link for .Net 4.0): https://msdn.microsoft.com/en-us/library/ff799534.aspx – Chris Dec 07 '15 at 19:47
9

i found this two solutions after searching for it:

1) Split Button in WPF

2) DropDownButtons in WPF

the second solution is my favorit (source taken from the website by Andrew Wilkinson)

public class DropDownButton : ToggleButton
{
  // *** Dependency Properties ***

  public static readonly DependencyProperty DropDownProperty =
    DependencyProperty.Register("DropDown",
                                typeof(ContextMenu),
                                typeof(DropDownButton),
                                new UIPropertyMetadata(null));

  // *** Constructors *** 

  public DropDownButton() {
    // Bind the ToogleButton.IsChecked property to the drop-down's IsOpen property 

    Binding binding = new Binding("DropDown.IsOpen");
    binding.Source = this;
    this.SetBinding(IsCheckedProperty, binding);
  }

  // *** Properties *** 

  public ContextMenu DropDown {
    get { return (ContextMenu)this.GetValue(DropDownProperty); }
    set { this.SetValue(DropDownProperty, value); }
  }

  // *** Overridden Methods *** 

  protected override void OnClick() {
    if (this.DropDown != null) {
      // If there is a drop-down assigned to this button, then position and display it 

      this.DropDown.PlacementTarget = this;
      this.DropDown.Placement = PlacementMode.Bottom;

      this.DropDown.IsOpen = true;
    }
  }
}

usage

<ctrl:DropDownButton Content="Drop-Down">
  <ctrl:DropDownButton.DropDown>
    <ContextMenu>
      <MenuItem Header="Item 1" />
      <MenuItem Header="Item 2" />
      <MenuItem Header="Item 3" />
    </ContextMenu>
  </ctrl:DropDownButton.DropDown>
</ctrl:DropDownButton>

hope that helps you...

punker76
  • 14,326
  • 5
  • 58
  • 96
  • 4
    This approach is not WPF-like - attached property should be used, not subclassing. Reasons: 1. styles do not work any more 2. you can derive only from one class but have many different attached properties on the same object – Mikhail Poda Nov 25 '13 at 13:57
  • 1
    As a WPF beginner, these are also incredibly difficult to get working. So much missing info. – Chris Dec 07 '15 at 19:26
  • For using with "Command" set "CommandParameter" to ContextMenu: – Jack Miller Mar 27 '17 at 08:17
2

There are lots of ways to get this done and you might consider this approach...

<ToolBar DockPanel.Dock="Top">
    <MenuItem IsSubmenuOpen="{Binding SomeProperty}">
        <MenuItem.Header>
            <Button Height="28">
                <Button.Content>
                    <Image Source="---your image---"></Image>
                </Button.Content>
            </Button>
        </MenuItem.Header>
        <Menu>
            <MenuItem Header="Do this" />
            <MenuItem Header="Do that"/>
        </Menu>
    </MenuItem>
</ToolBar>

This wraps your button into a MenuItem that has a submenu. As shown here, the MenuItem property called IsSubMenuOpen is bound to a notifying property of type bool in your ViewModel called SomeProperty.

You would have to have your ViewModel toggle this property depending upon what you are actually trying to do. You may want to consider making your button a toggle button so as to facilitate closing the submenu, otherwise you'll have to wire up additional behaviour in your ViewModel.

Vadim Ovchinnikov
  • 13,327
  • 5
  • 62
  • 90
Gayot Fow
  • 8,710
  • 1
  • 35
  • 48