WPF Adorners

An Adorner is a way of extending controls, generally by adding visual cues (or extra functionality). For example a visual cue might be adding drag and drop visuals when users try to drag and drop data onto a control or maybe we want to add resizing handles to controls within some form of UI design surface.

We can implement much of the same sort of visual cues and/or functionality by override the ControlTemplate or subclassing a control, but the Adorner offers a way to associate these cues/functionality to any type of control.

One example Adorner you may have seen is the validation UI whereby we see a red border around a control when validation fails and ofcourse we can extend this UI further if we wish.

Let’s take a look at how we might implement such an Adorner and use it.

Adorner

We’re going to start with a very simple Adorner which simply displays a little red triangle on a control – similar to the way Excel would when a note is attached to a cell.

Firstly, we need to create an Adorner. We subclass the Adorner class and supply a constructor which takes a UIElement, which is the element to be adorned.

public class NoteAdorner : Adorner
{
   public NoteAdorner(UIElement adornedElement) : 
      base(adornedElement)
   {
   }
}

This Adorner isn’t of much use. There’s nothing to see. So let’s write some code so that the Adorner displays our little red triangle over the AdornedElement. Remember that this is a layer on top of the AdorndedElement, in this case we’ll not do anything directly to the AdornedElement directly such as you might when handling drag and drop or the likes.

So add the following to the above class

protected override void OnRender(DrawingContext drawingContext)
{
   var adornedElementRect = new Rect(AdornedElement.RenderSize);

   const int SIZE = 10;

   var right = adornedElementRect.Right;
   var left = right - SIZE;
   var top = adornedElementRect.Top;
   var bottom = adornedElementRect.Bottom - SIZE;

   var segments = new[]
   {
      new LineSegment(new Point(left, top), true), 
      new LineSegment(new Point(right, bottom), true),
      new LineSegment(new Point(right, top), true)
   };

   var figure = new PathFigure(new Point(left, top), segments, true);
   var geometry = new PathGeometry(new[] { figure });
   drawingContext.DrawGeometry(Brushes.Red, null, geometry);
}

To apply an Adorner we need to write some code.

If you create a simple WPF application with a TextBox within it, and assuming the TextBox is named NoteTextBox, then we might write in the code behind of the Window class hosting the TextBox control

public MainWindow()
{
   InitializeComponent();

   Loaded += (sender, args) =>
   {
      var adorner = AdornerLayer.GetAdornerLayer(NoteTextBox);
      adorner.Add(new NoteAdorner(NoteTextBox));
   };
}

Note: It’s important to note that if you try and call the GetAdornerLayer on the TextBox before the controls are loaded you will get a null returned and thus cannot apply the adorner. So we need to apply it after the controls are loaded

In the above code we get the AdornerLayer for a control, in this case the TextBox named NoteTextBox, we then add the adorner to it.

If you run the above code you’ll get the triangle displayed over the top of the TextBox control.

One thing you may notice is that, if click on and the Adorned control it will not get focus. The Adorner ofcourse sits atop the AdornedControl and by default will not give focus to the underlying control. Basically we’ve added this control as an overlay to the AdornedElement, so ofcourse its higher in the z-order.

To change this behaviour we can alter the constructor of the NoteAdorner to the following

public NoteAdorner(UIElement adornedElement) : 
   base(adornedElement)
{
   IsHitTestVisible = false;
}

With the IsHitTestVisible set to false you can click on the Adorner UI and the AdornedElement will take focus.

As you’ll have noticed, the way to attach an Adorner is using code, this doesn’t fit so well with the idea of using XAML for such things, i.e. for a designer to handle such adornments. There are several examples on the web of ways to make adorners XAML friendly.

I’m going to implement a very simple attached property class to handle this. Which I’ve listed below

public class AttachedAdorner
{
   public static readonly DependencyProperty AdornerProperty = 
      DependencyProperty.RegisterAttached(
         "Adorner", typeof (Type), typeof (AttachedAdorner), 
         new FrameworkPropertyMetadata(default(Type), PropertyChangedCallback));

   private static void PropertyChangedCallback(
      DependencyObject dependencyObject, 
      DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
   {
      var frameworkElement = dependencyObject as FrameworkElement;
      if (frameworkElement != null)
      {
         frameworkElement.Loaded += Loaded;
      }
   }

   private static void Loaded(object sender, RoutedEventArgs e)
   {
      var frameworkElement = sender as FrameworkElement;
      if (frameworkElement != null)
      {
         var adorner = Activator.CreateInstance(
            GetAdorner(frameworkElement), 
            frameworkElement) as Adorner;
         if(adorner != null)
         {
            var adornerLayer = AdornerLayer.GetAdornerLayer(frameworkElement);
            adornerLayer.Add(adorner);
         }
      }
   }

   public static void SetAdorner(DependencyObject element, Type value)
   {
      element.SetValue(AdornerProperty, value);
   }

   public static Type GetAdorner(DependencyObject element)
   {
      return (Type)element.GetValue(AdornerProperty);
   }
}

And now let’s see how this might be used

<TextBox Text="Hello World" local:AttachedAdorner.Adorner="{x:Type local:NoteAdorner}" />

AdornerLayer and the AdornerDecorator

As discussed (above) – we need to get the get the adorner layer for example AdornerLayer.GetAdornerLayer(NoteTextBox) to add our adorner to. When GetAdornerLayer is called on a Visual object, the code traverses up the visual tree (starting with the supplied UIElement) looking for an Adorner layer. Then returns the first one it finds. Hence if you write your own custom control you will need to explcitly add a place holder on the ControlTemplate denoting where any adorner should be displayed – in other words where you want the adorner layer is.

So for writing our own custom control we need to put in a place holder, the place holder is an AdornerDecorator object

<AdornerDecorator>
   <ContentControl x:Name="PART_Input" />
</AdornerDecorator>

This can only contain a single child element, although ofcourse this element can contain multiple elements that can be adorned. The AdornerDecorator specifies the position of the AdornerLayer within the visual tree.