Monday, July 5, 2010

WPF Adorners

Adorner framework provides a way to decorate WPF elements with extra features. Let's start with the terminologies:

Adorner:
This is a special custom Framework element bounded to a UIElement. It is rendered in a Adorner layer which is always on top of the adorned element(s). All adorners inherit from Adorner, which is an abstract class. They must override OnRender method which is called by the layout system during rendering pass.

Namespace for Adorner: System.Windows.Documents

AdornerDecorator:
Adorner decorators provide adorner layer. This is a little funny name because 'adornment' itself means 'the act of decorating'. It provides a rendering layer for adorners.

AdornerLayer:
AdornerLayer provides rendering layer on top of the UIElement. Its placement is not affected by z-order. It is provided by using AdornerDecorator in the form.

Simple Example:
Let's see an example of usage of adorner. First we need to create an adorner.The adorner below creates a border around a UIElement. This finds out the RenderSize of the UIElement and draws a rectangle on its border. If we use DesiredSize instead, we would get the size as determined using Measure Pass which might not be the exact size as rendered.
class TextBoxBorderAdorner : Adorner
{
public TextBoxBorderAdorner(UIElement adornedElement) : base(adornedElement)
{
}

protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
base.OnRender(drawingContext);

//Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
Rect adornedElementRect = new Rect(this.AdornedElement.RenderSize);
Pen drawingPen = new Pen(new SolidColorBrush(Colors.Blue), 3);


drawingContext.DrawLine(drawingPen, adornedElementRect.BottomLeft, adornedElementRect.BottomRight);
drawingContext.DrawLine(drawingPen, adornedElementRect.TopRight, adornedElementRect.BottomRight);
}
}

Now we create a window with a text box named adornedTextBox. We have created this text box under a StackPanel defined under an AdornerDecorator (which, as discussed, would provide the adorner layer).
<Window x:Class="WPFAdornerProject.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300" Loaded="Window_Loaded">
<StackPanel HorizontalAlignment="Left" Margin="5,5,5,0">
<StackPanel>
<AdornerDecorator>
<StackPanel Orientation="Horizontal" Margin="5,5,5,0">
<Label Content="Name: " />
<TextBox x:Name="adornedTextBox" Width="200" Margin="3,3,3,3"></TextBox>
</StackPanel>
</AdornerDecorator>
</StackPanel>
</StackPanel>
</Window>

In order to apply the adorner to the textbox, we need to add the adorner to the adorner layer for the text box. This can be achieved as follows:

public Window2()
{
InitializeComponent();

//Get the adorner layer from the visual hierarchy upwards
var textBoxAdornerLayer = AdornerLayer.GetAdornerLayer(this.adornedTextBox);

//decorate text box with the adorner
textBoxAdornerLayer.Add(new TextBoxBorderAdorner(this.adornedTextBox));
}

As you can see, AdonerLayer class defines a static method, named GetAdornerLayer(). This method traverses up the visual hierarchy of the control and get the first adorner layer available (through AdornerDecorator). Later, this adorner layer is used to decorate the adornedTextBox with the defined adorner (TextBoxBorderAdorner).



As you can see, we have added a cool border to the adornedTextBox using Adorner framework.

More than one Adorner:
It seems kind of obvious requirement to allow more than one adorner to the same adorner layer. WPF does support applying them like this. They are rendered in the same adorner layer. Let's define one more adorner, which decorates the UIElement with a strikethrough line.
class TextBoxStrikeThroughAdorner : Adorner
{
public TextBoxStrikeThroughAdorner(UIElement adornedElement)
: base(adornedElement)
{
}

protected override void OnRender(System.Windows.Media.DrawingContext drawingContext)
{
base.OnRender(drawingContext);

Rect adornedElementRect = new Rect(this.AdornedElement.RenderSize);
Pen drawingPen = new Pen(new SolidColorBrush(Colors.Blue), 3);

Brush opacityMaskBrush = new SolidColorBrush(Colors.Yellow);
drawingContext.PushOpacityMask(opacityMaskBrush);
drawingContext.DrawLine(drawingPen, new Point(adornedElementRect.Left, adornedElementRect.Bottom - (adornedElementRect.Height / 2)), new Point(adornedElementRect.Right, adornedElementRect.Bottom - (adornedElementRect.Height / 2)));
}
}

Now we change the definition of Window2 as follows:
<Window x:Class="WPFAdornerProject.Window2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window2" Height="300" Width="300" Loaded="Window_Loaded">
<StackPanel HorizontalAlignment="Left" Margin="5,5,5,0">
<StackPanel>
<AdornerDecorator>
<StackPanel Orientation="Horizontal" Margin="5,5,5,0">
<Label Content="Name: " />
<TextBox x:Name="adornedTextBox" Width="200" Margin="3,3,3,3"></TextBox>
</StackPanel>
</AdornerDecorator>
<AdornerDecorator>
<StackPanel Orientation="Horizontal" Margin="5,5,5,0">
<Label Content="Grade: " />
<TextBox x:Name="adornedTextBox2" Width="200" Margin="3,3,3,3"></TextBox>
</StackPanel>
</AdornerDecorator>
</StackPanel>
</StackPanel>
</Window>
Now Window2 appears as follows:

Now we see how we can apply two adorners to the same UIElement. Let's change the Window2's constructor as follows:
public Window2()
{
InitializeComponent();

var textBoxAdornerLayer = AdornerLayer.GetAdornerLayer(this.adornedTextBox);
textBoxAdornerLayer.Add(new TextBoxBorderAdorner(this.adornedTextBox));

//Get the adorner layer
var textBox2AdornerLayer = AdornerLayer.GetAdornerLayer(this.adornedTextBox2);
//first adorner for decorating text box with border
textBox2AdornerLayer.Add(new TextBoxBorderAdorner(this.adornedTextBox2));
//second adorner for decorating text box with a strike through line
textBox2AdornerLayer.Add(new TextBoxStrikeThroughAdorner(this.adornedTextBox2));
}

I was expecting something like this to work but it seems that it doesn't:
textBox2AdornerLayer.Add(new TextBoxStrikeThroughAdorner(new TextBoxBorderAdorner(this.adornedTextBox2)));
This seems a bit of awkward for Adorner being a decorator (See Decorator Pattern).



You can verify that both adorners are applied to the second text box.

AdornedElementPlaceHolder:
AdornmentPlaceHolder is used when a custom error template is desired for controls in error as a result of some validation. Basically, a validation error template is defined as follows:

<ControlTemplate x:Key="ValidationErrorTemplate">
<Border BorderBrush="Blue" BorderThickness="5">
<AdornedElementPlaceholder />
</Border>
</ControlTemplate>
Here we have defined a blue border outlining the adorned element. Let us apply this template to the text box.

<TextBox Validation.ErrorTemplate="{StaticResource ValidationErrorTemplate}">
<Binding Path="Age">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox>

As you might have noticed, we will be assigning the an object to the data context having property "Age". e.g.

this.DataContext = new Student() { Age = 25 };
The example implementation of Student is as follows:

class Student : DependencyObject
{
public static DependencyProperty AgeProperty = DependencyProperty.Register("Age", typeof(int), typeof(Student));
public int Age
{
get
{
return (int)GetValue(AgeProperty);
}
set
{
SetValue(AgeProperty, value);
}
}
}
In the above example, if there is any exception to the text box in consideration then the ValidationErrorTemplate is applied. As soon as the user corrects the data, then the application of template is removed for the text box.

3 comments:

Alan Lam said...

Hi,

Great article but I got lost after

AdornedElementPlaceHolder:

Where do you put the block of code with the ControlTemplate?

When I place it after the Window definition I get the error Property "Content" can only be set once.

Thanks.

Alan Lam said...

Hi,

The ControlTemplate goes inside the Windows.Resources.

Now it runs. Thanks for the article.

Muhammad Shujaat Siddiqi said...

I am glad you found this useful. You are welcome!