Search code examples
wpfbuttonpopupcontrols

Custom Popup control not closing


I'm trying to create a custom dropdown control that acts like a ComboBox, such that the Popup opens when you click mouse down (not up), and closes when you click outside of the control.

The problem is that it only behaves if I set ClickMode to "Release". But what I really want is ClickMode="Press", such that the Popup opens on MouseDown instead of MouseUp.

But when I set it to ClickMode="Press", the popup won't close when you click outside the control.

Any ideas how I can achieve this?

Usage :

          <StackPanel>
            <local:CustomDropdown Width="200"
                                  Height="50"
                                  Content="Custom!" />

            <ComboBox Width="200"
                      Margin="20"> 
                <ComboBoxItem>A</ComboBoxItem>
                <ComboBoxItem>B</ComboBoxItem>
                <ComboBoxItem>C</ComboBoxItem>
            </ComboBox>
        </StackPanel>

Class :

internal class CustomDropdown : ContentControl
{
    public bool IsOpen
    {
        get { return (bool)GetValue(IsOpenProperty); }
        set { SetValue(IsOpenProperty, value); }
    }

    public static readonly DependencyProperty IsOpenProperty =
        DependencyProperty.Register("IsOpen", typeof(bool), typeof(CustomDropdown), new PropertyMetadata(false));
}

Xaml :

<Style TargetType="{x:Type local:CustomDropdown}">
            <Style.Setters>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <Grid>
                                <ToggleButton IsChecked="{Binding IsOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" 
                                              ClickMode="Press"/>
                                <ContentPresenter Content="{Binding Content, RelativeSource={RelativeSource TemplatedParent}}"
                                                  HorizontalAlignment="Center"
                                                  VerticalAlignment="Center"/>
                                <Popup StaysOpen="False"
                                       Placement="Bottom"
                                       IsOpen="{Binding IsOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
                                    <Border Background="White"
                                            BorderBrush="Black"
                                            BorderThickness="1"
                                            Padding="50">
                                        <TextBlock Text="Popup!" />
                                    </Border>
                                </Popup>
                            </Grid>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style.Setters>
        </Style>

Solution

  • You already have a working answer. However, finding the parent Window and parent ToggleButton can impact performance (depending on the depth of the visual tree).
    As an alternative solution I suggest to focus on handling the Popup instead.

    There are two conditions that prevent the Popup from closing itself: the button is configured with the ButtonBase.ClickMode set to ClickMode.Pressed AND the user is not clicking anything focusable inside the Popup.

    If one of those two conditions evaluates to false (=> ClickMode.Release or the user has moved focus inside the Popup) your code will work as you would expected it to work.
    Note that in order to allow the user to move focus inside the Popup, there must be a child that is focusable (UIElement.Focusable is set to true - it's false by default for most controls that don't require user interaction). For example, TextBlock is not focusable by default.

    Because you want to keep the button configured to raise the Click event on mouse button press, you have to move the focus manually. But when you set it manually, the Popup won't receive a mouse click to setup itself to watch the focus. Therefore, you will end up closing the Popup manually (taking away the related control from the Popup).

    The following example closes the Popup by observing the Mouse.PreviewMouseDownOutsideCapturedElement event to identify when the focus has moved away from the CustomDropdown control (mouse click outside the Popup):

    CustomDropdown.cs

    internal class CustomDropdown : ContentControl
    {
      public bool IsOpen
      {
        get => (bool)GetValue(IsOpenProperty);
        set => SetValue(IsOpenProperty, value);
      }
    
      public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register(
        "IsOpen",
        typeof(bool),
        typeof(CustomDropdown),
        new PropertyMetadata(default(bool), OnIsOpenChanged));
    
      public CustomDropdown()
      {
        Mouse.AddPreviewMouseDownOutsideCapturedElementHandler(this, OnPreviewMouseDownOutsideCapturedElement);
      }
    
      private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        bool isOpen = (bool)e.NewValue;
        if (isOpen)
        {
          _ = Mouse.Capture(d as IInputElement, CaptureMode.SubTree);
        }
        else
        {
          _ = Mouse.Capture(null);
        }
      }
    
      // Manually close the Popup if click is recorded outside the CustomDropdown/Popup 
      private void OnPreviewMouseDownOutsideCapturedElement(object sender, MouseButtonEventArgs e) 
      {
        SetCurrentValue(IsOpenProperty, false);
      }
    }