Back to list
christian289

implementing-wpf-adorners

by christian289

ClaudeCode와 함께하는 .NET 개발 튜토리얼

1🍴 0📅 Jan 25, 2026

SKILL.md


name: implementing-wpf-adorners description: Implements WPF Adorner decoration layers with AdornerLayer, AdornerDecorator, and custom Adorner patterns. Use when building drag handles, validation indicators, watermarks, selection visuals, or resize grips.

WPF Adorner Patterns

Adorner is a mechanism for overlaying decorative visual elements on top of UIElements.

1. Adorner Concept

1.1 Characteristics

  • AdornerLayer: Separate rendering layer that holds Adorners
  • Z-Order: Always renders above the adorned element
  • Layout Independent: No effect on target element's layout
  • Event Support: Can receive mouse/keyboard events

1.2 Usage Scenarios

ScenarioDescription
Validation DisplayInput field error display
Drag HandlesElement move/resize handles
WatermarkHint text for empty TextBox
Selection DisplayHighlight selected elements
Tooltip/BadgeAdditional info display on elements
Drag and DropPreview during drag

2. Basic Adorner Implementation

2.1 Simple Adorner

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);
    }
}

2.2 Applying Adorner

// Get AdornerLayer
var adornerLayer = AdornerLayer.GetAdornerLayer(targetElement);

if (adornerLayer is not null)
{
    // Add Adorner
    var adorner = new BorderAdorner(targetElement);
    adornerLayer.Add(adorner);
}

2.3 Removing 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);
    }
}

3. AdornerDecorator

3.1 Default Location

<!-- Window default template includes AdornerDecorator -->
<Window>
    <!-- AdornerDecorator is automatically included -->
    <Grid>
        <TextBox x:Name="MyTextBox"/>
    </Grid>
</Window>

3.2 Explicit AdornerDecorator

<!-- 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>

4. Practical Adorner Examples

4.1 Watermark Adorner

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;
    }
}

4.2 Watermark Attached Property

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);
            }
        }
    }
}

4.3 Using Watermark in XAML

<TextBox local:Watermark.Text="Enter email address"/>

5. Advanced Adorner Patterns

For advanced patterns, see references/advanced-adorners.md:

  • Resize Handle Adorner: Element resizing with corner/edge handles
  • Validation Error Adorner: Display validation errors with icons
  • Drag Preview Adorner: Visual feedback during drag operations
  • Adorner Management Service: Lifecycle management utilities

6. Checklist

  • Verify AdornerLayer exists before adding Adorner
  • Set IsHitTestVisible = false for decoration-only Adorners
  • Correctly implement VisualChildrenCount and GetVisualChild
  • Arrange children using MeasureOverride and ArrangeOverride
  • Remove unnecessary Adorners (prevent memory leaks)
  • Explicitly add AdornerDecorator in Popup, etc.

7. References

Score

Total Score

65/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

1ヶ月以内に更新

+10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

+5

Reviews

💬

Reviews coming soon