Today I want to talk about a useful control that WPF is missing (and to be honest, WinForms was also missing it): A Drop-Down Button, or (according to Microsoft’s UX Guide) a Menu Button.

Sadly, I didn’t find a satisfying implementation on the internet, and when I say "satisfying" I mean lightweight, with very little code, and most of the behavior in XAML.

I’ve has several attempts to do this, but I think my final attempt was the most satisfying.

In this attempt (the final one) I was using the technique that the ToolBar is using to apply its child controls their special template (you can find it with Reflector under ToolBar.PrepareContainerForItemOverride): The FrameworkElement.SetResourceReference(DependencyProperty, object) method, which searches for the resource by the key in the second argument, and assigns it to the dependency property from the first argument. It’s actually just like writing in XAML: <… DependencyProperty="{DynamicResource key}"/>.

 

My MenuButton derives from ToggleButton for maximum simplicity.

I’ve exposed 3 dependency properties:

  • DropDown (of type ContextMenu): In this property you can place your own context menu that will show the items of the button.
  • IsDropDownOpen (of type Boolean): The property indicates whether the drop-down is open or not. Notice that I’m using the BooleanBoxes technique from my previous post.
  • DropDownPlacement (of type PlacementMode): Lets you decide where to locate the opened drop-down.

I also have an internal attached property (named ParentMenuButton) in order to give the context menu items access to the MenuButton from the style I’m applying them, and a public static property (named MenuDropDownStyleKey) that defines the resource key for the ContextMenu style I’m applying.

 

The basics of the control are very simple: When a context menu is assigned to the DropDown property, I’m applying it a style using the SetResourceReference method. This style is defined in the Generic.xaml file, and its resource key is taken from the static MenuDropDownStyleKey property.

The rest resides in the default style in the XAML.
The default style binds the TobbleButton’s IsChecked property to the IsDropDownOpen property.
Since most of the drop-down buttons will probably have a downwards arrow, I added a default ContentTemplate that adds this arrow.

Finally, I created a style for the context menu that binds its IsOpen property to the IsDropDOwnOpen property, binds the placement, the MinWidth (to make the drop-down’s width at least as the width of the MenuButton’s), the data context and so on.

Also, I’ve created a style for the button inside a ToolBar. Now I could derive from ToolBar and override the PrepareContainerForItemOverride method to apply the style on the ToolBar, but I preferred to create a static class named ToolBarStyleKeys with a static MenuButtonStyleKey property that defines the key for the ToolBar-style and apply the style with this key inside a tool-bar.

 

I hope this post was helpful.

Haim.

 

The code of the MenuButton:

public class MenuButton : ToggleButton
    {
        private static ResourceKey _dropDownStyleKey;

        static MenuButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MenuButton), new FrameworkPropertyMetadata(typeof(MenuButton)));
            _dropDownStyleKey = new ComponentResourceKey(typeof(MenuButton), "MenuButton.ContextMenu style key");
        }

        public static ResourceKey MenuDropDownStyleKey
        {
            get { return _dropDownStyleKey; }
        }

        /// <summary>
        /// Gets or sets he drop-down menu of the button.
        /// </summary>
        public ContextMenu DropDown
        {
            get { return (ContextMenu)GetValue(DropDownProperty); }
            set { SetValue(DropDownProperty, value); }
        }

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


        private static void OnDropDownProeprtyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            MenuButton sender = (MenuButton)d;
            sender.OnDropDownChanged(e.OldValue as ContextMenu, e.NewValue as ContextMenu);
        }

        protected virtual void OnDropDownChanged(ContextMenu oldMenu, ContextMenu newMenu)
        {
            if (oldMenu != null)
            {
                oldMenu.SetResourceReference(ContextMenu.StyleProperty, ContextMenu.DefaultStyleKeyProperty);
                oldMenu.SetValue(FrameworkElement.DefaultStyleKeyProperty, ContextMenu.DefaultStyleKeyProperty.DefaultMetadata.DefaultValue);
                SetParentMenuButton(oldMenu, null);
            }

            if (newMenu != null)
            {
                SetParentMenuButton(newMenu, this);

                if (DependencyPropertyHelper.GetValueSource(newMenu, FrameworkElement.StyleProperty).BaseValueSource <= BaseValueSource.ImplicitStyleReference)
                {
                    newMenu.SetResourceReference(FrameworkElement.StyleProperty, _dropDownStyleKey);
                }

                newMenu.SetValue(FrameworkElement.DefaultStyleKeyProperty, _dropDownStyleKey);
            }
        }

        /// <summary>
        /// Gets or sets the whether the drop-down menu is open or not.
        /// </summary>
        public bool IsDropDownOpen
        {
            get { return (bool)GetValue(IsDropDownOpenProperty); }
            set { SetValue(IsDropDownOpenProperty, BooleanBoxes.Box(value)); }
        }

        public static readonly DependencyProperty IsDropDownOpenProperty =
            DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(MenuButton), new FrameworkPropertyMetadata(BooleanBoxes.FalseBox, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));


        /// <summary>
        /// Gets or sets the placement of the drop-down menu.
        /// </summary>
        public PlacementMode DropDownPlacement
        {
            get { return (PlacementMode)GetValue(DropDownPlacementProperty); }
            set { SetValue(DropDownPlacementProperty, value); }
        }

        public static readonly DependencyProperty DropDownPlacementProperty =
            DependencyProperty.Register("DropDownPlacement", typeof(PlacementMode), typeof(MenuButton), new UIPropertyMetadata(PlacementMode.Bottom));



        internal static MenuButton GetParentMenuButton(ContextMenu obj)
        {
            return (MenuButton)obj.GetValue(ParentMenuButtonProperty);
        }

        internal static void SetParentMenuButton(ContextMenu obj, MenuButton value)
        {
            obj.SetValue(ParentMenuButtonProperty, value);
        }

        // Using a DependencyProperty as the backing store for MenuButtonParent.  This enables animation, styling, binding, etc...
        internal static readonly DependencyProperty ParentMenuButtonProperty =
            DependencyProperty.RegisterAttached("ParentMenuButton", typeof(MenuButton), typeof(MenuButton), new UIPropertyMetadata());


    }

The XAML (in the Generic.XAML):

    <!--MenuButton:-->
    <Style TargetType="{x:Type local:MenuButton}" BasedOn="{StaticResource {x:Type ToggleButton}}">
        <Setter Property="IsChecked" Value="{Binding RelativeSource={RelativeSource Self}, Path=IsDropDownOpen, Mode=TwoWay}"/>
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Grid Margin="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MenuButton}, Path=Padding}">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="15"/>
                        </Grid.ColumnDefinitions>

                        <ContentControl Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MenuButton}, Path=Content}"/>
                        <Path Data="M0,0L3,3 6,0z" Fill="{TemplateBinding Foreground}" Margin="2,0" IsHitTestVisible="False" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Column="1"/>
                    </Grid>
                </DataTemplate>
            </Setter.Value>
        </Setter>

        <Style.Triggers>
            <Trigger Property="DropDown" Value="{x:Null}">
                <Setter Property="IsEnabled" Value="False"/>
            </Trigger>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=DropDown.HasItems}" Value="False">
                <Setter Property="IsEnabled" Value="False"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>

    <!--DropDown for MenuButton:-->
    <Style x:Key="{x:Static local:MenuButton.MenuDropDownStyleKey}" TargetType="ContextMenu" BasedOn="{StaticResource {x:Type ContextMenu}}">
        <Setter Property="IsOpen" Value="{Binding RelativeSource={RelativeSource Self}, Path=(local:MenuButton.ParentMenuButton).IsDropDownOpen}"/>
        <Setter Property="MinWidth" Value="{Binding RelativeSource={RelativeSource Self}, Path=(local:MenuButton.ParentMenuButton).ActualWidth}"/>
        <Setter Property="Placement" Value="{Binding RelativeSource={RelativeSource Self}, Path=(local:MenuButton.ParentMenuButton).DropDownPlacement}"/>
        <Setter Property="PlacementTarget" Value="{Binding RelativeSource={RelativeSource Self}, Path=(local:MenuButton.ParentMenuButton)}"/>
        <Setter Property="DataContext" Value="{Binding RelativeSource={RelativeSource Self}, Path=(local:MenuButton.ParentMenuButton).DataContext}"/>
    </Style>

    <!--MenuButton on ToolBar:-->
    <Style x:Key="{x:Static styles:ToolBarStyleKeys.MenuButtonStyleKey}" TargetType="{x:Type local:MenuButton}" BasedOn="{StaticResource {x:Static ToolBar.ToggleButtonStyleKey}}">
        <Setter Property="IsChecked" Value="{Binding RelativeSource={RelativeSource Self}, Path=IsDropDownOpen, Mode=TwoWay}"/>
        <Setter Property="ContentTemplate">
            <Setter.Value>
                <DataTemplate>
                    <Grid Margin="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MenuButton}, Path=Padding}">
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition/>
                            <ColumnDefinition Width="15"/>
                        </Grid.ColumnDefinitions>

                        <ContentControl Content="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:MenuButton}, Path=Content}"/>
                        <Path Data="M0,0L3,3 6,0z" Fill="{TemplateBinding Foreground}" Margin="2,0" IsHitTestVisible="False" HorizontalAlignment="Right" VerticalAlignment="Center" Grid.Column="1"/>
                    </Grid>
                </DataTemplate>
            </Setter.Value>
        </Setter>

        <Style.Triggers>
            <Trigger Property="DropDown" Value="{x:Null}">
                <Setter Property="IsEnabled" Value="False"/>
            </Trigger>
            <DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=DropDown.HasItems}" Value="False">
                <Setter Property="IsEnabled" Value="False"/>
            </DataTrigger>
        </Style.Triggers>
    </Style>


The ToolBarStyleKeys class:

public static class ToolBarStyleKeys
    {
        private static ResourceKey _menuButtonStyleKey;

        static ToolBarStyleKeys()
        {
            _menuButtonStyleKey = new ComponentResourceKey(typeof(ToolBarStyleKeys), "ToolBarStyleKeys.MenuButton style key");
        }

        public static ResourceKey MenuButtonStyleKey
        {
            get { return _menuButtonStyleKey; }
        }
    }
Advertisements