Sunday, October 29, 2006

DropDownButtons in WPF

Whilst Windows Presentation Foundation (WPF) provides good support for menus, one omission is a drop-down button. By this I mean a button, that when clicked will result in a menu dropping down from the control.

So I started to write my own. Easy I thought, just style a MenuItem to look like a button. Well, this doesn't work as the MenuItem control will only work if it is a child of a Menu control. I could just wrap all my DropDownButtons in Menus, but that becomes a little messy wrapping everything in extra menus. I tried a number of other approaches, none of which worked as I would like.


This was several months ago. Recently however I was informed of a couple of related blog articles at Sheva's Techspace and Lester's piece on WPF. In particular I liked Lester's approach. This is to use the Button's ContextMenu to apply add a context menu, and then to use code to open this on a left mouse click. A simple solution that works well. But what if I wanted to have a real context-menu too?


Solution: Well, in fact ContextMenu doesn't need to be attached to a control to work. All you need to do is create a new ContextMenu object, add your MenuItems, and set IsOpen to true. So I put together the control as below.



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)GetValue(DropDownProperty);
}
set
{
SetValue(DropDownProperty, value);
}
}

// *** Overridden Methods ***

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

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

DropDown.IsOpen = true;
}
}
}


This control derives directly from ToggleButton so we inherit the full behavior and styling of a button. We then provide a DropDown dependency property that takes a ContextMenu to show for the drop-down. In the OnClick event we simply position the menu under the control, and open it by setting IsOpen to true. Finally to tidy up, we bind the ToggleButton's IsChecked property to the drop-down menu's IsOpen property to ensure that these are synchronized.


To use this in your own code you simply use,



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



Technorati tags: , ,

13 comments:

Joe said...

For those who don't know where to find ToggleButton, you will need to add a using statement like the one below:

using System.Windows.Controls.Primitives;

Andrew Wilkinson said...

Thanks for that Joe - I'd forgotten to mention the namespaces.

All together you need,

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

Don't think I've missed any there.

PBJJ said...

I'm doing this in one of the MenuItem's PreviewMouseLeftButtonUp event:

Window w = new Window();
w.Show();

The new window appears, then goes behind its creator. I think it's related to DropDownButton collapsing and forcing the active window to change. Any ideas on how to get around this?

Andrew Wilkinson said...

pbjj,

The problem you are having is that the MenuItem is handling the MouseLeftButtonUp event itself. In its handler it then grabs back the focus. There are three general methods that I can think of to solve this.

(1) The first thought I have is why are you handling PreviewMouseButtonUp and not the Click event? If you handled the Click event instead then you avoid problems with focus, and also enable other goodness such as the ability to select the menu item with the keyboard.

This is probably the easiest and most reliable solution, however I will assume that you are handling the PreviewMouseButtonUp event for some special reason. Therefore my other suggestions are,

(2) Make the MenuItem non-focusable. Then it cannot grab back the focus,

MenuItem Focusable="False"

(3) Mark the MouseButtonUp event as handled,

private void MenuItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Window w = new Window();
w.Show();
e.handled = true;
}

Let me know if any of these options solves your problem, or if not do you have any more details on why you are using PreviewMouseLeftButtonUp rather than Click?

Andy

Huy Pham said...

This is my version of this control (base on your toggle button idea).
Thank you, excite idea Andy.

http://huydinhpham.blogspot.com/2008/09/wpf-drop-down-and-split-button.html

David said...

thanks for your post
Is there any way in which I can alter the behaviour of the context menu that it opens on the right side of the button instead of the bottom?

Fatema said...

thanks for the post. I have tried oder .But this one i like most. simply done :))

fatema :)

Andrew Wilkinson said...

David,

If you wish to alter the position for the popup then then you need to look at the PlacementMode enumeration. To achieve what you are looking for it should be as simple as changing...

DropDown.Placement = PlacementMode.Bottom;

... to ...

DropDown.Placement = PlacementMode.Right;

Andy

Andrew Wilkinson said...

Fatema,

Pleased you liked the post and found it useful.

Andy

dghnfgj said...
This post has been removed by a blog administrator.
Kevin Berridge said...

Very helpful thanks!

I just wanted to point out that because you setup the Binding on IsChecked, you don't need to override the OnClick. The Binding takes care of opening the ContextMenu for you.

To setup the placement info, you can just add a PropertyChnagedCallback to your DropDownProperty and set the PlacementTarget there. Like this:

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

static void DropDownPropertyChangedCallback( DependencyObject d, DependencyPropertyChangedEventArgs e )
{
( (DropDownButton)d ).DropDownPropertyChangedCallback( e );
}

void DropDownPropertyChangedCallback( DependencyPropertyChangedEventArgs e )
{
if ( DropDown != null )
{
DropDown.PlacementTarget = this;
DropDown.Placement = PlacementMode.Bottom;
}
}

Thanks again!

Andrew Wilkinson said...

Kevin,

Thanks for the comment - sounds good to me.

Andy

rutt said...

I'm pretty sure you can accomplish the same thing just using the Button's content property.
It looks like I have to leave the tags off, but here's the tagless XAML:

Button
Menu
MenuItem Header="MyButton"
MenuItem Header="Choice1" /
MenuItem Header="Choice2" /
MenuItem Header="Choice3" /
/MenuItem
/Menu
/Button