From wpf-dev-pack
Implements WPF Adorner decoration layers with AdornerLayer, AdornerDecorator, and custom patterns for drag handles, validation indicators, watermarks, selection visuals, and resize grips.
npx claudepluginhub christian289/dotnet-with-claudecode --plugin wpf-dev-packThis skill uses the workspace's default tool permissions.
Adorner is a mechanism for overlaying decorative visual elements on top of UIElements.
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Adorner is a mechanism for overlaying decorative visual elements on top of UIElements.
| Scenario | Description |
|---|---|
| Validation Display | Input field error display |
| Drag Handles | Element move/resize handles |
| Watermark | Hint text for empty TextBox |
| Selection Display | Highlight selected elements |
| Tooltip/Badge | Additional info display on elements |
| Drag and Drop | Preview during drag |
namespace MyApp.Adorners;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
/// <summary>
/// Adorner that draws border around element
/// </summary>
public sealed class BorderAdorner : Adorner
{
private readonly Pen _borderPen;
public BorderAdorner(UIElement adornedElement) : base(adornedElement)
{
_borderPen = new Pen(Brushes.Red, 2)
{
DashStyle = DashStyles.Dash
};
_borderPen.Freeze();
// Disable mouse events (decoration only)
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
var rect = new Rect(AdornedElement.RenderSize);
// Draw border
drawingContext.DrawRectangle(null, _borderPen, rect);
}
}
// Get AdornerLayer
var adornerLayer = AdornerLayer.GetAdornerLayer(targetElement);
if (adornerLayer is not null)
{
// Add Adorner
var adorner = new BorderAdorner(targetElement);
adornerLayer.Add(adorner);
}
// Remove all Adorners from specific element
var adornerLayer = AdornerLayer.GetAdornerLayer(targetElement);
var adorners = adornerLayer?.GetAdorners(targetElement);
if (adorners is not null)
{
foreach (var adorner in adorners)
{
adornerLayer!.Remove(adorner);
}
}
<!-- Window default template includes AdornerDecorator -->
<Window>
<!-- AdornerDecorator is automatically included -->
<Grid>
<TextBox x:Name="MyTextBox"/>
</Grid>
</Window>
<!-- Explicit AdornerDecorator in ControlTemplate -->
<ControlTemplate TargetType="{x:Type ContentControl}">
<AdornerDecorator>
<ContentPresenter/>
</AdornerDecorator>
</ControlTemplate>
<!-- In Popup or special containers -->
<Popup>
<AdornerDecorator>
<Border>
<StackPanel>
<TextBox/>
<Button Content="OK"/>
</StackPanel>
</Border>
</AdornerDecorator>
</Popup>
namespace MyApp.Adorners;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
/// <summary>
/// Display watermark (hint text) on TextBox
/// </summary>
public sealed class WatermarkAdorner : Adorner
{
private readonly TextBlock _watermarkText;
public WatermarkAdorner(UIElement adornedElement, string watermark)
: base(adornedElement)
{
_watermarkText = new TextBlock
{
Text = watermark,
Foreground = Brushes.Gray,
FontStyle = FontStyles.Italic,
Margin = new Thickness(4, 2, 0, 0),
IsHitTestVisible = false
};
AddVisualChild(_watermarkText);
IsHitTestVisible = false;
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => _watermarkText;
protected override Size MeasureOverride(Size constraint)
{
_watermarkText.Measure(constraint);
return _watermarkText.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_watermarkText.Arrange(new Rect(finalSize));
return finalSize;
}
}
namespace MyApp.Behaviors;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using MyApp.Adorners;
public static class Watermark
{
public static readonly DependencyProperty TextProperty =
DependencyProperty.RegisterAttached(
"Text",
typeof(string),
typeof(Watermark),
new PropertyMetadata(null, OnTextChanged));
public static string GetText(DependencyObject obj) =>
(string)obj.GetValue(TextProperty);
public static void SetText(DependencyObject obj, string value) =>
obj.SetValue(TextProperty, value);
private static void OnTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not TextBox textBox)
{
return;
}
textBox.Loaded -= OnTextBoxLoaded;
textBox.Loaded += OnTextBoxLoaded;
textBox.TextChanged -= OnTextBoxTextChanged;
textBox.TextChanged += OnTextBoxTextChanged;
}
private static void OnTextBoxLoaded(object sender, RoutedEventArgs e)
{
if (sender is TextBox textBox)
{
UpdateWatermark(textBox);
}
}
private static void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
{
if (sender is TextBox textBox)
{
UpdateWatermark(textBox);
}
}
private static void UpdateWatermark(TextBox textBox)
{
var adornerLayer = AdornerLayer.GetAdornerLayer(textBox);
if (adornerLayer is null)
{
return;
}
// Remove existing watermark
RemoveWatermark(textBox, adornerLayer);
// Add watermark if text is empty
if (string.IsNullOrEmpty(textBox.Text))
{
var watermark = GetText(textBox);
if (!string.IsNullOrEmpty(watermark))
{
adornerLayer.Add(new WatermarkAdorner(textBox, watermark));
}
}
}
private static void RemoveWatermark(TextBox textBox, AdornerLayer adornerLayer)
{
var adorners = adornerLayer.GetAdorners(textBox);
if (adorners is null)
{
return;
}
foreach (var adorner in adorners)
{
if (adorner is WatermarkAdorner)
{
adornerLayer.Remove(adorner);
}
}
}
}
<TextBox local:Watermark.Text="Enter email address"/>
For advanced patterns, see references/advanced-adorners.md:
IsHitTestVisible = false for decoration-only Adorners