WPF Tips n' Tricks – Reusing the content of a Popup control

That one is far from obvious, and is what I'd classify as a bug. As soon as Microsoft fix the Connect web-site to let us report RTM bugs, I'll more than happily fill one up.

In WPF, controls can only have one parent at a time. Because rendering is done top down and every control has a sequence of Measure/Arrange calls to define the layout of the windows, it makes perfect sense.

It also makes perfect sense to be able to remove a control from somewhere (let's say a Button from a Panel) and re-add it somewhere else. Quick and dirty example, a button switching between two panels.

<Window x:Class="PopupExample.Window1"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:my="clr-namespace:IsDirtyExample"

    Title="IsDirtyExample" SizeToContent="WidthAndHeight">

    <DockPanel LastChildFill="False">

        <DockPanel DockPanel.Dock="Top" Name="FirstGrid" Background="Orange" Width="250" Height="200">

            <Button Name="OneButtonToRuleThemAll">Ding</Button>

        </DockPanel>

        <DockPanel DockPanel.Dock="Top" Name="SecondGrid" Background="OrangeRed" Width="250" Height="200"></DockPanel>

        <Button DockPanel.Dock="Top" Click="HandleMoveClick">Move the Ding!</Button>

    </DockPanel>

</Window>

namespace PopupExample

{

    public partial class Window1 : System.Windows.Window

    {

        public Window1()

        {

            InitializeComponent();

        }

        public void HandleMoveClick(object source, RoutedEventArgs e)

        {

            FirstGrid.Children.Remove(OneButtonToRuleThemAll);

            SecondGrid.Children.Add(OneButtonToRuleThemAll);

        }

    }

}

When you create a Popup control however, it only has a Child property. So you'd assume you would just change the Child property to a new control, and that new control would then be showing next time you open the Popup, and the old control wouldn't be in any visual tree anymore. Well, it doesn't always happen that way at all.

If your Popup is visible, then you'll see the change. If the Popup is not visible anymore, and you set it's Child property to null, and reattach your control to a new Popup, you'll be greeted by a very useful message.

Must disconnect specified child from current parent Visual before attaching to new parent Visual.

But wait, the control is not assigned to the Child property anymore? It is and it is not. Whenever the popup gets created, it keeps a separate object deep within called a PopupRoot. That one gets created once and has as a child the control pointed by the Popup's Child property. But whenever a popup is closed (IsOpened=false), changes to its Child property will not impact the PopupRoot that still has a reference to the previous value. Hence why when you try to add your control somewhere else, you have an exception

The solution? Put as the first child of your popup a neutral panel (Grid for example), and move your controls around only as a child of that grid. Full code sample shown below.

namespace PopupExample

{

    public partial class Window1 : System.Windows.Window

    {

        public Window1()

        {

            InitializeComponent();

        }

        private void button_Click(object sender, RoutedEventArgs e)

        {

            if (popupButton == null)

            {

                popupButton = new Button();

                popupButton.Content = "This button on a popup";

            }

 

            Popup popup = new Popup();

            Grid grid = new Grid();

            grid.Children.Add(popupButton);

            popup.Child = grid;

            popup.PlacementTarget = this;

            popup.PlacementRectangle = new Rect(0, 0, this.ActualWidth, this.ActualHeight - 18);

            popup.Placement = PlacementMode.Bottom;

            popup.StaysOpen = false;

            popup.IsOpen = true// Second call used to trigger an exception

            popup.Closed += new EventHandler(popup_Closed);

 

        }

 

        private void popup_Closed(object sender, EventArgs e)

        {

            Popup popup = (Popup)sender;

            ((Grid)popup.Child).Children.Clear(); // will finally clear the control from being in a visual tree

            popup.Closed -= new EventHandler(popup_Closed);

 

        }

    }

}

 

Ads

Comment