Silverlight: close Popup on click outside

This was a hard one…
One of the things that is very much missing in Silverlight is closing a Popup upon clicking outside of it.
this is implemented as StaysOpen (true/false) in WPF.
but is very much missing in Silverlight Popup.

i wanted to make it as simple as possible, without any dependencies on other dll, and support binding changes.
so an attached property was the way to go.

lots of thanks to Vladi (koganvladimir at yahoo dot com) on helping here.

to use it simply add the attached property to your Popup, when the value is set to False the popup will be closed upon clicking outside.
here are two common use case examples:

<Popup helpers:PopupHelper.StaysOpen="False">
<Popup helpers:PopupHelper.StaysOpen="{Binding SomeChangingBool}">

and here is the PopupHelper.cs class:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;

namespace Helpers
{
    public class PopupHelper
    {

        public static bool GetStaysOpen(DependencyObject obj)
        {
            return (bool)obj.GetValue(StaysOpenProperty);
        }

        public static void SetStaysOpen(DependencyObject obj, bool value)
        {
            obj.SetValue(StaysOpenProperty, value);
        }

        public static readonly DependencyProperty StaysOpenProperty =
            DependencyProperty.RegisterAttached("StaysOpen", typeof(bool), typeof(PopupHelper),
                                                new PropertyMetadata(true, StaysOpenChanged));

        private static void StaysOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var pop = d as Popup;
            if (pop == null) return;

            // this is the only way i could find to tell (or wait) for after loaded.
            if (pop.Child == null)
            {
                pop.Loaded += PopOnLoaded;
            }
            else
            {
                UpdateStaysOpen(pop, (bool)e.NewValue);
            }
        }

        private static void PopOnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var pop = sender as Popup;
            if (pop == null) return;
            pop.Loaded -= PopOnLoaded;
            UpdateStaysOpen(pop, (bool)pop.GetValue(StaysOpenProperty));
        }

        private static void UpdateStaysOpen(Popup popup, bool stayOpen)
        {
            var blocker = GetBlocker(popup);
            if (blocker == null)
            {
                SetBlockerLayer(popup);
                blocker = GetBlocker(popup);
            }
            blocker.IsHitTestVisible = !stayOpen;
        }

        private static Canvas GetBlocker(Popup pop)
        {
            var elementPopupChildCanvas = pop.Child as FrameworkElement;
            var blocker = VisualTreeHelper.GetChild(elementPopupChildCanvas, 0) as FrameworkElement;
            Canvas retVal;
            if ((retVal = blocker as Canvas) != null && blocker.Tag.ToString() == "ElementPopupBlocker")
            {
                return retVal;
            }
            return null;
        }

        private static void SetBlockerLayer(Popup popup)
        {
            var popupChild = popup.Child as FrameworkElement;
            if (popupChild == null) return;
            var blocker = new Canvas { Background = new SolidColorBrush(Colors.Gray), Tag = "ElementPopupBlocker" };
            blocker.MouseLeftButtonDown += delegate { popup.IsOpen = false; };
            var elementPopupChildCanvas = new Canvas();
            popup.Child = elementPopupChildCanvas;
            elementPopupChildCanvas.Children.Add(blocker);
            elementPopupChildCanvas.Children.Add(popupChild);

            popupChild.HorizontalAlignment = HorizontalAlignment.Left;
            popupChild.VerticalAlignment = VerticalAlignment.Top;
            Canvas.SetLeft(popupChild, popup.HorizontalOffset);
            Canvas.SetTop(popupChild, popup.VerticalOffset);
            popup.HorizontalOffset = 0.0;
            popup.VerticalOffset = 0.0;

            popup.LayoutUpdated += delegate { Arrange(popup); };
        }

        private static void Arrange(Popup popup)
        {
            if (!popup.IsOpen) return;
            var elementPopupChildCanvas = popup.Child;
            if (elementPopupChildCanvas == null) return;
            var blocker = VisualTreeHelper.GetChild(elementPopupChildCanvas, 0) as Canvas;
            var popupChild = VisualTreeHelper.GetChild(elementPopupChildCanvas, 1) as FrameworkElement;
            if (blocker == null || popupChild == null) return;

            var width = Application.Current.Host.Content.ActualWidth;
            var height = Application.Current.Host.Content.ActualHeight;
            if (height < 50.0 || width < 50.0)
                return;

            var generalTransform = popup.TransformToVisual(null);
            var point1 = new Point(0.0, 0.0);
            var point2 = new Point(1.0, 0.0);
            var point3 = new Point(0.0, 1.0);
            var point4 = generalTransform.Transform(point1);
            var point5 = generalTransform.Transform(point2);
            var point6 = generalTransform.Transform(point3);

            var identity = Matrix.Identity;
            identity.M11 = point5.X - point4.X;
            identity.M12 = point5.Y - point4.Y;
            identity.M21 = point6.X - point4.X;
            identity.M22 = point6.Y - point4.Y;
            identity.OffsetX = point4.X;
            identity.OffsetY = point4.Y;
            var num = identity.M11 * identity.M22 - identity.M12 * identity.M21;
            var matrix = identity;
            identity.M11 = matrix.M22 / num;
            identity.M12 = -1.0 * matrix.M12 / num;
            identity.M21 = -1.0 * matrix.M21 / num;
            identity.M22 = matrix.M11 / num;
            identity.OffsetX = (matrix.OffsetY * matrix.M21 - matrix.OffsetX * matrix.M22) / num;
            identity.OffsetY = (matrix.OffsetX * matrix.M12 - matrix.OffsetY * matrix.M11) / num;

            blocker.Width = width;
            blocker.Height = height;
            blocker.RenderTransform = new MatrixTransform { Matrix = identity };
        }
    }
}
Advertisements

8 Responses to Silverlight: close Popup on click outside

  1. Pingback: Silverlight: close Popup on click outside

  2. Mike says:

    Thank you so much for sharing this!

  3. Omer says:

    Would you also show how to add PopupHelper by code ?

    • shemesh says:

      there is a XAML example on top.
      in c#: myPopup.SetValue(helpers:PopupHelper.StaysOpen, false);

      is that good?

      • Omer says:

        Here’s my code:
        RelativesPopup = new Popup();
        RelativesPopup.SetValue(PopupHelper.StaysOpenProperty, false);
        popuplistbox = new ListBox { ItemsSource = Relatives, HorizontalContentAlignment = HorizontalAlignment.Stretch };
        RelativesPopup.Child = popuplistbox;
        RelativesPopup.Loaded += new RoutedEventHandler(RelativesPopup_Loaded);
        RelativesPopup.IsOpen = true;

        but I get this error:
        System.ArgumentException was unhandled by user code
        Message=Value does not fall within the expected range.
        StackTrace:
        at MS.Internal.XcpImports.MethodEx(IntPtr ptr, String name, CValue[] cvData)
        at MS.Internal.XcpImports.MethodPack(IntPtr objectPtr, String methodName, Object[] rawData)
        at MS.Internal.XcpImports.UIElement_TransformToVisual(UIElement element, UIElement visual)
        at System.Windows.UIElement.TransformToVisual(UIElement visual)
        at CNG.Controls.MyPopupHelper.Arrange(Popup popup)
        at CNG.Controls.MyPopupHelper.c__DisplayClass3.b__2(Object , EventArgs )
        at System.Windows.FrameworkElement.OnLayoutUpdated(Object sender, EventArgs e)
        at MS.Internal.JoltHelper.RaiseEvent(IntPtr target, UInt32 eventId, IntPtr coreEventArgs, UInt32 eventArgsTypeIndex)
        InnerException:

        at:
        private static void Arrange(Popup popup)
        {
        …..
        var generalTransform = popup.TransformToVisual(null); <– this line
        …..

      • shemesh says:

        mmm… i see what you mean. i’ll take a look.

      • shemesh says:

        it seems the problem here is that coded Popup does not have a parent (weird).
        i suggest you add your Popup as child of some root element in your app, that should do the trick.

      • Omer says:

        Ok, thank you

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: