Although this is already an old question, I had the same need too — I needed to be able to align a Popup
to its placement target. Not happy with the converter solution, I came up with my own solution, using attached dependency properties, which I'm sharing here with you and anyone with the same need.
NOTE: This solution doesn't cover how to show a Popup
on mouse
hover. It covers only the most tricky part — the alignment of the Popup
to its placement target. There are several ways to show a Popup
on mouse hover, like using Triggers or Bindings, both widely covered on StackOverflow.
Attached dependency properties solution
This solution uses a single static class that exposes some attached dependency properties. Using these properties, you can align a Popup
to its PlacementTarget
or its PlacementRectangle
, either horizontally or vertically. The alignment only occurs when the value of Popup
's Placement
property represents an edge (Left
, Top
, Right
or Bottom
).
Implementation
PopupProperties.cs
using System;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace MyProjectName.Ui
{
/// <summary>
/// Exposes attached dependency properties that provide
/// additional functionality for <see cref="Popup"/> controls.
/// </summary>
/// <seealso cref="Popup"/>
/// <seealso cref="DependencyProperty"/>
public static class PopupProperties
{
#region Properties
#region IsMonitoringState attached dependency property
/// <summary>
/// Attached <see cref="DependencyProperty"/>. This property
/// registers (<b>true</b>) or unregisters (<b>false</b>) a
/// <see cref="Popup"/> from the popup monitoring mechanism
/// used internally by <see cref="PopupProperties"/> to keep
/// the <see cref="Popup"/> in synchrony with the
/// <see cref="PopupProperties"/>' attached properties. A
/// <see cref="Popup"/> will be automatically unregistered from
/// this mechanism after it is unloaded.
/// </summary>
/// <seealso cref="Popup"/>
private static readonly DependencyProperty IsMonitoringStateProperty
= DependencyProperty.RegisterAttached("IsMonitoringState",
typeof(bool), typeof(PopupProperties),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(IsMonitoringStatePropertyChanged)));
private static void IsMonitoringStatePropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
bool value = (bool)e.NewValue;
if (value)
{
// Attach popup.
popup.Opened += Popup_Opened;
popup.Unloaded += Popup_Unloaded;
// Update popup.
UpdateLocation(popup);
}
else
{
// Detach popup.
popup.Opened -= Popup_Opened;
popup.Unloaded -= Popup_Unloaded;
}
}
private static bool GetIsMonitoringState(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (bool)popup.GetValue(IsMonitoringStateProperty);
}
private static void SetIsMonitoringState(Popup popup, bool isMonitoringState)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(IsMonitoringStateProperty, isMonitoringState);
}
#endregion
#region HorizontalPlacementAlignment attached dependency property
public static readonly DependencyProperty HorizontalPlacementAlignmentProperty
= DependencyProperty.RegisterAttached("HorizontalPlacementAlignment",
typeof(AlignmentX), typeof(PopupProperties),
new FrameworkPropertyMetadata(AlignmentX.Left,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(HorizontalPlacementAlignmentPropertyChanged)),
new ValidateValueCallback(HorizontalPlacementAlignmentPropertyValidate));
private static void HorizontalPlacementAlignmentPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool HorizontalPlacementAlignmentPropertyValidate(object obj)
{
return Enum.IsDefined(typeof(AlignmentX), obj);
}
public static AlignmentX GetHorizontalPlacementAlignment(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (AlignmentX)popup.GetValue(HorizontalPlacementAlignmentProperty);
}
public static void SetHorizontalPlacementAlignment(Popup popup, AlignmentX alignment)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(HorizontalPlacementAlignmentProperty, alignment);
}
#endregion
#region VerticalPlacementAlignment attached dependency property
public static readonly DependencyProperty VerticalPlacementAlignmentProperty
= DependencyProperty.RegisterAttached("VerticalPlacementAlignment",
typeof(AlignmentY), typeof(PopupProperties),
new FrameworkPropertyMetadata(AlignmentY.Top,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(VerticalPlacementAlignmentPropertyChanged)),
new ValidateValueCallback(VerticalPlacementAlignmentPropertyValidate));
private static void VerticalPlacementAlignmentPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool VerticalPlacementAlignmentPropertyValidate(object obj)
{
return Enum.IsDefined(typeof(AlignmentY), obj);
}
public static AlignmentY GetVerticalPlacementAlignment(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (AlignmentY)popup.GetValue(VerticalPlacementAlignmentProperty);
}
public static void SetVerticalPlacementAlignment(Popup popup, AlignmentY alignment)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
popup.SetValue(VerticalPlacementAlignmentProperty, alignment);
}
#endregion
#region HorizontalOffset attached dependency property
public static readonly DependencyProperty HorizontalOffsetProperty
= DependencyProperty.RegisterAttached("HorizontalOffset",
typeof(double), typeof(PopupProperties),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(HorizontalOffsetPropertyChanged)),
new ValidateValueCallback(HorizontalOffsetPropertyValidate));
private static void HorizontalOffsetPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool HorizontalOffsetPropertyValidate(object obj)
{
double value = (double)obj;
return !double.IsNaN(value) && !double.IsInfinity(value);
}
public static double GetHorizontalOffset(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (double)popup.GetValue(HorizontalOffsetProperty);
}
public static void SetHorizontalOffset(Popup popup, double offset)
{
if (popup is null)
throw new ArgumentNullException(nameof(offset));
popup.SetValue(HorizontalOffsetProperty, offset);
}
#endregion
#region VerticalOffset attached dependency property
public static readonly DependencyProperty VerticalOffsetProperty
= DependencyProperty.RegisterAttached("VerticalOffset",
typeof(double), typeof(PopupProperties),
new FrameworkPropertyMetadata(0d,
FrameworkPropertyMetadataOptions.None,
new PropertyChangedCallback(VerticalOffsetPropertyChanged)),
new ValidateValueCallback(VerticalOffsetPropertyValidate));
private static void VerticalOffsetPropertyChanged(
DependencyObject dObject, DependencyPropertyChangedEventArgs e)
{
Popup popup = (Popup)dObject;
SetIsMonitoringState(popup, true);
UpdateLocation(popup);
}
private static bool VerticalOffsetPropertyValidate(object obj)
{
double value = (double)obj;
return !double.IsNaN(value) && !double.IsInfinity(value);
}
public static double GetVerticalOffset(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
return (double)popup.GetValue(VerticalOffsetProperty);
}
public static void SetVerticalOffset(Popup popup, double offset)
{
if (popup is null)
throw new ArgumentNullException(nameof(offset));
popup.SetValue(VerticalOffsetProperty, offset);
}
#endregion
#endregion Properties
#region Methods
private static void OnMonitorState(Popup popup)
{
if (popup is null)
throw new ArgumentNullException(nameof(popup));
UpdateLocation(popup);
}
private static void UpdateLocation(Popup popup)
{
// Validate parameters.
if (popup is null)
throw new ArgumentNullException(nameof(popup));
// If the popup is not open, we don't need to update its position yet.
if (!popup.IsOpen)
return;
// Setup initial variables.
double offsetX = 0d;
double offsetY = 0d;
PlacementMode placement = popup.Placement;
UIElement placementTarget = popup.PlacementTarget;
Rect placementRect = popup.PlacementRectangle;
// If the popup placement mode is an edge of the placement target,
// determine the alignment offset.
if (placement == PlacementMode.Top || placement == PlacementMode.Bottom
|| placement == PlacementMode.Left || placement == PlacementMode.Right)
{
// Try to get the popup size. If its size is empty, use the size
// of its child, if any child exists.
Size popupSize = GetElementSize(popup);
UIElement child = popup.Child;
if ((popupSize.IsEmpty || popupSize.Width <= 0d || popupSize.Height <= 0d)
&& child != null)
{
popupSize = GetElementSize(child);
}
// Get the placement rectangle size. If it's empty, get the
// placement target's size, if a target is set.
Size targetSize;
if (placementRect.Width > 0d && placementRect.Height > 0d)
targetSize = placementRect.Size;
else if (placementTarget != null)
targetSize = GetElementSize(placementTarget);
else
targetSize = Size.Empty;
// If we have a valid popup size and a valid target size, determine
// the offset needed to align the popup to the target rectangle.
if (!popupSize.IsEmpty && popupSize.Width > 0d && popupSize.Height > 0d
&& !targetSize.IsEmpty && targetSize.Width > 0d && targetSize.Height > 0d)
{
switch (placement)
{
// Horizontal alignment offset.
case PlacementMode.Top:
case PlacementMode.Bottom:
switch (GetHorizontalPlacementAlignment(popup))
{
case AlignmentX.Left:
offsetX = 0d;
break;
case AlignmentX.Center:
offsetX = -(popupSize.Width - targetSize.Width) / 2d;
break;
case AlignmentX.Right:
offsetX = -(popupSize.Width - targetSize.Width);
break;
default:
break;
}
break;
// Vertical alignment offset.
case PlacementMode.Left:
case PlacementMode.Right:
switch (GetVerticalPlacementAlignment(popup))
{
case AlignmentY.Top:
offsetY = 0d;
break;
case AlignmentY.Center:
offsetY = -(popupSize.Height - targetSize.Height) / 2d;
break;
case AlignmentY.Bottom:
offsetY = -(popupSize.Height - targetSize.Height);
break;
default:
break;
}
break;
default:
break;
}
}
}
// Add the developer specified offsets to the offsets we've calculated.
offsetX += GetHorizontalOffset(popup);
offsetY += GetVerticalOffset(popup);
// Apply the final computed offsets to the popup.
popup.SetCurrentValue(Popup.HorizontalOffsetProperty, offsetX);
popup.SetCurrentValue(Popup.VerticalOffsetProperty, offsetY);
}
private static Size GetElementSize(UIElement element)
{
if (element is null)
return new Size(0d, 0d);
else if (element is FrameworkElement frameworkElement)
return new Size(frameworkElement.ActualWidth, frameworkElement.ActualHeight);
else
return element.RenderSize;
}
#endregion Methods
#region Event handlers
private static void Popup_Unloaded(object sender, RoutedEventArgs e)
{
if (sender is Popup popup)
{
// Stop monitoring the popup state, because it was unloaded.
SetIsMonitoringState(popup, false);
}
}
private static void Popup_Opened(object sender, EventArgs e)
{
if (sender is Popup popup)
{
OnMonitorState(popup);
}
}
#endregion Event handlers
}
}
How it works
The code above creates a static class that exposes 4 attached dependency properties for Popup
controls. Namely, they are HorizontalPlacementAlignment
, VerticalPlacementAlignment
, HorizontalOffset
and VerticalOffset
.
HorizontalPlacementAlignment
and VerticalPlacementAlignment
attached dependency properties allow you to align a popup relative to its PlacementTarget
or PlacementRectangle
. To achieve this, the mechanism uses Popup.HorizontalOffset
and Popup.VerticalOffset
properties to position the Popup
.
Because the mechanism uses Popup.HorizontalOffset
and Popup.VerticalOffset
properties to work, this class provides its own HorizontalOffset
and VerticalOffset
properties (attached dependency properties). You can use them to adjust the position of the Popup
in addition to its alignment.
The mechanism automatically updates the Popup
position every time the Popup
is opened. However, its position will not be automatically updated when the popup size changes or when its placement target or placement rectangle sizes change. Nonetheless, with a bit more work put into it, that functionality could be easily implemented.
Usage example
You would use the attached properties on a Popup
like on the example below. In this example, we have a simple Button
and a Popup
. The Popup
is displayed aligned to the bottom of the Button
and horizontally centered to the Button
's center.
<Button x:Name="MyTargetElement">My Button</Button>
<Popup xmlns:ui="clr-namespace:MyProjectName.Ui"
PlacementTarget="{Binding ElementName=MyTargetElement}"
Placement="Bottom"
ui:PopupProperties.HorizontalPlacementAlignment="Center"
ui:PopupProperties.VerticalOffset="2">
</Popup>
By adding ui:PopupProperties.HorizontalPlacementAlignment="Center"
and ui:PopupProperties.VerticalOffset="2"
to a Popup
, it will be aligned to the horizontal center of its placement target and have 2 WPF units of vertical offset as well.
Note the use of xmlns:ui="clr-namespace:MyProjectName.Ui"
on the Popup
. This attribute only imports the types from MyProjectName.Ui
namespace on your project and makes them available through the usage of the ui:
prefix on your XAML attributes. In the example, this attribute is set on the Popup
for simplicity, but you would usually set it on your Window
or ResourceDictionary
where you're using these custom attached dependency properties.
Conclusion
The idea behind using attached dependency properties to achieve this functionality is to make its usage in XAML as simple as possible. For a simple one-time need, using converters may be simpler to implement. However, using attached dependency properties, in this case, may provide a more dynamic and usage-friendly approach.