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.
Comments
Anonymous
August 14, 2007
This is great information to know, but its very complicated. This kind of stuff should of been integrated better into wpf. Maybe I'm wrong, but it seems like there are a lot of "advanced"/hackish type things you have to do with wpf to get simple controls. Maybe this was a poor/incomplete api and implementation design. I still enjoy wpf programming, thanks for the popup I hope to use it in an application today.Anonymous
March 01, 2008
Nice control. It lacks one feature that I'm not sure how to add. When the form is moved, it either needs to close or move with the form. You handle this in you sample by toggling it off and on in the move event of the window. But it would be much nicer if that was handled in the control itself. Stays open almost works, but it consumes the mouse click for the window move, forcing you to click twice. And the popup does not seem to want to reopen once it has been closed by a move. Any Ideas?Anonymous
October 31, 2008
http://blogs.msdn.com/wpfsdk/archive/2007/04/27/popup-your-control.aspxAnonymous
January 11, 2009
In continuation to my first post - WPF Popups and ToolTip behavior - A Journey I decided to approachAnonymous
January 15, 2009
In continuation to my first post - WPF Popups and ToolTip behavior - A Journey I decided to approach