Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WPF Behavior to close a Popup with MouseLeave

5.00/5 (7 votes)
3 Apr 2018CPOL2 min read 23.6K   377  
Normally a popup will stay open until explicitly closed. The Behavior will close the Popup if the mouse is clicked within or outside the Popup area or leaves the Popup area.

Introduction

I was using a Popup to implement a ListBox that would appear after a ToggleButton with the current selection is pressed. Unfortunately, it was after I had implemented everything that I discovered that closing the Popup was not that straight forward.

The desired functionality of having the Popup close is the following:

  1. The user clicks on the Popup.
  2. The mouse leaves the area of the Popup. One of the concerns was that the mouse may not initially be in the area of the Popup, and want to allow the user to move the mouse into the area of the Popup.
  3. The user clicks on an area outside of the Popup.
  4. The Popup looses Focus.

Background

The following is the code for the behavior:

C#
public static class ClosePopupBehavior
{
    public static ContentControl GetPopupContainer(DependencyObject obj)
    {
        return (ContentControl)obj.GetValue(PopupContainerProperty);
    }

    public static void SetPopupContainer(DependencyObject obj, ContentControl value)
    {
        obj.SetValue(PopupContainerProperty, value);
    }

    public static readonly DependencyProperty PopupContainerProperty =
        DependencyProperty.RegisterAttached("PopupContainer",
            typeof(ContentControl), typeof(ClosePopupBehavior)
        , new PropertyMetadata(OnPopupContainerChanged));

    private static void OnPopupContainerChanged(DependencyObject d
        , DependencyPropertyChangedEventArgs e)
    {
        var popup = (Popup)d;
        var contentControl = e.NewValue as ContentControl;

        popup.LostFocus += (sender, args) =>
        {
            var popup1 = (Popup)sender;
            popup.IsOpen = false;
            if (contentControl != null)
                contentControl.PreviewMouseDown -= ContainerOnPreviewMouseDown;
        };
        popup.Opened += (sender, args) =>
        {
            var popup1 = (Popup)sender;
            popup.Focus();
            SetWindowPopup(contentControl, popup1);
            contentControl.PreviewMouseDown -= ContainerOnPreviewMouseDown;
            contentControl.PreviewMouseDown += ContainerOnPreviewMouseDown;
        };
        popup.PreviewMouseUp += (sender, args) =>
        {
            popup.IsOpen = false;
            if (contentControl != null)
                contentControl.PreviewMouseDown -= ContainerOnPreviewMouseDown;
        };
        popup.MouseLeave += (sender, args) =>
        {
            popup.IsOpen = false;
            if (contentControl != null)
                contentControl.PreviewMouseDown -= ContainerOnPreviewMouseDown;
        };
        popup.Unloaded += (sender, args) =>
        {
            popup.IsOpen = false;
            if (contentControl != null)
                contentControl.PreviewMouseDown -= ContainerOnPreviewMouseDown;
        };
    }

    //This is really to handle touch panel, Not sure if it is needed since handle LostFocus.
    //Remove the contentControl stuff if it is not needed
    private static void ContainerOnPreviewMouseDown(object sender
        , MouseButtonEventArgs mouseButtonEventArgs)
    {
        var popup = GetWindowPopup((DependencyObject)sender);
        popup.IsOpen = false;
        ((FrameworkElement)sender).PreviewMouseUp -= ContainerOnPreviewMouseDown;
    }

    private static Popup GetWindowPopup(DependencyObject obj)
    {
        return (Popup)obj.GetValue(WindowPopupProperty);
    }

    private static void SetWindowPopup(DependencyObject obj, Popup value)
    {
        obj.SetValue(WindowPopupProperty, value);
    }

    private static readonly DependencyProperty WindowPopupProperty =
        DependencyProperty.RegisterAttached("WindowPopup",
            typeof(Popup), typeof(ClosePopupBehavior));
}

The behavior is initiated by providing a ContentControl in the PopupContainer DependencyProperty. This does not have to be a Window, it can be any ContentControl, which can be a Window or UserControl.

The change event handler for this DependencyProperty then attaches event handlers for the following events:

  • LostFocus: Closes the Popup when the Popup looses focus and removes the PreviewMouseUp event handler from the PopupContainer.
  • Opened: Attaches a event handler to the PreviewMouseDown on the PopupContainer. This allows the PopupContainer PreviewMouseDown event handler to close the Popup and remove the event handler.
  • PreviewMouseUp: Closes the Popup on PreviewMouseUp of the Popup and removes the PreviewMouseUp event handler from the PopupContainer.
  • MouseLeave: Closes the Popup on MouseLeave of the Popup and removes the PreviewMouseUp event handler from the PopupContainer.
  • Unloaded: Removes the PreviewMouseUp event handler from the PopupContainer.

The code is probably a bit too overprotected in the sense that some of these event handlers are probably not needed, and the handling of the PreviewMouseDown is probably not needed except I was not sure about the case of touch interaction.

Using the Code

The Behavior is only useable on a Popup Control. All that is needed is to include the behavior in the XAML for the Popup, and reference a ContentControl containing the Popup:

XML
<Popup local:ClosePopupBehavior.PopupContainer="{Binding ElementName=Window}"
        IsOpen="{Binding ElementName=ToggleButton1,
                         Path=IsChecked}"
        PlacementTarget="{Binding ElementName=ToggleButton1}">
     <Border Width="200"
             Height="100"
             Background="LightBlue" />
 </Popup>

The Sample

Image 1

The bottom ToggleButton opens a PopUp which does not have the behavior attached. The Popup has a Background Color of Pink. You will notice that the only way to close the Popup is to press the ToggleButton again. Hopefully, the ToggleButton is not hidden.

Image 2

The top ToggleButton opens a PopUp which does have the behavior attached. The Popup has a Background Color of LightBlue.

History

  • 04/03/2018: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)