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:
- The user clicks on the
Popup
. - 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
. - The user clicks on an area outside of the
Popup
. - The
Popup
looses Focus
.
Background
The following is the code for the behavior:
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;
};
}
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
:
<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
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.
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