Introduction
This article addresses an issue with the WPF Popup control, which can be very annoying sometimes. The issue I am referring to is the logic of the behavior behind the StaysOpen
property. If you've ever tried to use a popup in a control template, you might have run into the problem where the popup either refuses to disappear when you want it to, or it disappears too soon. This article addresses a specific requirement where the popup is to be closed once a click is performed inside it. The default behavior causes the popup to close only when you click outside its area.
Furthermore, the popup refuses to close when you hit a key when your keyboard focus is not outside the popup area. The solution described in this article makes the popup close on any keypress.
The Implementation
The implementation of this solution uses a behavior class with an attached property called ClosesOnInput
. When set to true
, this attached property registers for the popup's PreviewMouseUp
event, and in the event handler, assigns false
to its IsOpen
property, thereby closing it. Note that we only need to register for the popup's own PreviewMouseUp
events, since the StaysOpen
property handles mouse events outside of it.
Here is the definition of the class and the attached property; notice that we specify a callback (highlighted in the snippet below) in the metadata of the attached property in order to detect its change.
public class PopupBehavior
{
public static bool GetClosesOnInput(DependencyObject obj)
{
return (bool)obj.GetValue(ClosesOnInputProperty);
}
public static void SetClosesOnInput(DependencyObject obj, bool value)
{
obj.SetValue(ClosesOnInputProperty, value);
}
public static readonly DependencyProperty ClosesOnInputProperty =
DependencyProperty.RegisterAttached(
"ClosesOnInput",
typeof(bool),
typeof(PopupBehavior),
new UIPropertyMetadata(false, OnClosesOnInputChanged));
And here is the callback so far:
static void OnClosesOnInputChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is Popup == false) return;
Popup popup = (Popup)depObj;
bool value = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (value && !oldValue)
{
popup.PreviewMouseUp += new MouseButtonEventHandler(Popup_PreviewMouseUp);
}
else if(!value && oldValue)
{
popup.PreviewMouseUp -= Popup_PreviewMouseUp;
}
}
static void Popup_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
Popup popup = (Popup)sender;
popup.IsOpen = false;
}
The second part of the implementation deals with handling the keyboard events. This is slightly less trivial, since we need to register for the entire window's PreviewKeyUp
event, rather than the popup's. We do this by travelling up the visual tree to the topmost FrameworkElement
, and we register to its PreviewKeyUp
event.
This is the method that finds the topmost FrameworkElement
:
private static FrameworkElement FindTopLevelElement(Popup popup)
{
FrameworkElement iterator, nextUp = popup;
do
{
iterator = nextUp;
nextUp = VisualTreeHelper.GetParent(iterator) as FrameworkElement;
} while (nextUp != null);
return iterator;
}
And here's the code snippet for registering and unregistering from the appropriate event:
var topLevelElement = FindTopLevelElement(popup);
topLevelElement.PreviewKeyUp += new KeyEventHandler(TopLevelElement_PreviewKeyUp);
topLevelElement.PreviewKeyUp -= TopLevelElement_PreviewKeyUp;
However, here comes the tricky part, the registering is easy, but how do we remember which element we registered for when the time comes to unregister? Sure we can travel up the tree again, but what if the control was reparented? This is not uncommon. To get around that, we store the association between the popup and the element we used in a dictionary. Moreover, we create a context capture object which remembers it, and which contains methods to register and unregister from the event.
These are the dictionary and context class definitions:
static Dictionary<Popup, PopupTopLevelContext> TopLevelPopupAssociations =
new Dictionary<Popup, PopupTopLevelContext>();
class PopupTopLevelContext
{
private Popup popup;
private FrameworkElement topLevelElement;
internal PopupTopLevelContext(Popup Popup, FrameworkElement TopLevelElement)
{
popup = Popup;
topLevelElement = TopLevelElement;
TopLevelElement.PreviewKeyUp += Popup_PreviewKeyUp;
}
internal void Popup_PreviewKeyUp(object sender, KeyEventArgs e)
{
popup.IsOpen = false;
}
internal void Release()
{
topLevelElement.PreviewKeyUp -= Popup_PreviewKeyUp;
}
}
And here is the complete callback function with everything thrown in:
static void OnClosesOnInputChanged(
DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
if (depObj is Popup == false) return;
Popup popup = (Popup)depObj;
bool value = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (value && !oldValue)
{
popup.PreviewMouseUp += new MouseButtonEventHandler(Popup_PreviewMouseUp);
var topLevelElement = FindTopLevelElement(popup);
PopupTopLevelContext cp = new PopupTopLevelContext(popup, topLevelElement);
TopLevelPopupAssociations[popup] = cp;
}
else if(!value && oldValue)
{
popup.PreviewMouseUp -= Popup_PreviewMouseUp;
TopLevelPopupAssociations[popup].Release();
TopLevelPopupAssociations.Remove(popup);
}
}
Using the Code
Using the code is now as simple as referencing the appropriate CLR namespace in XAML, and assigning a value to an attached property. The following example shows a templated checkbox which has an auto closing popup bound to it. This type of control is also known as a popup button or a menu button. The important parts are highlighted. Please excuse the horizontal scrollbar, XAML bindings can be very verbose.
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WPFPopupBehavior"
x:Class="WPFPopupBehavior.Window1"
x:Name="Window"
Title="Window1"
Width="300" Height="200">
<Window.Resources>
<Style x:Key="RBStyle1" TargetType="RadioButton">
<Setter Property="Margin" Value="3,3,15,3"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="GroupName" Value="MyGroup"/>
</Style>
<Style x:Key="CBStyle1" TargetType="{x:Type CheckBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type CheckBox}">
<Grid>
<ContentPresenter
HorizontalAlignment=
"{TemplateBinding HorizontalContentAlignment}"
Margin="{TemplateBinding Padding}"
VerticalAlignment=
"{TemplateBinding VerticalContentAlignment}" />
<Popup
IsOpen="{Binding RelativeSource=
{RelativeSource TemplatedParent},
Path=IsChecked, Mode=TwoWay}"
StaysOpen="False"
PopupAnimation="Slide"
local:PopupBehavior.ClosesOnInput="True">
<StackPanel>
<RadioButton Content="Option 1"
IsChecked="True" Style="{StaticResource RBStyle1}"/>
<RadioButton Content="Option 2"
Style="{StaticResource RBStyle1}"/>
<RadioButton Content="Option 3"
Style="{StaticResource RBStyle1}"/>
<RadioButton Content="Option 4"
Style="{StaticResource RBStyle1}"/>
</StackPanel>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid x:Name="LayoutRoot">
<CheckBox Margin="30,30,0,0" Style="{DynamicResource CBStyle1}"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Ellipse Fill="Red" Width="50" Height="50"/>
</CheckBox>
</Grid>
</Window>
Points of Interest
Sometimes you feel like the world (and Microsoft's WPF designers) are against you, and that they are forcing you to code stuff that you do not want to! Like reaching into the visual tree and manhandling controls directly. However, the truly enlightened WPF programmer knows that in 99% of the cases, that is not necessary, and that such cases can be solved by using attached properties, data binding, templating, etc.
That's it, enjoy.
History
- 23rd November, 2009: Initial post