This blog post shows how to decorate controls of an existing WPF/Xaml UI with limited code changes. To achieve this WPF concepts like Adorners, Dependency Properties and ControlTemplates come into play.
Recently I worked on an application which was overloaded with input fields. To make it even worse user input (mostly into a TextBox) triggered internal calculations and the update of multiple dependent fields. To bring changed values to the users attention we decided to highlight the changed values by decorating controls (TextBox elements) whenever the displayed value is changed. The picture below shows a simplified version of the user interface. Input of the TextBox Distance Top updates the values Distance Ax, Distance Ay and Distance Az.
In short the following steps are necessary:
- Create an Attached Property to let controls register for/enable the highlight feature
- Create a ControlTemplate which is attached to existing controls
- Upon registration of an Attached Property register for TextChanged events
- Create an Adorner Class which holds the defined ControlTemplate as its only child
- Add an instance of Adorner to the TextBox's AdornerLayer.
Create Attached Property
The Attached Property mechanism is a mean to create Dependency Properties and reuse these properties from other elements. Layout constraints are a good example for these kind of properties (e.g. DockPanel.Dock="Top"...).
We need an Attached Property to let a TextBox declare that value changes should be highlighted.
//The property to enable highlight feature for a specific user control public static readonly DependencyProperty HighlightProperty = DependencyProperty.RegisterAttached("Highlight", typeof (Boolean), typeof (UIElement), new FrameworkPropertyMetadata(false, HighlightPropertySetCallback));public static void SetHighlight(UIElement element, Boolean value) { element.SetValue(HighlightProperty, value); } public static Boolean GetHighlight(UIElement element) { return (Boolean)element.GetValue(HighlightProperty); }
Create a visual indicator using ControlTemplates
<ControlTemplate x:Key="HighlightTemplate"> <DockPanel Name="in"> <Grid DockPanel.Dock="Right" Width="16" Height="16" VerticalAlignment="Center" Margin="3 4 0 0"> <Ellipse Width="16" Height="16"> <Ellipse.Fill> </Ellipse> <SolidColorBrush x:Name="brush" Color="SteelBlue"></SolidColorBrush></Ellipse.Fill> <Polyline.Fill> <Polyline VerticalAlignment="Top" HorizontalAlignment="Center" StrokeThickness="0" Margin="0 2 0 0" Points="3,0,6,0,5,5,8,5,3,14,4,8,1.5,8,3,0"> <SolidColorBrush x:Name="arrowBrush" Color="White"></SolidColorBrush> <controltemplate> </Polyline.Fill> </Polyline> </Grid> </DockPanel>
Register on value changes
In the first snippet we registered a callback method upon setting the attached property. What the callback method does is to register for TextChanged events of the given TextBox.private static void HighlightPropertySetCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
//get target textbox
TextBox textBox = dependencyObject as TextBox;
if (textBox == null)
{
return;
}
//Register on text changed
textBox.TextChanged += OnTextBoxChanged;
}
Upon text changed we will add an Adorner to our TextBox's Adornerlayer. But first, we need to create an Adorner which holds the ControlTemplate presented above.
Create an Adorner to display the ControlTemplate
The following snippet shows the implementation of the Adorner used to decorate the TextBox with the shown ControlTemplate.
public class PresentationAdorner : Adorner
{
private Control child;
public Control Child
{
get { return child; }
set
{
if (child != null)
{
RemoveVisualChild(child);
}
child = value;
if (child != null)
{
AddVisualChild(child);
}
}
}
public PresentationAdorner([NotNull] UIElement adornedElement) : base(adornedElement) { }
protected override int VisualChildrenCount
{
get { return 1; }
}
protected override Visual GetVisualChild(int index)
{
if (index != 0) throw new ArgumentOutOfRangeException();
return child;
}
protected override Size MeasureOverride(Size constraint)
{
child.Measure(constraint);
return child.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
child.Arrange(new Rect(new Point(0, 0), finalSize));
return new Size(child.ActualWidth, child.ActualHeight);
}
}
The implementation needs to be derived from Adorner. Typically Adorner implementation override the
OnRender method and directly draw on the passed DrawingContext.
In our case the appearance of the Adorner is defined by the ControlTemplate shown above. The template is set as the Adorners only visual child. Reseting the adorners Child property will remove any previously added visual child. Methods MeasureOverride and ArrangeOverride derive Size an Position of the Adorner from its Child Property.Add Adorner to AdornerLayer on TextChanged
private static void OnTextBoxChanged(object sender, TextChangedEventArgs args)
{
var textbox = sender as TextBox;
if (textbox == null)
{
return;
}
//Get the adorner layer. Be sure your Application has one.
AdornerLayer layer = AdornerLayer.GetAdornerLayer(textbox);
if (layer == null)
{
return;
}
//Get the control template from resources.
var controlTemplate = Application.Current.Resources["HighlightTemplate"] as ControlTemplate;
if (controlTemplate == null)
{
return;
}
//Get allready existing adorner from adorner layer
var adorners = layer.GetAdorners(textbox);
if (adorners != null)
{
var presentationAdorner = adorners.OfType().First();
if (presentationAdorner != null)
{
presentationAdorner.Child = new Control { Template = controlTemplate, Focusable = false, };
}
}
else
{
// Or create a new adorner and add it to the layer
var adorner = new PresentationAdorner(textbox);
adorner.Child = new Control { Template = controlTemplate, Focusable = false, };
layer.Add(adorner);
}
}
Code above is executed whenever the text of a registered TextBox changes. If the AdornerLayer contains no Adorner of type PresentationAdorner a new instance is created and the Adorner's Child property is set to a Control holding the ControlTemplated shown above.
Be sure that your application provides an AdornerLayer by using an AdornerDecorator element within the applications ui element hierarchy. The AdornerLayer provides the static method GetAdorners to retrieve all Adorners for a specific ui element.
The screenshot above shows our example application after the value in Distance Top changed. For the sake of brevity the implementation shown in this blog post is rather simple and has many limitations (Decorations are not removed once a value change occured, decorations also appear on the control the user entered a value, the ControlTemplate for decorations is hardcoded and cannot changed, ... and many more).
However, for a start I hope this blog helps if you need to add decorations to controls.

