Popup your control

You want a custom control to behave like a Popup and appear over your application and other windows? So you should inherit from Popup, right? Not necessarily.

Another approach is to create your custom control, add a Popup to it, and use the popup service. In fact, this is exactly what ContextMenu and ToolTip do. Popup, ContextMenu, and ToolTip all have the following properties defined, but the ContextMenu and ToolTip actually leave it up to the Popup to implement them.

  • Placement
  • PlacementTarget
  • PlacementRectangle
  • HorizontalOffset
  • VerticalOffset
  • IsOpen

The ContextMenu and ToolTip call the Popup.CreateRootPopup which sets the Popup.Child property to the control and binds the six properties of the Popup to the control.

Doing the same for your custom control is easy enough. I created a control that accepts text and is editable. I could have added a TextBox to a control that inherits from Popup, but then I either hide the functionality of the TextBox from application developers, or have to expose TextBox through a property. Furthermore, Popup does not inherit from Control, so I wouldn’t have the ability to template the control. Instead, I inherited from TextBox. Below is the beginning of my control, EditablePopup.

    public partial class EditablePopup : System.Windows.Controls.TextBox

    {

        Popup _parentPopup;

        public EditablePopup()

            : base()

        {

        }

        static EditablePopup()

        {

            //This OverrideMetadata call tells the system that this element

  //wants to provide a style that is different than its base class.

            //This style is defined in themes\generic.xaml

            DefaultStyleKeyProperty.OverrideMetadata(typeof(EditablePopup),

                new FrameworkPropertyMetadata(typeof(EditablePopup)));

        }

    }

I don’t want my EditablePopup to look like a normal TextBox, so I define my Template to contain a border and a decorator.

  <Style TargetType="{x:Type local:EditablePopup}">

    <Setter Property="Margin" Value="1"/>

    <Setter Property="Template">

      <Setter.Value>

        <!--Create a TextBox that looks "flat".

              The control template for a TextBox or RichTextBox must

              include an element tagged as the content host. An element is

              tagged as the content host element when it has the special name

              PART_ContentHost. The content host element must be a ScrollViewer,

              or an element that derives from Decorator.

              -->

        <ControlTemplate TargetType="{x:Type local:EditablePopup}">

          <Border Background="{TemplateBinding Background}"

                  BorderBrush="{TemplateBinding BorderBrush}"

                  BorderThickness="{TemplateBinding BorderThickness}">

            <Decorator x:Name="PART_ContentHost"/>

          </Border>

        </ControlTemplate>

      </Setter.Value>

    </Setter>

  </Style>

Now I need to add the six properties I mentioned above to my control. Because Popup has these properties, creating the first five is pretty straightforward. You just need to call AddOwner on each DependencyProperty of the Popup and provide CLR Accessors. For more information about creating dependency properties, see Custom Dependency Properties in the SDK. Here are the declarations for Placement, PlacementTarget, PlacementRectangle, HorizontalOffset, and VerticalOffset:

        //Placement

        public static readonly DependencyProperty PlacementProperty =

                    Popup.PlacementProperty.AddOwner(typeof(EditablePopup));

        public PlacementMode Placement

        {

            get { return (PlacementMode)GetValue(PlacementProperty); }

            set { SetValue(PlacementProperty, value); }

        }

        //PlacementTarget

        public static readonly DependencyProperty PlacementTargetProperty =

           Popup.PlacementTargetProperty.AddOwner(typeof(EditablePopup));

        public UIElement PlacementTarget

        {

            get { return (UIElement)GetValue(PlacementTargetProperty); }

            set { SetValue(PlacementTargetProperty, value); }

        }

        //PlacementRectangle

        public static readonly DependencyProperty PlacementRectangleProperty =

                    Popup.PlacementRectangleProperty.AddOwner(typeof(EditablePopup));

        public Rect PlacementRectangle

        {

            get { return (Rect)GetValue(PlacementRectangleProperty); }

            set { SetValue(PlacementRectangleProperty, value); }

        }

        //HorizontalOffset

        public static readonly DependencyProperty HorizontalOffsetProperty =

            Popup.HorizontalOffsetProperty.AddOwner(typeof(EditablePopup));

        public double HorizontalOffset

        {

            get { return (double)GetValue(HorizontalOffsetProperty); }

            set { SetValue(HorizontalOffsetProperty, value); }

        }

        //VerticalOffset

        public static readonly DependencyProperty VerticalOffsetProperty =

                Popup.VerticalOffsetProperty.AddOwner(typeof(EditablePopup));

        public double VerticalOffset

        {

            get { return (double)GetValue(VerticalOffsetProperty); }

            set { SetValue(VerticalOffsetProperty, value); }

        }

Adding the IsOpen property is a little more involved, but isn’t that complex. When you define the DependenceProperty for IsOpen, you also need to provide a callback function when the value for IsOpen changes. When IsOpen is true, create the Popup and call Popup.CreateRootPopup.

        public static readonly DependencyProperty IsOpenProperty =

                Popup.IsOpenProperty.AddOwner(

               typeof(EditablePopup),

                        new FrameworkPropertyMetadata(

                                false,

                                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,

                                new PropertyChangedCallback(OnIsOpenChanged)));

        public bool IsOpen

        {

            get { return (bool)GetValue(IsOpenProperty); }

            set { SetValue(IsOpenProperty, value); }

        }

        private static void OnIsOpenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)

        {

            EditablePopup ctrl = (EditablePopup)d;

            if ((bool)e.NewValue)

            {

                if (ctrl._parentPopup == null)

                {

                    ctrl.HookupParentPopup();

                }

            }

        }

        private void HookupParentPopup()

        {

            _parentPopup = new Popup();

            _parentPopup.AllowsTransparency = true;

            Popup.CreateRootPopup(_parentPopup, this);

   }

Now that we’ve made the control, there are a few things to keep in mind before you use the control. First, set PlacementTarget before you call CreateRootPopup. If you call CreateRootPopup first, the PlacementTarget is ignored. Essentially this means you need to set PlacementTarget before setting IsOpen to true, just like you need to for a Popup.

Second, CreateRootPopup sets the Child property of the Popup to your custom control. As a result, your custom control cannot have a logical or visual parent and the following doesn’t work:

    <WrapPanel>

      <src:EditablePopup IsOpen="True">Hello</src:EditablePopup>

    </WrapPanel>

Instead, you need to create your control as a resource, or create it in code. In my project, I created the EditablePopup controls in code.

budapestDesc = new EditablePopup();

budapestDesc.Text = "Title:\tBudapest, Hungary\n" +

"Description:\n\tHungarian Parliament Building and the Danube River";

budapestDesc.PlacementTarget = budapestPic;

Now I can position the EditablePopup, just like I would a Popup, ContextMenu, or ToolTip, by setting the Placement, PlacementRectangle, HorizontalOffset, and VerticalOffset properties.

I’ve attached the EditablePopup zip file so you can download and experiment with it.

 

EditablePopup.zip