An adorner is a special kind of class that is rendered above all UI controls, in what is known as an adorner layer. Adorner elements in this layer will always be rendered on top of the normal WPF controls, regardless of their Panel.ZIndex property setting. Each adorner is bound to an element of type UIElement and independently rendered in a position that is relative to the adorned element.
The purpose of the adorner is to provide certain visual cues to the application user. For example, we could use an adorner to display a visual representation of UI elements that are being dragged in a drag and drop operation. Alternatively, we could use an adorner to add handles to a UI control to enable users to resize the element.
As the adorner is added to the adorner layer, it is the adorner layer that is the parent of the adorner, rather than the adorned element. In order to create a custom adorner, we need to declare a class that extends the Adorner class.
When creating a custom adorner, we need to be aware that we are responsible for writing the code to render its visuals. However, there are a few different ways to construct our adorner graphics; we can use the OnRender or OnRenderSizeChanged methods and a drawing context to draw basic lines and shapes, or we can use the ArrangeOverride method to arrange .NET controls.
Adorners receive events like other .NET controls, although if we don't need to handle them, we can arrange for them to be passed straight through to the adorned element. In these cases, we can set the IsHitTestVisible property to false and this will enable pass-through hit-testing of the adorned element. Let's look at an example of a resizing adorner that lets us resize shapes on a canvas.
Before we investigate the adorner class, let's first see how we can use it. Adorners need to be initialized in code, and so a good place to do this is in the UserControl.Loaded method, when we can be certain that the canvas and its items will have been initialized. Note that as adorners are purely UI related, initializing them in the control's code behind does not present any conflict when using MVVM:
public AdornerView()
{
InitializeComponent();
Loaded += View_Loaded;
} ... private void View_Loaded(object sender, RoutedEventArgs e) { AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(Canvas); foreach (UIElement uiElement in Canvas.Children) { adornerLayer.Add(new ResizeAdorner(uiElement)); } }
We access the adorner layer for the canvas that we will add the adorners to using the AdornerLayer.GetAdornerLayer method, passing in the canvas as the Visual input parameter. In this example, we attach an instance of our ResizeAdorner to each element in the canvas' Children collection and then add it to the adorner layer.
Now, we just need a Canvas panel named Canvas and some shapes to resize:
<Canvas Name="Canvas"> <Rectangle Canvas.Top="50" Canvas.Left="50" Fill="Lime" Stroke="Black" StrokeThickness="3" Width="150" Height="50" /> <Rectangle Canvas.Top="25" Canvas.Left="250" Fill="Yellow" Stroke="Black" StrokeThickness="3" Width="100" Height="150" /> </Canvas>
Let's now see the code in our ResizeAdorner class:
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; namespace CompanyName.ApplicationName.Views.Adorners { public class ResizeAdorner : Adorner { private VisualCollection visualChildren; private Thumb top, left, bottom, right; public ResizeAdorner(UIElement adornedElement) : base(adornedElement) { visualChildren = new VisualCollection(this); top = InitializeThumb(Cursors.SizeNS, Top_DragDelta); left = InitializeThumb(Cursors.SizeWE, Left_DragDelta); bottom = InitializeThumb(Cursors.SizeNS, Bottom_DragDelta); right = InitializeThumb(Cursors.SizeWE, Right_DragDelta); } private Thumb InitializeThumb(Cursor cursor, DragDeltaEventHandler eventHandler) { Thumb thumb = new Thumb(); thumb.BorderBrush = Brushes.Black; thumb.BorderThickness = new Thickness(1); thumb.Cursor = cursor; thumb.DragDelta += eventHandler; thumb.Height = thumb.Width = 6.0; visualChildren.Add(thumb); return thumb; } private void Top_DragDelta(object sender, DragDeltaEventArgs e) { FrameworkElement adornedElement = (FrameworkElement)AdornedElement; adornedElement.Height = Math.Max(adornedElement.Height - e.VerticalChange, 6); Canvas.SetTop(adornedElement, Canvas.GetTop(adornedElement) + e.VerticalChange); } private void Left_DragDelta(object sender, DragDeltaEventArgs e) { FrameworkElement adornedElement = (FrameworkElement)AdornedElement; adornedElement.Width = Math.Max(adornedElement.Width - e.HorizontalChange, 6); Canvas.SetLeft(adornedElement, Canvas.GetLeft(adornedElement) + e.HorizontalChange); } private void Bottom_DragDelta(object sender, DragDeltaEventArgs e) { FrameworkElement adornedElement = (FrameworkElement)AdornedElement; adornedElement.Height = Math.Max(adornedElement.Height + e.VerticalChange, 6); } private void Right_DragDelta(object sender, DragDeltaEventArgs e) { FrameworkElement adornedElement = (FrameworkElement)AdornedElement; adornedElement.Width = Math.Max(adornedElement.Width + e.HorizontalChange, 6); } protected override void OnRender(DrawingContext drawingContext) { SolidColorBrush brush = new SolidColorBrush(Colors.Transparent); Pen pen = new Pen(new SolidColorBrush(Colors.DeepSkyBlue), 1.0); drawingContext.DrawRectangle(brush, pen, new Rect(-2, -2, AdornedElement.DesiredSize.Width + 4, AdornedElement.DesiredSize.Height + 4)); } protected override Size ArrangeOverride(Size finalSize) { top.Arrange( new Rect(AdornedElement.DesiredSize.Width / 2 - 3, -8, 6, 6)); left.Arrange( new Rect(-8, AdornedElement.DesiredSize.Height / 2 - 3, 6, 6)); bottom.Arrange(new Rect(AdornedElement.DesiredSize.Width / 2 - 3, AdornedElement.DesiredSize.Height + 2, 6, 6)); right.Arrange(new Rect(AdornedElement.DesiredSize.Width + 2, AdornedElement.DesiredSize.Height / 2 - 3, 6, 6)); return finalSize; } protected override int VisualChildrenCount { get { return visualChildren.Count; } } protected override Visual GetVisualChild(int index) { return visualChildren[index]; } } }
Note that we have declared the Adorners namespace within the Views project, as this is the only place that it will be used. Inside the class, we declare the VisualCollection object that will contain the visuals that we want to render and then the visuals themselves, in the shape of Thumb controls.
We've chosen Thumb elements because they have built-in functionality that we want to take advantage of. They provide a DragDelta event that we will use to register the users' mouse movements when they drag each Thumb. These controls are normally used internally in the Slider and ScrollBar controls to enable users to alter values, so they're perfect for our purposes here.
We initialize these objects in the constructor, specifying a custom cursor and a different DragDelta event handler for each Thumb control. In these separate event handlers, we use the HorizontalChange or VerticalChange properties of the DragDeltaEventArgs object to specify the distance and direction of the mouse movement that triggered the event.
We use these values to move and/or resize the adorned element by the appropriate amount and direction. Note that we use the Math.Max method and the value 6 in our example to ensure that the adorned element cannot be resized smaller than the size of each Thumb element and the Stroke size of each adorned element.
After the four DragDelta event handlers, we find two different ways to render our adorner visuals. In the first method, we use the DrawingContext object that is passed into the OnRender method by the base class to manually draw shapes. This is somewhat similar to the way that we used to draw in the Control.Paint event handler methods when using Windows.Forms.
In this overridden method, we draw a rectangle that surrounds our element and is four pixels bigger than it in both dimensions. Note that we define a transparent background for the drawing brush, as we only want to see the rectangle border. Remember that adorner graphics are rendered on top of the adorned element, but we do not want to cover it.
In the ArrangeOverride method, we use .NET Framework to render our Visual elements using their Arrange methods, as we would in a custom panel. Note that we could just as easily render our rectangle border in this method using a Rectangle element; the OnRender method was used in this example merely as a demonstration.
In this method, we simply arrange each Visual element at the relevant position and size in turn. Calculating the appropriate positions can be achieved simply by dividing the width or height of each adorned element in half and subtracting half of the width or height of each thumb element.
Finally, we get to the protected overridden VisualChildrenCount property and GetVisualChild method. The Adorner class extends the FrameworkElement class and that will normally return either zero or one from the VisualChildrenCount property, as each instance is normally represented by either no visual, or a single rendered visual.
In our case and other situations when a derived class has multiple visuals to render, it is a requirement of the layout system that the correct number of visuals is specified. For example, if we always returned the value 2 from this property, then only two of our thumbs would be rendered on screen.
Likewise, we also need to return the correct item from our visual collection when requested to from the GetVisualChild method. If, for example, we always returned the first visual from our collection, then only that visual would be rendered, as the same visual cannot be rendered more than once. Let's see what our adorners look like when rendered above each of our shapes: