Introduction
This article explains a technique that I have used to define WPF adorners in XAML. The technique requires two custom classes.
These classes are used to define:
- the control to be adorned; and
- the UI elements that make up the adorner
The main class is AdornedControl
which is derived from ContentControl. The content of AdornedControl
is displayed in the normal way in the visual tree of the UI. The adorner is defined using the AdornerContent
property of AdornedControl
. The content of this property, in XAML, defines the UI elements that will be placed in the adorner that is attached to the AdornedControl
.
I have included two sample projects to accompany this article. The simple sample shows a very simple use of AdornedControl
that is described in this article. The advanced sample shows a more advanced usage that meets the needs described in the Background section below.
Assumed Knowledge
It is assumed that you already know C# and have a basic knowledge of using WPF and XAML. Knowledge of the WPF visual and logical tree will also be helpful.
Background
Recently, I found that I was in need of a way to display auxiliary controls when the user hovers the mouse over a particular control.
The particular control is a flow-chart node embedded in a Canvas that looks like this:
The purpose of the auxiliary controls was for the user to be able to drag the node around the Canvas and also to be able to remove the node from the Canvas.
This is what the node looks like with the auxiliary controls displayed:
Normally, the auxiliary controls will not be displayed. They will only be visible when the mouse has moved over the node. They will remain visible until the mouse has moved away from the node and a period of time has elapsed. I decided that adorners would fit this situation well because they are defined outside of the node's visual tree and therefore will not interfere with the normal layout and visuals of the node.
The usual way of creating adorners and adding them to the adorner layer is accomplished through procedural code. What I really wanted was to design both the adorned controls and their adorners in XAML. I created two custom classes that work together to allow this: AdornedControl
and FrameworkElementAdorner
.
But first let's review the usual way of working with adorners.
Using Adorners - The Usual Way
The normal method of using adorners is procedural. You need to create a class that derives from Adorner. In the derived class, you either have some custom rendering code or you attach UI elements and visuals as children to the adorner. Next, you create an instance of your derived adorner class. The UI element that is to be adorned is passed into the constructor of the adorner. Lastly, the adorner is added to the adorner layer. All this is achieved procedurally in C# code.
To illustrate, here is a simple example.
Firstly, here is the class derived from adorner:
class MyAdorner : Adorner
{
public MyAdorner(UIElement adornedElement) :
base(adornedElement)
{
}
}
Custom rendering code is added to 'OnRender
' where you can use 'drawingContext
' directly:
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
}
When you are ready to display the adorner and allow the user to interact with it must be added to the adorner layer. AdornerLayer.GetAdornerLayer
is called and typically passed a reference to the UI element that is to be adorned. GetAdornerLayer
searches back up the visual tree for an appropriate adorner layer to use.
Control parentControl = ...
UIElement adornedElement = ...
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(parentControl);
MyAdorner myAdorner = new MyAdorner(adornedElement);
adornerLayer.Add(myAdorner);
The alternative to writing custom rendering code is to add children to the visual tree of the adorner. This means that any class derived from Visual can be instanced and added to the visual tree underneath the adorner. I have used this technique in FrameworkElementAdorner
.
This has only been a brief discussion about using adorners procedurally. The main purpose of my article is to discuss how to use adorners defined in XAML.
Defining Adorners in XAML using AdornedControl
There are many advantages to defining the UI in XAML. Not only is it simpler than writing the equivalent procedural code, but it's also safer and less prone to errors. Unfortunately, as shown above, adorners are usually created procedurally and cannot, by default, be defined in XAML. This seems like an unnecessary omission from WPF and so I implemented a class that allows it.
AdornedControl
allows adorners to be defined in XAML. It defines both the adorner and the control that is to be adorned.
This is the most basic definition of an AdornedControl
in XAML:
>> <local:AdornedControl
>> Width="50"
>> Height="50"
>> >
>> </local:AdornedControl>
AdornedControl
derives from ContentControl
. As such it can wrap any other content, visual or UI element. For example, this defines the UI element to be adorned as a rectangle:
<local:AdornedControl
Width="50"
Height="50"
>
>> <Rectangle
>> Stroke="Blue"
>> />
</local:AdornedControl>
The next example defines an ellipse as the adorner for the rectangle:
<local:AdornedControl
Width="50"
Height="50"
>
<Rectangle
Stroke="Blue"
/>
>> <local:AdornedControl.AdornerContent>
>> <Ellipse
>> Width="50"
>> Height="50"
>> Stroke="Green"
>> />
>> </local:AdornedControl.AdornerContent>
</local:AdornedControl>
Now we use AdornedControl
's HorizontalAdornerPlacement
combined with the adorner's HorizontalAlignment
to place the adorner horizontally, and outside the adorned control on the right.
<local:AdornedControl
Width="50"
Height="50"
>> HorizontalAdornerPlacement="Outside"
>
<Rectangle
Stroke="Blue"
/>
<local>AdornedControl.AdornerContent>
<Ellipse
Width="50"
Height="50"
Stroke="Green"
>> HorizontalAlignment="Right"
/>
</local:AdornedControl.AdornerContent>
</local:AdornedControl>
AdornedControl
has various ways of showing and hiding the adorner but here we use the IsAdornerVisible property to make it visible by default:
<local:AdornedControl
Width="50"
Height="50"
HorizontalAdornerPlacement="Outside"
>> IsAdornerVisible="True"
>
<Rectangle
Stroke="Blue"
/>
<local:AdornedControl.AdornerContent>
<Ellipse
Width="50"
Height="50"
Stroke="Green"
HorizontalAlignment="Right"
/>
</local:AdornedControl.AdornerContent>
</local:AdornedControl>
IsAdornerVisible can also be set procedurally to show or hide the adorner. Alternatively the Show()
and Hide()
functions and Show
and Hide
commands can be used. In the advanced sample, I use animation to fade the adorner in and out when it is shown and hidden.
Finally, the other custom class which I haven't yet mentioned is FrameworkElementAdorner
. This class is used internally by AdornedControl
. It derives from Adorner
and references a FrameworkElement
as its child in the visual tree. This is what allows us to add any FrameworkElement
into the visual tree of the adorner.
FrameworkElementAdorner
is based on
UIElementAdorner
by Josh Smith which can be found
here. I adapted it to work with
FrameworkElement
and made a few modifications that I needed. The code for
FrameworkElementAdorner
is a good example of how to create an adorner that has visual children.
Conclusion
This example has explained how to use AdornedControl
to define, in XAML, an adorned control and its adorner.
History
- 7th February, 2010: Article updated
- The
SizeChanged
of the adorned element is now monitored in order to update the placement of the adorner - Fixed issue with placement of the adorner on the outside top & left of the adorned element
- 27th February, 2010: Code updated
- The update is simply to remove the copyright information from Assembly.cs that was automatically added when the project was generated. I tested that the code still compiles and runs.
- 18th June, 2010: Article updated
- The '
Focusable
' property for AdornedControl
is now set to 'false
' by default. - Added a new improved sample project. Functionally the 'improved sample' is the same as the 'advanced sample', however the animation code to fade in and fade out the adorner is now integrated into the
AdornedControl
class itself which makes it trivial to move this functionality to new projects. The new methods 'FadeInAdorner
' and 'FadeOutAdorner
' can be called to fade the adorner in and out. Alternately the 'FadeIn
' and 'FadeOut
' commands can trigger this behaviour. The new property 'IsMouseOverShowEnabled
' can be set to 'true
' to automatically fade in and show the adorner when the mouse cursor is hovered over the adorned control.
- 25th September, 2010: Article updated
- The animation state of the adorner is now set when showing/hiding the adorner by setting the
IsAdornerVisible
property (thanks to Tri Q Tran for pointing this out) - Made some changes to make sure the animation state of the adorner is always correct
- Opacity is now animated on the adorner rather than the adorner content
- 11th October, 2010: Article updated
- Added a new dependency property to
AdornedControl
. AdornedTemplatePartName
is used to specify the part name of an element in the visual-tree that is to be adorned. By default, the property is set to null
which causes the AdornedControl
itself to be the UI element that is adorned (the original behaviour). When the property is set to a valid part name, AdornedControl
searches the visual-tree below itself to find the named UI element that is to be adorned. For example, when AdornerContent
is an editable ComboBox
setting AdornedTemplatePartName
to PART_EditableTextBox
will cause the TextBox
part of the ComboBox
to be adorned.
This feature was requested by Richard Deeming.
- 15th March, 2011: Article updated
- Fixed an issue in ImprovedAdornedControlSample. This issue was reported by Member 2477019 (see article messages for details) and the fix was proposed by Louis-Philippe Lauzier.
I removed the code in HideAdornerInternal
that was setting IsAdornerVisible
to false
. This line of code appears to be interfering with data-binding, and after analysis, this line of code appears totally unnecessary, so removing it should cause no issues.
Software craftsman | Author | Writing rapidfullstackdevelopment.com - Posting about how to survive and flourish as a software developer
Follow on Twitter for news and updates: https://twitter.com/codecapers
I'm writing a new book: Rapid Fullstack Development. Learn from my years of experience and become a better developer.
My second book, Bootstrapping Microservices, is a practical and project-based guide to building distributed applications with microservices.
My first book Data Wrangling with JavaScript is a comprehensive overview of working with data in JavaScript.
Data-Forge Notebook is my notebook-style application for data transformation, analysis and transformation in JavaScript.
I have a long history in software development with many years in apps, web apps, backends, serious games, simulations and VR. Making technology work for business is what I do: building bespoke software solutions that span multiple platforms.
I have years of experience managing development teams, preparing technical strategies and creation of software products. I can explain complicated technology to senior management. I have delivered cutting-edge products in fast-paced and high-pressure environments. I know how to focus and prioritize to get the important things done.
Author
- Rapid Fullstack Development
- Bootstrapping Microservices
- Data Wrangling with JavaScript
Creator of Market Wizard
- https://www.market-wizard.com.au/
Creator of Data-Forge and Data-Forge Notebook
- http://www.data-forge-js.com
- http://www.data-forge-notebook.com
Web
- www.codecapers.com.au
Open source
- https://github.com/ashleydavis
- https://github.com/data-forge
- https://github.com/data-forge-notebook
Skills
- Quickly building MVPs for startups
- Understanding how to get the most out of technology for business
- Developing technical strategies
- Management and coaching of teams & projects
- Microservices, devops, mobile and fullstack software development