Search code examples
c#.netwpfxamlcontrols

WPF Dropdown Menu Button


I've created my own control MenuButton which looks like on the screenshot below:

enter image description here

So, the menu items appears when I'm clicking on the button and hides when the focus is lost. Handling clicks on menu items also works fine. BUT! Only from the second time! The first click is ignoring. I crash my brains with trying to fix this weird issue. Can someone take a look at my code and tell me where I was wrong?

public partial class MenuButton : Button, INotifyPropertyChanged
{
    private readonly ContextMenu Menu;
    private SolidColorBrush arrowColor = Brushes.Transparent;

    public static readonly DependencyProperty TextProperty =
        DependencyProperty.Register(nameof(Text), typeof(string), typeof(MenuButton), new(string.Empty, new(OnSomeTextPropertyChanged)));

    public static readonly DependencyProperty MenuPlacementProperty =
        DependencyProperty.Register(nameof(MenuPlacement), typeof(PlacementMode), typeof(MenuButton), new(PlacementMode.Top, new(OnSomeMenuPlacementPropertyChanged)));

    public static readonly DependencyProperty MenuItemsProperty =
        DependencyProperty.Register(nameof(MenuItems), typeof(ItemCollection), typeof(MenuButton), new(default(ItemCollection), new(OnSomeMenuItemsPropertyChanged)));

    public SolidColorBrush ArrowColor
    {
        get => arrowColor;
        private set
        {
            arrowColor = value;
            OnPropertyChanged(nameof(arrowColor));
        }
    }

    public string Text
    {
        get => (string)GetValue(TextProperty);
        set => SetCurrentValue(TextProperty, value);
    }

    public PlacementMode MenuPlacement
    {
        get => (PlacementMode)GetValue(MenuPlacementProperty);
        set => SetCurrentValue(MenuPlacementProperty, value);
    }

    public ItemCollection MenuItems
    {
        get => (ItemCollection)GetValue(MenuItemsProperty);
        set => SetCurrentValue(MenuItemsProperty, value);
    }

    public MenuButton()
    {
        Menu = new()
        {
            StaysOpen = true,
            HasDropShadow = false,
            PlacementTarget = this,
            Placement = MenuPlacement,
            ItemsSource = MenuItems,
        };

        SetCurrentValue(MenuItemsProperty, new DataGrid().Items);

        InitializeComponent();
    }

    public event PropertyChangedEventHandler PropertyChanged;
    public void OnPropertyChanged([CallerMemberName] string prop = "")
    {
        PropertyChanged?.Invoke(this, new(prop));
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseLeftButtonDown(e);

        Menu.Width = ActualWidth;
        Menu.IsOpen = true;
    }

    private void MenuButtonControl_IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (sender is MenuButton button && bool.TryParse(e.NewValue?.ToString(), out bool enabled))
        {
            button.ArrowColor = enabled ? Brushes.Black : Brushes.DimGray;
        }
    }

    private static void OnSomeTextPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton)
        {
            menuButton.ButtonText.Content = e.NewValue.ToString();
        }
    }

    private static void OnSomeMenuPlacementPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && Enum.TryParse(e.NewValue?.ToString(), true, out PlacementMode placementMode))
        {
            menu.Placement = placementMode;
        }
    }

    private static void OnSomeMenuItemsPropertyChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
    {
        if (target is MenuButton menuButton && menuButton.Menu is ContextMenu menu && e.NewValue is ItemCollection itemCollection)
        {
            menu.ItemsSource = itemCollection;
            menu.IsOpen = true;
            menu.IsOpen = false;
        }
    }
}

XAML:

<Button x:Name="MenuButtonControl"
        x:Class="Controls.UserControls.MenuButton"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
        mc:Ignorable="d"
        d:DesignHeight="20"
        d:DesignWidth="100"
        HorizontalContentAlignment="Stretch"
        VerticalContentAlignment="Stretch"
        IsEnabledChanged="MenuButtonControl_IsEnabledChanged">
    <Button.Content>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="*"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="Auto"></ColumnDefinition>
            </Grid.ColumnDefinitions>

            <Label x:Name="ButtonText" Grid.Row="0" Grid.Column="0" Margin="0" Padding="0" Content="{Binding Text, ElementName=MenuButtonControl}" HorizontalAlignment="Center" VerticalAlignment="Stretch"></Label>
            <Path Grid.Row="0" Grid.Column="1" Margin="5,0" Fill="{Binding ArrowColor, ElementName=MenuButtonControl}" Data="M 0 0 L 4 5 L 8 0 Z" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
    </Button.Content>
</Button>

Solution

  • Wpf has a control for this purpose. You can create a custom combo box that uses popup to show the items and you can change the popup animation to PopupAnimation="Fade" here is the code

    <UserControl x:Class="StackOverFlowQuestionOrAnswer.MenuButton"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:StackOverFlowQuestionOrAnswer"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <ControlTemplate x:Key="ComboBoxToggleButton" TargetType="{x:Type ToggleButton}">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="*" />
                    <ColumnDefinition Width="20" />
                </Grid.ColumnDefinitions>
                <Border
              x:Name="Border" 
              Grid.ColumnSpan="2"
              CornerRadius="0"
              Background="#FF3F3F3F"
              BorderBrush="#FF97A0A5"
              BorderThickness="1" />
                <Border 
              Grid.Column="0"
              CornerRadius="0" 
              Margin="1" 
              Background="#FF3F3F3F" 
              BorderBrush="#FF97A0A5"
              BorderThickness="0,0,1,0">
                    <TextBlock Foreground="#fff"
                           FontSize="25"
                           Text="Copy/Paste"
                           VerticalAlignment="Center"/>
                </Border>
                <Path 
              x:Name="Arrow"
              Grid.Column="1"     
              Fill="White"
              HorizontalAlignment="Center"
              VerticalAlignment="Center"
              Data="M 0 0 L 4 5 L 8 0 Z"
            />
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter TargetName="Border" Property="Background" Value="#FF3F3F3F" />
                    <Setter TargetName="Border" Property="BorderBrush" Value="#d4d2d2" />
                    <Setter Property="Foreground" Value="#888888"/>
                    <Setter TargetName="Arrow" Property="Fill" Value="#888888" />
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    
        <ControlTemplate x:Key="ComboBoxTextBox" TargetType="{x:Type TextBox}">
            <Border x:Name="PART_ContentHost" Focusable="False" Background="{TemplateBinding Background}" />
        </ControlTemplate>
    
        <Style x:Key="{x:Type ComboBox}" TargetType="{x:Type ComboBox}">
            <Setter Property="SnapsToDevicePixels" Value="true"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
            <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
            <Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
            <Setter Property="MinHeight" Value="20"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ComboBox}">
                        <Grid>
                            <ToggleButton
                        Name="ToggleButton" 
                        Template="{StaticResource ComboBoxToggleButton}" 
                        Grid.Column="2" 
                        Focusable="false"
                        IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}">
                            </ToggleButton>
                            <!--If you want to show the selected item use this-->
                            <!--<ContentPresenter Name="ContentSite" IsHitTestVisible="False"  Content="{TemplateBinding SelectionBoxItem}"
                        ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                        ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                        Margin="3,3,23,3"
                        VerticalAlignment="Center"
                        HorizontalAlignment="Left" />-->
                            <Popup 
                        Name="Popup"
                        Placement="Top"
                        IsOpen="{TemplateBinding IsDropDownOpen}"
                        AllowsTransparency="True" 
                        Focusable="False"
                        PopupAnimation="Fade">
    
                                <Grid Name="DropDown"
                                  VerticalAlignment="Center"
                                  HorizontalAlignment="Center"
                          SnapsToDevicePixels="True"                
                          MinWidth="{TemplateBinding ActualWidth}"
                          MaxHeight="{TemplateBinding MaxDropDownHeight}">
                                    <Border 
                            x:Name="DropDownBorder"
                            Background="#FF3F3F3F"
    
                            BorderThickness="1"
                            BorderBrush="#888888"/>
                                    <ScrollViewer SnapsToDevicePixels="True">
                                        <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
                                    </ScrollViewer>
                                </Grid>
                            </Popup>
                        </Grid>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
            <Style.Triggers>
            </Style.Triggers>
        </Style>
        <Style x:Key="{x:Type ComboBoxItem}" TargetType="{x:Type ComboBoxItem}">
            <Setter Property="SnapsToDevicePixels" Value="true"/>
            <Setter Property="Foreground" Value="White"/>
            <Setter Property="OverridesDefaultStyle" Value="true"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ComboBoxItem}">
                        <Border Name="Border"
                          Padding="2"
                          SnapsToDevicePixels="true">
                            <ContentPresenter />
                        </Border>
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsHighlighted" Value="true">
                                <Setter TargetName="Border" Property="Background" Value="#15cf81"/>
                            </Trigger>
                            <Trigger Property="IsEnabled" Value="false">
                                <Setter Property="Foreground" Value="#888888"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    
    
    
    
    </UserControl.Resources>
    <Grid Height="50">
        <ComboBox Width="200">
            <ComboBoxItem>
                <TextBlock Text="WWWWW"></TextBlock>
            </ComboBoxItem>
            <ComboBoxItem>
                <TextBlock Text="WWWWW"></TextBlock>
            </ComboBoxItem>
        </ComboBox>
    </Grid>