Introduction
This
article presents the SmartAdorner
class that allows you to design and attach adorner to
any visual element in WPF using only XAML. The adorner content is defined in a
data template allowing data-binding to the adorned element’s data context. In this way, properties from the bound model (or view model) are used and manipulated in the adorner elements in the same as in the adorned element.
To further
support the design of adorners, the article code also includes a Thumb
-derived
control that allows manipulation of coordinates using only data binding and a
general control to support resizing handles in the corners.
Background
Adorners in WPF are decorations on existing elements shown in an adorner layer above other visual
elements. Typical use for adorners is for dragging/resize handles on selected elements. Without the adorner layer, such handles would sometimes be hidden behind other
objects, making it more difficult or even impossible for the user to interact
with them. The SmartAdorner
solution presented here is general and supports other types of handles and dynamic and interactive decorations too.
An adorner
is always associated with an adorned element which has to be passed as an
argument to the Adorner
constructor. Thus it is not possible to create
adorners directly in WPF, without help from a helper, such as the SmartAdorner
presented here. SmartAdorner
derives from Adorner
but has attached properties
to allow definition of the XAML content in a data template and to dynamically
show and hide the adorner.
When searching
the internet for existing solutions, I did not find anyone as simple as this
one, also supporting data binding.
Using the Code
Add a General Adorner
To specify
the content for the adorner of an element, you simply set the SmartAdorner.Template
attached property like this:
<StackPanel Name="test" a:SmartAdorner.Visible="{Binding IsSelected}" >
<Rectangle Width="64" Height="64" Stroke="Black" Fill="White" />
<TextBlock Text="{Binding Text}" />
<a:SmartAdorner.Template>
<DataTemplate DataType="{x:Type local:IconViewModel}" >
<Canvas>
<a:DragThumb Name="IconThumb" Canvas.Left="-6"
Canvas.Top="-6" Width="12"
Height="12" X="{Binding X}" Y="{Binding Y}" />
<TextBox Canvas.Top="64" T
ext="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
</Canvas>
</DataTemplate>
</a:SmartAdorner.Template>
</StackPanel>
In this
example, we adorn the StackPanel
using a Canvas
containing a thumb control and
a textbox. As demonstrated, data bindings can be used. To make the adorner
visible, the SmartAdorner.Visible
has to be set to true
. In this example, we use
data binding to set this dynamically when the item is selected in the
containing list box.
When
designing the adorner, it is good to know that the data template context is
layout in a ContentPresenter
in the adorner layer whose position, size and layout
and rendering transformation is automatically adjusted to the layout of the
adorned element. Here, a Canvas
is used to allow exact position of elements,
even outside the boundaries of the adorned element, but any element can be
used inside the SmartAdorner
's data template.
For
advanced usage, SmartAdorner
also supports dynamically selecting adorner
template based on the data object through the SmartAdorner.TemplateSelector
attached property.
Adding Dragging Thumbs
A common
usage for adorners is to provide dragging and resize handles. The in-built Thumb
class can be used for that provided that at least a DragDelta
event handler is
written in code behind. The article code includes a derived version of Thumb
, DragThumb
,
that allows any pair of properties (or just a single dimension) to be changed
whenever dragged. The dependency properties X
and Y
in DragThumb
allows binding
to properties directly in XAML without writing any code-behind. In the sample
project, this is used to be able to manipulate a line object with properties X1
,
Y1
, X2
and Y2
:
<a:SmartAdorner.Template>
<DataTemplate DataType="{x:Type local:LineViewModel}" >
<Canvas>
<a:DragThumb Canvas.Left="{Binding X1}"
Canvas.Top="{Binding Y1}" Margin="-6"
X="{Binding X1}"
Y="{Binding Y1}" Width="12"
Height="12" />
<a:DragThumb Canvas.Left="{Binding X2}"
Canvas.Top="{Binding Y2}" Margin="-6"
X="{Binding X2}" Y="{Binding Y2}"
Width="12" Height="12"/>
</Canvas>
</DataTemplate><br /> </a:SmartAdorner.Template>
Adding Resizing Handles
Since a
common usage of adorners is to provide resize handles in the corners, I created
a control that can simplify that. Just put the ResizingAdorner
inside the
adorner template and bind the properties you want to manipulate to X
, Y
, Width
and Height
like this:
<a:SmartAdorner.Template>
<DataTemplate DataType="{x:Type local:RectViewModel}" >
<a:ResizingAdorner X="{Binding X}" Y="{Binding Y}"
Width="{Binding Width}" Height="{Binding Height}"
MinWidth="10" MinHeight="20"
MaxWidth="200" MaxHeight="400" />
</DataTemplate>
</a:SmartAdorner.Template>
As shown in
the code-snippet, ResizingAdorner
supports minimum and maximum constraints of
the width and height.
Since
ResizingAdorner
is a content control arbitrary content can be put inside it and
if you want you can use it outside an adorner template as well.
By default, ordinary Thumbs
are shown in the
corners, but since ResizingAdorner
is designed as a look-less custom control, it
supports complete restyling by specifying a new control template.
Points of Interest
Efficient Implementation of SmartAdorner
The SmartAdorner
class derives from Adorner
and defines the attached properties used to set the template and make adorner visible. It also uses the private
attached property Adorner
to store a reference to the adorner created just when Visible
is set to true
in the property change callback, as shown below:
private static void OnVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
FrameworkElement adornedElement = d as FrameworkElement;
if (adornedElement == null) throw new InvalidOperationException("Adorners can only be applied to elements deriving from FrameworkElement");
AdornerLayer layer = AdornerLayer.GetAdornerLayer(adornedElement);
if (layer == null) throw new InvalidOperationException("Cannot show adorner since no adorner layer was found in the visual tree");
SmartAdorner adorner = GetAdorner(adornedElement);
bool isVisible = (bool)e.NewValue;
if (isVisible && adorner == null)
{
adorner = new SmartAdorner(adornedElement);
SetAdorner(adornedElement, adorner);
layer.Add(adorner);
}
else if( !isVisible && adorner != null )
{
layer.Remove(adorner);
SetAdorner(adornedElement, null);
}
}
The SmartAdorner
uses a ContentPresenter
as a single child, whose data context and template is initialized in the constructor:
private ContentPresenter presenter;
public SmartAdorner(FrameworkElement adornedElement)
: base(adornedElement)
{
presenter = new ContentPresenter();
Binding dataContextBinding = new Binding("DataContext");
dataContextBinding.Source = adornedElement;
BindingOperations.SetBinding(presenter, ContentPresenter.ContentProperty, dataContextBinding);
Template = GetTemplate(adornedElement);
TemplateSelector = GetTemplateSelector(adornedElement);
AddVisualChild(presenter);
AddLogicalChild(presenter);
}
public DataTemplate Template
{
get { return presenter.ContentTemplate; }
set { presenter.ContentTemplate = value; }
}
public DataTemplateSelector TemplateSelector
{
get { return presenter.ContentTemplateSelector; }
set { presenter.ContentTemplateSelector = value; }
}
The rest is just some method that has to be overridden to layout the single child.
protected override int VisualChildrenCount
{
get
{
return 1;
}
}
protected override System.Windows.Media.Visual GetVisualChild(int index)
{
if (index == 0) return presenter;
throw new ArgumentOutOfRangeException("index");
}
protected override Size MeasureOverride(Size constraint)
{
presenter.Measure(constraint);
return presenter.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
presenter.Arrange(new Rect(new Point(0,0), finalSize));
return finalSize;
}
Conclusion
In this article, you learned an efficient implementation of a general adorner class that allows you do define new adorners in XAML with support for data binding. The solution is quite general and efficient. Although it may not be not a complete solution for building a design Surface, I hope you find good use of it.
History
- 20 January, 2014 – Initial version