Как да отхвърлите изскачащ прозорец в Silverlight, когато щракнете извън контролата?

В моя Silverlight UI имам бутон, който при щракване изскача контрола с някои параметри за филтриране. Бих искал този контрол да се скрие, когато щракнете извън него. С други думи, той трябва да функционира по начин, подобен на комбинирана кутия, но не е комбинирана кутия (не избирате елемент в нея). Ето как се опитвам да уловя щракване извън контрола, за да го отхвърля:

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

За съжаление манипулаторът на събития за MouseLeftButtonDown никога не се задейства. Има ли добре установен начин за създаване на изскачащ контрол, който автоматично се отхвърля, когато щракнете извън него? Ако не, защо моят манипулатор MouseLeftButtonDown не се задейства?

Решение:

Мислех да публикувам цялото си решение, в случай че други го намерят за полезно. В моето визуално изображение от най-високо ниво декларирам "щит" за изскачащите прозорци по следния начин:

<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>

След това добавих метод за разширение за класа Popup, като този:

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

Методът на разширение се използва по следния начин:

private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
    FilterPopup.MakeAutoDismissing();
}

person Jacob    schedule 23.02.2010    source източник
comment
Вашето решение работи за мен. Ще бъде по-добре да създадете интерфейс с едно свойство PopupShield и да го използвате в метода MakeAutoDismissing. Вашата главна страница или друг потребителски контрол ще трябва да внедри свойството и да върне прозрачното платно. (Защо не публикувате вашето решение като отговор, така че хората да могат да гласуват за него?)   -  person kgiannakakis    schedule 14.01.2011


Отговори (7)


Зададохте ли фонов цвят на вашия RootVisual?

person Glenn Sandoval    schedule 24.02.2010
comment
Добра мисъл. Ще го пробвам утре. - person Jacob; 24.02.2010
comment
След като имах непрозрачен фон, започнах да получавам събития с мишката. - person Jacob; 24.02.2010
comment
Това също трябва да работи, ако използвате Transparent като цвят на фона. - person Ashley Davis; 28.10.2011

Един от начините е да поставите контрола върху прозрачно платно, което изпълва цялата повърхност на Silverlight. Когато се щракне върху платното, затворете платното и контролирайте. Важно е да се уверите, че четката за фон на платното е настроена на „Прозрачен“, ако искате да получавате събития с мишката.

Алтернативен метод, с който нямах успех, е използването на улавяне на мишката в Silverlight и откриване, когато мишката е щракната извън изскачащия прозорец.

person Doug Ferguson    schedule 23.02.2010
comment
Иска ми се да приемам и този отговор. Проблемът само със заснемането на събития в основния визуален елемент е, че контролите на наследниците, като бутони, блокират събитията с мишката надолу. Така че създадох платно от най-високо ниво, което ще се намира точно под изскачащите контроли. Когато показвам изскачащия прозорец, правя този щит видим и го правя свит, когато изскачащият прозорец бъде отхвърлен. - person Jacob; 24.02.2010
comment
Благодаря, че ме уведомихте, че отговорът ви помогна. - person Doug Ferguson; 24.02.2010

Току-що създадох нещо подобно и се надявам, че това може да бъде от полза. :)

В моята контрола PopUp имам граница, която има мрежа, която съдържа редица текстови полета. Нарекох границата „PopUpBorder“.

В конструктора на моя UserControl имам,

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

И изглежда, че работи според очакванията. Моля, уведомете ме, ако това не работи във вашия случай.

person Justin XL    schedule 11.04.2011
comment
Не се ли абонира за едно и също събитие всеки път, когато мишката напусне границата на изскачащия прозорец? Може би това води до ненужно използване на паметта? - person synergetic; 17.08.2011
comment
Наистина харесвам този отговор заради неговата простота. - person David Faivre; 25.05.2012

При първото щракване извикайте метода CaptureMouse() на контролата. След това извикайте ReleaseMouseCapture() при второто щракване.

person Alun Harford    schedule 23.02.2010
comment
Бих искал да направя нещо толкова просто, но не мога да го накарам да работи. Имате ли по-конкретни инструкции как да направите това или примерен код? - person Jacob; 24.02.2010

Предложеното решение използва специално екранно платно, за да блокира входа към други контроли. Това е добре, но се опитвам да внедря общ контрол и не мога да разчитам на съществуването на такова платно за щит. Свързването на събитието MouseLeftButtonDown на основния визуален елемент също не работи за мен, тъй като съществуващите бутони на моето платно все още биха задействали свое собствено събитие за кликване, а не MouseLeftButtonDown на основния визуален елемент. Намерих решението в тази ледена статия: Popup.StaysOpen в Silverlight от Kent Boograart. Основните методи за този въпрос са:

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

Все още не съм сигурен защо това решение работи за мен, а това не:

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

Изглежда, че методът FindHighestAncestor просто ще върне основния визуален елемент? Но тогава разликата трябва да е манипулаторът на събития? Предполагам, че основната разлика е последният параметър на метода AddHandler, който е верен в случая:

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

Документите на MSDN казват:

handledEventsToo

Тип: System.Boolean

true за регистриране на манипулатора, така че да бъде извикан дори когато маршрутизираното събитие е маркирано като обработено в данните за събитието; false за регистриране на манипулатора с условието по подразбиране, че няма да бъде извикан, ако маршрутизираното събитие вече е маркирано като обработено. По подразбиране е невярно. Не изисквайте редовно да обработите маршрутизирано събитие. За повече информация вижте Забележки.

person stefan.s    schedule 04.05.2011

По-проста алтернатива (макар и не точно това, което поискахте) би било да затворите изскачащия прозорец на MouseLeave. MouseLeave на самия обект Popup няма да работи, но MouseLeave на контейнера на най-високо ниво в рамките на Popup работи.

person beameup    schedule 22.09.2011

Публикувах решение за това в моя блог, можете да видите публикацията тук - Silverlight: затваряне на изскачащ прозорец при щракване отвън. както можете да видите, използването е много просто - това е прикачено свойство, което добавяте към вашия изскачащ прозорец. не е нужно да добавяте обвивки, не е нужно да се грижите дали имате или нямате някакъв фонов цвят... моят код ще се погрижи за всичко.

person shemesh    schedule 09.12.2013