Skip to content

Commit bb120de

Browse files
Add proper Adorner wrapper class for coordination of size/layout with AdornerLayer and customization point
Add extra info about this to the doc TODO: need to test passing in a custom/subclassed Adorner still
1 parent b420c9e commit bb120de

File tree

8 files changed

+170
-39
lines changed

8 files changed

+170
-39
lines changed

components/Adorners/samples/Adorners.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Adorners allow a developer to overlay any content on top of another UI element i
1818

1919
## Background
2020

21-
Adorners originally existed in WPF as a main integration part as part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) See more about the commonalities and differences to WinUI adorners in the migration section below.
21+
Adorners originally existed in WPF as an extension part of the framework. [You can read more about how they worked in WPF here.](https://learn.microsoft.com/dotnet/desktop/wpf/controls/adorners-overview) See more about the commonalities and differences to WinUI adorners in the migration section below.
2222

2323
### Without Adorners
2424

@@ -46,16 +46,16 @@ Adorners can be used in a variety of scenarios. For instance, if you wanted to h
4646

4747
Another common use case for adorners is to allow a user to resize a visual element.
4848

49-
// TODO: Make a simple example here for this soon...
49+
// TODO: Make an example here for this w/ custom Adorner class...
5050

5151
## Migrating from WPF
5252

53-
The WinUI Adorner API surface adapts many similar names and concepts as WPF Adorners; however, WinUI Adorners are XAML based and make use of the attached properties to make using Adorners much simpler, like Behaviors. Where as defining Adorners in WPF required custom drawing routines. It's possible to replicate many similar scenarios with this new API surface and make better use of XAML features like data binding; however, it will mean rewriting any existing WPF code.
53+
The WinUI Adorner API surface adapts many similar names and concepts as WPF Adorners; however, WinUI Adorners are XAML based and make use of the attached properties to make using Adorners much simpler, like Behaviors. Where as defining Adorners in WPF required custom drawing routines. It's possible to replicate many similar scenarios with this new API surface and make better use of XAML features like data binding and styling; however, it will mean rewriting any existing WPF code.
5454

5555
### Concepts
5656

57-
The `AdornerLayer` is still an element of the visual tree which resides atop other content within your app and is the parent of all adorners. In WPF, this is usually already automatically a component of your app or `ScrollViewer`. Like WPF, adorners parent's in the visual tree will be the `AdornerLayer` and not the adorned element.
57+
The `AdornerLayer` is still an element of the visual tree which resides atop other content within your app and is the parent of all adorners. In WPF, this is usually already automatically a component of your app or `ScrollViewer`. Like WPF, adorners parent's in the visual tree will be the `AdornerLayer` and not the adorned element. The WinUI-based `AdornerLayer` will automatically be inserted in many common scenarios, otherwise, an `AdornerDecorator` may still be used to direct the placement of the `AdornerLayer` within the Visual Tree.
5858

5959
The `AdornerDecorator` provides a similar purpose to that of its WPF counterpart, it will host an `AdornerLayer`. The main difference with the WinUI API is that the `AdornerDecorator` will wrap your contained content vs. in WPF it sat as a sibling to your content. We feel this makes it easier to use and ensure your adorned elements reside atop your adorned content, it also makes it easier to find within the Visual Tree for performance reasons.
6060

61-
TODO: Adorner class info...
61+
The `Adorner` class in WinUI is now a XAML-based element that can contain any content you wish to overlay atop your adorned element. In WPF, this was a non-visual class that required custom drawing logic to render the adorner's content. This change allows for easier creation of adorners using XAML, data binding, and styling. Many similar concepts and properties still exist between the two, like a reference to the `AdornedElement`. Any loose XAML attached via the `AdornerLayer.Xaml` attached property is automatically wrapped within a basic `Adorner` container. You can either restyle or subclass the `Adorner` class in order to better encapsulate logic of a custom `Adorner` for your specific scenario, like a behavior, as shown above.

components/Adorners/src/Adorner.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
using CommunityToolkit.WinUI.Helpers;
2+
3+
namespace CommunityToolkit.WinUI;
4+
5+
/// <summary>
6+
/// A class which represents a <see cref="FrameworkElement"/> that decorates a <see cref="UIElement"/>.
7+
/// </summary>
8+
/// <remarks>
9+
/// An adorner is a custom element which is bound to a specific <see cref="UIElement"/> and can
10+
/// provide additional visual cues to the user. Adorners are rendered in an
11+
/// <see cref="AdornerLayer"/>, a special layer that is on top of the adorned element or a collection
12+
/// of adorned elements. Rendering of an adorner is independent of the UIElement it is bound to. An
13+
/// adorner is typically positioned relative to the element it is bound to based on the upper-left
14+
/// coordinate origin of the adorned element.
15+
///
16+
/// Note: The parent of an <see cref="Adorner"/> is always an <see cref="AdornerLayer"/> and not the element being adorned.
17+
/// </remarks>
18+
public partial class Adorner : ContentControl
19+
{
20+
/// <summary>
21+
/// Gets the element being adorned by this <see cref="Adorner"/>.
22+
/// </summary>
23+
public UIElement? AdornedElement
24+
{
25+
get;
26+
internal set
27+
{
28+
var oldvalue = field;
29+
field = value;
30+
OnAdornedElementChanged(oldvalue, value);
31+
}
32+
}
33+
34+
private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue)
35+
{
36+
if (oldvalue is not null
37+
&& oldvalue is FrameworkElement oldfe)
38+
{
39+
// TODO: Should we explicitly detach the WEL here?
40+
}
41+
42+
if (newvalue is not null
43+
&& newvalue is FrameworkElement newfe)
44+
{
45+
// Track changes to the AdornedElement's size
46+
var weakPropertyChangedListenerSize = new WeakEventListener<Adorner, object, SizeChangedEventArgs>(this)
47+
{
48+
OnEventAction = static (instance, source, eventArgs) => instance.OnSizeChanged(source, eventArgs),
49+
OnDetachAction = (weakEventListener) => newfe.SizeChanged -= weakEventListener.OnEvent // Use Local References Only
50+
};
51+
newfe.SizeChanged += weakPropertyChangedListenerSize.OnEvent;
52+
53+
// Track changes to the AdornedElement's layout
54+
// Note: This is pretty spammy, thinking we don't need this?
55+
/*var weakPropertyChangedListenerLayout = new WeakEventListener<Adorner, object?, object>(this)
56+
{
57+
OnEventAction = static (instance, source, eventArgs) => instance.OnLayoutUpdated(source, eventArgs),
58+
OnDetachAction = (weakEventListener) => newfe.LayoutUpdated -= weakEventListener.OnEvent // Use Local References Only
59+
};
60+
newfe.LayoutUpdated += weakPropertyChangedListenerLayout.OnEvent;*/
61+
62+
// Initial size & layout update
63+
OnSizeChanged(null, null!);
64+
OnLayoutUpdated(null, null!);
65+
}
66+
}
67+
68+
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
69+
{
70+
if (AdornedElement is null) return;
71+
72+
Width = AdornedElement.ActualSize.X;
73+
Height = AdornedElement.ActualSize.Y;
74+
}
75+
76+
internal void OnLayoutUpdated(object? sender, object e)
77+
{
78+
// Note: Also called by the parent AdornerLayer when its size changes
79+
if (AdornerLayer is not null
80+
&& AdornedElement is not null)
81+
{
82+
var coord = AdornerLayer.CoordinatesTo(AdornedElement);
83+
84+
Canvas.SetLeft(this, coord.X);
85+
Canvas.SetTop(this, coord.Y);
86+
}
87+
}
88+
89+
internal AdornerLayer? AdornerLayer { get; set; }
90+
91+
/// <summary>
92+
/// Constructs a new instance of <see cref="Adorner"/>.
93+
/// </summary>
94+
public Adorner()
95+
{
96+
this.DefaultStyleKey = typeof(Adorner);
97+
}
98+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
2+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
3+
xmlns:ui="using:CommunityToolkit.WinUI">
4+
5+
<Style BasedOn="{StaticResource AdornerDefaultStyle}"
6+
TargetType="ui:Adorner" />
7+
8+
<Style x:Key="AdornerDefaultStyle"
9+
TargetType="ui:Adorner">
10+
<Setter Property="IsTabStop" Value="False" />
11+
<Setter Property="HorizontalAlignment" Value="Stretch" />
12+
<Setter Property="VerticalAlignment" Value="Stretch" />
13+
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
14+
<Setter Property="VerticalContentAlignment" Value="Stretch" />
15+
<Setter Property="Template">
16+
<Setter.Value>
17+
<ControlTemplate TargetType="ui:Adorner">
18+
<ContentPresenter Padding="{TemplateBinding Padding}"
19+
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
20+
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
21+
AutomationProperties.AccessibilityView="Raw"
22+
Background="{TemplateBinding Background}"
23+
BackgroundSizing="{TemplateBinding BackgroundSizing}"
24+
BorderBrush="{TemplateBinding BorderBrush}"
25+
BorderThickness="{TemplateBinding BorderThickness}"
26+
Content="{TemplateBinding Content}"
27+
CornerRadius="{TemplateBinding CornerRadius}" />
28+
</ControlTemplate>
29+
</Setter.Value>
30+
</Setter>
31+
</Style>
32+
</ResourceDictionary>

components/Adorners/src/AdornerLayer.cs

Lines changed: 27 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,12 @@ public AdornerLayer()
5050

5151
private void AdornerLayer_SizeChanged(object sender, SizeChangedEventArgs e)
5252
{
53-
foreach (var adorner in Children)
53+
foreach (var adornerXaml in Children)
5454
{
55-
if (adorner is Border border && border.Tag is FrameworkElement adornedElement)
55+
if (adornerXaml is Adorner adorner)
5656
{
57-
border.Width = adornedElement.ActualWidth;
58-
border.Height = adornedElement.ActualHeight;
59-
60-
var coord = this.CoordinatesTo(adornedElement);
61-
62-
Canvas.SetLeft(border, coord.X);
63-
Canvas.SetTop(border, coord.Y);
57+
// Notify each adorner that our general layout has updated.
58+
adorner.OnLayoutUpdated(null, EventArgs.Empty);
6459
}
6560
}
6661
}
@@ -229,38 +224,40 @@ private static async void XamlPropertyFrameworkElement_Loaded(object sender, Rou
229224
}
230225

231226
// TODO: Temp helper? Build into 'Adorner' base class?
232-
private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adorner)
227+
private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adornerXaml)
233228
{
234-
// Add adorner XAML content to the Adorner Layer
235-
236-
var border = new Border()
229+
if (adornerXaml is Adorner adorner)
237230
{
238-
Child = adorner,
239-
Width = adornedElement.ActualWidth, // TODO: Register/tie to size of element better for changes.
240-
Height = adornedElement.ActualHeight,
241-
HorizontalAlignment = HorizontalAlignment.Stretch,
242-
VerticalAlignment = VerticalAlignment.Stretch,
243-
Tag = adornedElement,
244-
};
245-
246-
var coord = layer.CoordinatesTo(adornedElement);
231+
// We already have an adorner type, use it directly.
232+
}
233+
else
234+
{
235+
adorner = new Adorner()
236+
{
237+
Content = adornerXaml,
238+
};
239+
}
247240

248-
Canvas.SetLeft(border, coord.X);
249-
Canvas.SetTop(border, coord.Y);
241+
// Add adorner XAML content to the Adorner Layer
242+
adorner.AdornerLayer = layer;
243+
adorner.AdornedElement = adornedElement;
250244

251-
layer.Children.Add(border);
245+
layer.Children.Add(adorner);
252246
}
253247

254-
private static void RemoveAdorner(AdornerLayer layer, UIElement adorner)
248+
private static void RemoveAdorner(AdornerLayer layer, UIElement adornerXaml)
255249
{
256-
var border = adorner.FindAscendant<Border>();
250+
var adorner = adornerXaml.FindAscendant<Adorner>();
257251

258-
if (border != null)
252+
if (adorner != null)
259253
{
260-
layer.Children.Remove(border);
254+
adorner.AdornedElement = null;
255+
adorner.AdornerLayer = null;
256+
257+
layer.Children.Remove(adorner);
261258

262259
#if !HAS_UNO
263-
VisualTreeHelper.DisconnectChildrenRecursive(border);
260+
VisualTreeHelper.DisconnectChildrenRecursive(adorner);
264261
#endif
265262
}
266263
}

components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
<!-- Rns suffix is required for namespaces shared across projects. See https://github.com/CommunityToolkit/Labs-Windows/issues/152 -->
99
<RootNamespace>CommunityToolkit.WinUI.Controls.AdornersRns</RootNamespace>
10+
<LangVersion>preview</LangVersion>
1011
</PropertyGroup>
1112

1213
<!-- Sets this up as a toolkit component's source project -->

components/Adorners/src/Dependencies.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@
1212
<!-- WinUI 2 / UWP / Uno -->
1313
<ItemGroup Condition="'$(IsUwp)' == 'true' OR ('$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '2')">
1414
<PackageReference Include="CommunityToolkit.Uwp.Extensions" Version="8.2.250402"/>
15+
<PackageReference Include="CommunityToolkit.Uwp.Helpers" Version="8.2.250402"/>
1516
</ItemGroup>
1617

1718
<!-- WinUI 3 / WinAppSdk / Uno -->
1819
<ItemGroup Condition="'$(IsWinAppSdk)' == 'true' OR ('$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3')">
1920
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402"/>
21+
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402"/>
2022
</ItemGroup>
2123
</Project>
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
1+
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
22
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
33
<ResourceDictionary.MergedDictionaries>
4+
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Adorners/Adorner.xaml" />
45
<ResourceDictionary Source="ms-appx:///CommunityToolkit.WinUI.Adorners/AdornerDecorator.xaml" />
56
</ResourceDictionary.MergedDictionaries>
67
</ResourceDictionary>
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
1-
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
1+
<!-- Licensed to the .NET Foundation under one or more agreements. The .NET Foundation licenses this file to you under the MIT license. See the LICENSE file in the project root for more information. -->
22
<Page x:Class="AdornersTests.ExampleAdornersTestPage"
33
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
44
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5-
xmlns:controls="using:CommunityToolkit.WinUI.Controls"
5+
xmlns:ui="using:CommunityToolkit.WinUI"
66
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
77
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
88
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
99
mc:Ignorable="d">
1010

1111
<Grid>
12-
<controls:AdornerLayer x:Name="AdornersControl" />
12+
<ui:AdornerLayer x:Name="AdornersControl" />
1313
</Grid>
1414
</Page>

0 commit comments

Comments
 (0)