Click here to Skip to main content
15,860,859 members
Articles / Desktop Programming / WPF

Defining WPF Adorners in XAML

Rate me:
Please Sign up or sign in to vote.
4.97/5 (89 votes)
15 Mar 2011CPOL7 min read 403.3K   24.6K   181   104
Examines a custom class that allows adorners to be defined in XAML.

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:

adornedcontrol1.jpg

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:

adornedcontrol2.jpg

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:

C#
class MyAdorner : Adorner
{
    public MyAdorner(UIElement adornedElement) :
    base(adornedElement)
    {
        // ... other initialisation ...
    }

    // ... class members ...
}

Custom rendering code is added to 'OnRender' where you can use 'drawingContext' directly:

C#
protected override void OnRender(DrawingContext drawingContext)
{
	base.OnRender(drawingContext);

	// ... add custom rendering code here ...
}

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.

C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Australia Australia
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

Comments and Discussions

 
Questionas usual another dumbass create sh*tty useless code Pin
Member 156665198-Jun-22 5:01
Member 156665198-Jun-22 5:01 
QuestionRemoving event handlers Pin
Member 999117926-Nov-20 3:24
Member 999117926-Nov-20 3:24 
QuestionAdorner content disappears! Pin
Member 1344443514-Nov-17 3:37
Member 1344443514-Nov-17 3:37 
AnswerRe: Adorner content disappears! Pin
tom kefauver17-Dec-20 10:20
tom kefauver17-Dec-20 10:20 
QuestionLittle question Pin
Mauro Sampietro5-Aug-16 0:29
Mauro Sampietro5-Aug-16 0:29 
GeneralMy vote of 1 Pin
jacob80007-Dec-14 19:54
jacob80007-Dec-14 19:54 
QuestionHow is the drag handler functionality implemented? Pin
Sabuncu30-Apr-14 6:51
Sabuncu30-Apr-14 6:51 
AnswerRe: How is the drag handler functionality implemented? Pin
Ashley Davis1-May-14 0:22
Ashley Davis1-May-14 0:22 
GeneralRe: How is the drag handler functionality implemented? Pin
Sabuncu1-May-14 0:48
Sabuncu1-May-14 0:48 
Thanks! Incredible functionality behind a seemingly minor parameter.
QuestionGreat help! Pin
Thornik3-Sep-13 6:46
Thornik3-Sep-13 6:46 
AnswerRe: Great help! Pin
Ashley Davis8-Sep-13 12:05
Ashley Davis8-Sep-13 12:05 
GeneralMy vote of 5 Pin
DrewAH15-Jul-13 8:36
DrewAH15-Jul-13 8:36 
GeneralMy vote of 5 Pin
Rufus Buschart7-May-13 8:37
Rufus Buschart7-May-13 8:37 
QuestionBinding question Pin
jenschrrh3-Jan-13 22:13
jenschrrh3-Jan-13 22:13 
AnswerRe: Binding question Pin
Ashley Davis17-Mar-13 0:39
Ashley Davis17-Mar-13 0:39 
QuestionWhat happens Pin
John Zabroski6-Dec-12 7:52
John Zabroski6-Dec-12 7:52 
AnswerRe: What happens Pin
Ashley Davis17-Mar-13 0:37
Ashley Davis17-Mar-13 0:37 
GeneralRe: What happens Pin
m.gnu.27-Feb-18 22:55
m.gnu.27-Feb-18 22:55 
QuestionDataContext question Pin
Member 83457018-May-12 5:59
Member 83457018-May-12 5:59 
AnswerRe: DataContext question Pin
Ashley Davis8-May-12 14:42
Ashley Davis8-May-12 14:42 
BugMemory leakS Pin
MrBaboon31-Jan-12 23:17
MrBaboon31-Jan-12 23:17 
GeneralRe: Memory leakS Pin
Ashley Davis14-Feb-12 16:23
Ashley Davis14-Feb-12 16:23 
QuestionMy vote of 5 Pin
MrBaboon26-Jan-12 6:48
MrBaboon26-Jan-12 6:48 
GeneralMy vote of 5 Pin
acryll5-Dec-11 6:56
acryll5-Dec-11 6:56 
QuestionWOW Pin
acryll5-Dec-11 6:53
acryll5-Dec-11 6:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.