How to dismiss a popup in Silverlight when clicking outside of the control?
Asked Answered
S

7

23

In my Silverlight UI, I have a button that when clicked pops up a control with some filtering parameters. I would like this control to hide itself when you click outside of it. In other words, it should function in a manner similar to a combo box, but it's not a combo box (you don't select an item in it). Here's how I'm trying to capture a click outside of the control to dismiss it:

public partial class MyPanel : UserControl
{
    public MyPanel()
    {
        InitializeComponent();
    }

    private void FilterButton_Click(object sender, RoutedEventArgs e)
    {
        // Toggle the open state of the filter popup
        FilterPopup.IsOpen = !FilterPopup.IsOpen;
    }

    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
        // Capture all clicks and close the popup
        App.Current.RootVisual.MouseLeftButtonDown += delegate {
            FilterPopup.IsOpen = false; };
    }
}

Unfortunately, the event handler for MouseLeftButtonDown is never getting fired. Is there a well-established way of making a popup control that auto-dismisses when you click outside of it? If not, why isn't my MouseLeftButtonDown handler firing?

Solution:

I thought I'd post my entire solution in case others find it helpful. In my top-level visual, I declare a "shield" for the popups, like this:

<UserControl xmlns:my="clr-namespace:Namespace"
    x:Class="Namespace.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation" 
    xmlns:uriMapper="clr-namespace:System.Windows.Navigation;assembly=System.Windows.Controls.Navigation"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
>
  <Grid Background="Black" HorizontalAlignment="Stretch" 
          VerticalAlignment="Stretch">
    <my:MyStuff/>
    <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
            x:Name="PopupShield" Background="Transparent" Width="Auto" 
            Height="Auto" Visibility="Collapsed"/>
  </Grid>
</UserControl>

Then, I added an extension method for the Popup class, like this:

public static class PopupUtils
{
    public static void MakeAutoDismissing(this Popup popup)
    {
        var shield = (App.Current.RootVisual as MainPage).PopupShield;

        // Whenever the popup opens, deploy the shield
        popup.HandlePropertyChanges(
            "IsOpen",
            (s, e) =>
            {
                shield.Visibility = (bool)e.NewValue 
                    ? Visibility.Visible : Visibility.Collapsed;
            }
        );

        // Whenever the shield is clicked, dismiss the popup
        shield.MouseLeftButtonDown += (s, e) => popup.IsOpen = false;
    }
}

public static class FrameworkUtils
{
    public static void HandlePropertyChanges(
        this FrameworkElement element, string propertyName, 
        PropertyChangedCallback callback)
    {
        //Bind to a depedency property
        Binding b = new Binding(propertyName) { Source = element };
        var prop = System.Windows.DependencyProperty.RegisterAttached(
            "ListenAttached" + propertyName,
            typeof(object),
            typeof(UserControl),
            new System.Windows.PropertyMetadata(callback));

        element.SetBinding(prop, b);
    }
}

The extension method is used like this:

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    FilterPopup.MakeAutoDismissing();
}
Suanne answered 23/2, 2010 at 22:25 Comment(1)
Your solution worked for me. It will be better to create an interface with a single property PopupShield and use this in MakeAutoDismissing method. Your MainPage or other UserControl will have to implement the property and return the transparent Canvas. (Why not posting your solution as an answer so people can vote for it?)Osher
C
4

Did you set a background color on your RootVisual?

Clipfed answered 24/2, 2010 at 4:33 Comment(3)
Good thought. I'll try that tomorrow.Suanne
Once I had an opaque background, I started getting the mouse down events.Suanne
This should also work if use "Transparent" as your background color.Cushitic
D
5

One way is to put your control on a transparent canvas that fills the entire Silverlight surface. When the canvas is clicked close the canvas and control. It is important to ensure that the Background brush of the canvas is set to "Transparent" if you want to receive mouse events.

An alternate method that I have not had success with is using mouse capture in Silverlight and detecting when the mouse is clicked outside of the popup.

Dispensation answered 23/2, 2010 at 22:49 Comment(1)
I wish I could accept this answer as well. The problem with just capturing events on the root visual is that descendant controls like buttons blocked the mouse down events. So I created a top-level canvas that would reside just below popup controls. When showing the popup, I make this "shield" visible and make it collapsed when the popup is dismissed.Suanne
C
4

Did you set a background color on your RootVisual?

Clipfed answered 24/2, 2010 at 4:33 Comment(3)
Good thought. I'll try that tomorrow.Suanne
Once I had an opaque background, I started getting the mouse down events.Suanne
This should also work if use "Transparent" as your background color.Cushitic
S
3

I just created something similar and hopefully this could be of any help. :)

In my PopUp control, I have a Border which has a Grid that contains a number of textboxes. I named the Border 'PopUpBorder'.

In my UserControl's constructor, I have,

        this.PopUpBorder.MouseLeave += (s, e) =>
        {
            Application.Current.RootVisual.MouseLeftButtonDown += (s1, e1) =>
            {
                this.PopUp.IsOpen = false;
            };
        };

And looks like it is working as expected. Please let me know if this doesn't work in your case.

Sikh answered 11/4, 2011 at 5:8 Comment(2)
Isn't it subscribing to the same event every time mouse leaves the popup border? Maybe, it results unnecessary use of memory?Aga
I really like this answer for its simplicity.Cyclopean
C
1

On the first click, call the CaptureMouse() method on the control. Then call ReleaseMouseCapture() on the second click.

Canaanite answered 23/2, 2010 at 22:55 Comment(1)
I'd like to do something this simple, but I cannot get it working. Do you have more specific instructions on how to do this or sample code?Suanne
S
1

The proposed solution uses a special shield canvas to block input to other controls. This is fine, but I am trying implement a generic control and cannot depend on the existence of such a shield canvas. The binding of the MouseLeftButtonDown event on the root visual didn't work for me either, because existing buttons on my canvas would still fire their own click event and not the MouseLeftButtonDown on the root visual. I found the solution in this ice article: Popup.StaysOpen in Silverlight from Kent Boograart. The main methods for this question are:

        private void OnPopupOpened(object sender, EventArgs e)
        {
            var popupAncestor = FindHighestAncestor(this.popup);

            if (popupAncestor == null)
            {
                return;
            }

            popupAncestor.AddHandler(Windows.Popup.MouseLeftButtonDownEvent, (MouseButtonEventHandler)OnMouseLeftButtonDown, true);
        }

        private void OnPopupClosed(object sender, EventArgs e)
        {
            var popupAncestor = FindHighestAncestor(this.popup);

            if (popupAncestor == null)
            {
                return;
            }

            popupAncestor.RemoveHandler(Windows.Popup.MouseLeftButtonDownEvent, (MouseButtonEventHandler)OnMouseLeftButtonDown);
        }

        private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            // in lieu of DependencyObject.SetCurrentValue, this is the easiest way to enact a change on the value of the Popup's IsOpen
            // property without overwriting any binding that may exist on it
            var storyboard = new Storyboard() { Duration = TimeSpan.Zero };
            var objectAnimation = new ObjectAnimationUsingKeyFrames() { Duration = TimeSpan.Zero };
            objectAnimation.KeyFrames.Add(new DiscreteObjectKeyFrame() { KeyTime = KeyTime.FromTimeSpan(TimeSpan.Zero), Value = false });
            Storyboard.SetTarget(objectAnimation, this.popup);
            Storyboard.SetTargetProperty(objectAnimation, new PropertyPath("IsOpen"));
            storyboard.Children.Add(objectAnimation);
            storyboard.Begin();
        }
    private static FrameworkElement FindHighestAncestor(Popup popup)
    {
        var ancestor = (FrameworkElement)popup;

        while (true) {
            var parent = VisualTreeHelper.GetParent(ancestor) as FrameworkElement;

            if (parent == null) {
                return ancestor;
            }

            ancestor = parent;
        }
    }

I'm still not sure, why this solution works for me and this not:

    Application.Current.RootVisual.MouseLeftButtonDown += (s1, e1) =>
    {
        this.PopUp.IsOpen = false;
    };

It looks like the FindHighestAncestor method will just return the root visual? But then the difference must be the event handler? I guess the main difference is the last parameter of the AddHandler method which is true in the case:

AddHandler(Windows.Popup.MouseLeftButtonDownEvent, (MouseButtonEventHandler)OnMouseLeftButtonDown, true);

The MSDN docs say:

handledEventsToo

Type: System.Boolean

true to register the handler such that it is invoked even when the routed event is marked handled in its event data; false to register the handler with the default condition that it will not be invoked if the routed event is already marked handled. The default is false. Do not routinely ask to rehandle a routed event. For more information, see Remarks.

Stuckup answered 4/5, 2011 at 6:44 Comment(0)
C
0

A simpler alternative (although not exactly what you asked for) would be to close the popup on MouseLeave. MouseLeave on the Popup object itself wont work, but MouseLeave on the highest level container within the Popup does.

Classicist answered 22/9, 2011 at 19:12 Comment(0)
B
0

i have posted a solution for this on my blog, you can see the post here - Silverlight: close Popup on click outside. as you can see the use is very simple - it is an attached property you add on your popup. you do not have to add any wrappers, you do not have to take care if you do or do not have some background color... my code will take care of it all.

Boathouse answered 9/12, 2013 at 9:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.