Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Customizing a Popup's Auto-Close Behavior

0.00/5 (No votes)
23 Nov 2009 1  
Using attached properties to customize the conditions under which a popup closes

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);
    }

    // Using a DependencyProperty as the backing store for ClosesOnInput.
    // This enables animation, styling, binding, etc...
    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:

// Registering
var topLevelElement = FindTopLevelElement(popup);
topLevelElement.PreviewKeyUp += new KeyEventHandler(TopLevelElement_PreviewKeyUp);

// Unregistering
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)
    {
        // Register for the popup's PreviewMouseUp event.
        popup.PreviewMouseUp += new MouseButtonEventHandler(Popup_PreviewMouseUp);
        
        // Obtain the top level element and register to its PreviewKeyUp event
        // using a context object.
        var topLevelElement = FindTopLevelElement(popup);
        PopupTopLevelContext cp = new PopupTopLevelContext(popup, topLevelElement);
        
        // Associate the popup with the context object, for the unregistering operation.
        TopLevelPopupAssociations[popup] = cp;
    }
    else if(!value && oldValue)
    {
        // Unregister from the popup's PreviewMouseUp event.
        popup.PreviewMouseUp -= Popup_PreviewMouseUp;
        
        // Tell the context object to unregister from the PreviewKeyUp event
        // of the appropriate element.
        TopLevelPopupAssociations[popup].Release();
        
        // Disassociate the popup from the context object. The context object
        // is now unreferenced and may be garbage collected.
        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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here