Skip to content

Commit f37a46b

Browse files
Add an initial custom adorner sample
Provide OnAttached/OnDetached helper methods and Typed abstract class
1 parent 460c3f3 commit f37a46b

File tree

9 files changed

+306
-1
lines changed

9 files changed

+306
-1
lines changed

components/Adorners/samples/Adorners.Samples.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project>
1+
<Project>
22
<Import Project="$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))" Condition="Exists('$([MSBuild]::GetPathOfFileAbove(Directory.Build.props))')" />
33

44
<PropertyGroup>
55
<ToolkitComponentName>Adorners</ToolkitComponentName>
6+
<LangVersion>preview</LangVersion>
67
</PropertyGroup>
78

89
<!-- Sets this up as a toolkit component's sample project -->

components/Adorners/samples/Adorners.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ Adorners can be used in a variety of scenarios. For instance, if you wanted to h
4848

4949
> [!SAMPLE ElementHighlightAdornerSample]
5050
51+
The above examples highlights how adorners are sized and positioned directly atop the adorned element. This allows for relative positioning of elements within the context of the Adorner's visuals in relation to the Adorned Element itself.
52+
53+
## Custom Adorner Example
54+
55+
Adorners can be subclassed in order to encapsulate specific logic and/or styling for your scenario. For instance, you may want to create a custom Adorner that allows a user to edit a piece of text in place:
56+
57+
> [!SAMPLE InPlaceTextEditorAdornerSample]
58+
59+
Adorners are templated controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation.
60+
5161
## TODO: Resize Example
5262

5363
Another common use case for adorners is to allow a user to resize a visual element.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<ResourceDictionary x:Class="AdornersExperiment.Samples.InPlaceTextEditorAdornerResources"
2+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4+
xmlns:Windows11="http://schemas.microsoft.com/winfx/2006/xaml/presentation?IsApiContractPresent(Windows.Foundation.UniversalApiContract, 14)"
5+
xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
6+
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
7+
xmlns:local="using:AdornersExperiment.Samples">
8+
9+
<!-- Implicitly applied default style -->
10+
<Style BasedOn="{StaticResource DefaultInPlaceTextEditorAdornerStyle}"
11+
TargetType="local:InPlaceTextEditorAdorner" />
12+
13+
<Style x:Key="DefaultInPlaceTextEditorAdornerStyle"
14+
TargetType="local:InPlaceTextEditorAdorner">
15+
<Setter Property="Template">
16+
<Setter.Value>
17+
<ControlTemplate TargetType="local:InPlaceTextEditorAdorner">
18+
<ContentControl x:Name="ContentContainer"
19+
HorizontalContentAlignment="Stretch"
20+
VerticalContentAlignment="Stretch">
21+
<ContentControl.ContentTemplate>
22+
<DataTemplate x:DataType="local:InPlaceTextEditorAdorner">
23+
<Popup x:Name="PopupRoot"
24+
Windows11:DesiredPlacement="BottomEdgeAlignedLeft"
25+
Windows11:PlacementTarget="{x:Bind AdornedElement, Mode=OneWay}"
26+
IsLightDismissEnabled="False"
27+
IsOpen="{x:Bind IsPopupOpen, Mode=OneWay}">
28+
<Grid MinWidth="320"
29+
Margin="-4"
30+
Padding="4"
31+
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
32+
BorderBrush="{ThemeResource SurfaceStrokeColorFlyoutBrush}"
33+
ColumnSpacing="4"
34+
CornerRadius="8">
35+
<Grid.ColumnDefinitions>
36+
<ColumnDefinition Width="*" />
37+
<ColumnDefinition Width="Auto" />
38+
<ColumnDefinition Width="Auto" />
39+
</Grid.ColumnDefinitions>
40+
<TextBox Text="{x:Bind AdornedElement.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
41+
<interactivity:Interaction.Behaviors>
42+
<behaviors:AutoSelectBehavior />
43+
</interactivity:Interaction.Behaviors>
44+
</TextBox>
45+
<Button Grid.Column="1"
46+
Click="{x:Bind ConfirmButton_Click}">
47+
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
48+
Glyph="&#xE73E;" />
49+
</Button>
50+
<Button x:Name="CloseButton"
51+
Grid.Column="2"
52+
Click="{x:Bind CloseButton_Click}">
53+
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
54+
Glyph="&#xE711;" />
55+
</Button>
56+
</Grid>
57+
</Popup>
58+
</DataTemplate>
59+
</ContentControl.ContentTemplate>
60+
</ContentControl>
61+
</ControlTemplate>
62+
</Setter.Value>
63+
</Setter>
64+
</Style>
65+
</ResourceDictionary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace AdornersExperiment.Samples;
6+
7+
public sealed partial class InPlaceTextEditorAdornerResources : ResourceDictionary
8+
{
9+
// NOTICE
10+
// This file only exists to enable x:Bind in the resource dictionary.
11+
// Do not add code here.
12+
// Instead, add code-behind to your templated control.
13+
public InPlaceTextEditorAdornerResources()
14+
{
15+
this.InitializeComponent();
16+
}
17+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
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. -->
2+
<Page x:Class="AdornersExperiment.Samples.InPlaceTextEditorAdornerSample"
3+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:local="using:AdornersExperiment.Samples"
7+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
8+
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
9+
xmlns:ui="using:CommunityToolkit.WinUI"
10+
mc:Ignorable="d">
11+
12+
<Page.Resources>
13+
<local:InPlaceTextEditorAdornerResources />
14+
<!-- Style copied here from above for convenience of the sample
15+
<Style x:Key="DefaultInPlaceTextEditorAdornerStyle"
16+
TargetType="local:InPlaceTextEditorAdorner">
17+
<Setter Property="Template">
18+
<Setter.Value>
19+
<ControlTemplate TargetType="local:InPlaceTextEditorAdorner">
20+
<ContentControl x:Name="ContentContainer"
21+
HorizontalContentAlignment="Stretch"
22+
VerticalContentAlignment="Stretch">
23+
<ContentControl.ContentTemplate>
24+
<DataTemplate x:DataType="local:InPlaceTextEditorAdorner">
25+
<Popup x:Name="PopupRoot"
26+
Windows11:DesiredPlacement="BottomEdgeAlignedLeft"
27+
Windows11:PlacementTarget="{x:Bind AdornedElement, Mode=OneWay}"
28+
IsLightDismissEnabled="False"
29+
IsOpen="{x:Bind IsPopupOpen, Mode=OneWay}">
30+
<Grid MinWidth="320"
31+
Margin="-4"
32+
Padding="4"
33+
Background="{ThemeResource AcrylicInAppFillColorDefaultBrush}"
34+
BorderBrush="{ThemeResource SurfaceStrokeColorFlyoutBrush}"
35+
ColumnSpacing="4"
36+
CornerRadius="8">
37+
<Grid.ColumnDefinitions>
38+
<ColumnDefinition Width="*" />
39+
<ColumnDefinition Width="Auto" />
40+
<ColumnDefinition Width="Auto" />
41+
</Grid.ColumnDefinitions>
42+
<TextBox Text="{x:Bind AdornedElement.Text, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
43+
<interactivity:Interaction.Behaviors>
44+
<behaviors:AutoSelectBehavior />
45+
</interactivity:Interaction.Behaviors>
46+
</TextBox>
47+
<Button Grid.Column="1"
48+
Click="{x:Bind ConfirmButton_Click}">
49+
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
50+
Glyph="&#xE73E;" />
51+
</Button>
52+
<Button x:Name="CloseButton"
53+
Grid.Column="2"
54+
Click="{x:Bind CloseButton_Click}">
55+
<FontIcon FontFamily="{ThemeResource SymbolThemeFontFamily}"
56+
Glyph="&#xE711;" />
57+
</Button>
58+
</Grid>
59+
</Popup>
60+
</DataTemplate>
61+
</ContentControl.ContentTemplate>
62+
</ContentControl>
63+
</ControlTemplate>
64+
</Setter.Value>
65+
</Setter>
66+
</Style>
67+
-->
68+
</Page.Resources>
69+
70+
<StackPanel Spacing="8">
71+
<TextBlock Text="Editable Text:" />
72+
73+
<TextBlock Text="Hello, World!">
74+
<ui:AdornerLayer.Xaml>
75+
<!-- Style manually set here as local to example -->
76+
<local:InPlaceTextEditorAdorner Style="{StaticResource DefaultInPlaceTextEditorAdornerStyle}"/>
77+
</ui:AdornerLayer.Xaml>
78+
</TextBlock>
79+
</StackPanel>
80+
</Page>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using CommunityToolkit.WinUI;
6+
using Windows.Foundation.Metadata;
7+
8+
namespace AdornersExperiment.Samples;
9+
10+
[ToolkitSample(id: nameof(InPlaceTextEditorAdornerSample), "In place text editor Adorner", description: "A sample for showing how add a popup TextBox component via an Adorner of a TextBlock.")]
11+
public sealed partial class InPlaceTextEditorAdornerSample : Page
12+
{
13+
public InPlaceTextEditorAdornerSample()
14+
{
15+
this.InitializeComponent();
16+
}
17+
}
18+
19+
public sealed partial class InPlaceTextEditorAdorner : Adorner<TextBlock>
20+
{
21+
/// <summary>
22+
/// Gets or sets whether the popup is open.
23+
/// </summary>
24+
public bool IsPopupOpen
25+
{
26+
get { return (bool)GetValue(IsPopupOpenProperty); }
27+
set { SetValue(IsPopupOpenProperty, value); }
28+
}
29+
30+
/// <summary>
31+
/// Identifies the <see cref="IsPopupOpen"/> dependency property.
32+
/// </summary>
33+
public static readonly DependencyProperty IsPopupOpenProperty =
34+
DependencyProperty.Register("IsPopupOpen", typeof(bool), typeof(InPlaceTextEditorAdorner), new PropertyMetadata(false));
35+
36+
private string _originalText = string.Empty;
37+
38+
public InPlaceTextEditorAdorner()
39+
{
40+
this.DefaultStyleKey = typeof(InPlaceTextEditorAdorner);
41+
42+
// Uno workaround
43+
DataContext = this;
44+
}
45+
46+
protected override void OnApplyTemplate()
47+
{
48+
base.OnApplyTemplate();
49+
}
50+
51+
protected override void OnAttached()
52+
{
53+
base.OnAttached();
54+
55+
AdornedElement?.Tapped += AdornedElement_Tapped;
56+
}
57+
58+
protected override void OnDetaching()
59+
{
60+
base.OnDetaching();
61+
62+
AdornedElement?.Tapped -= AdornedElement_Tapped;
63+
}
64+
65+
private void AdornedElement_Tapped(object sender, TappedRoutedEventArgs e)
66+
{
67+
_originalText = AdornedElement?.Text ?? string.Empty;
68+
IsPopupOpen = true;
69+
}
70+
71+
public void ConfirmButton_Click(object sender, RoutedEventArgs e)
72+
{
73+
IsPopupOpen = false;
74+
}
75+
76+
public void CloseButton_Click(object sender, RoutedEventArgs e)
77+
{
78+
AdornedElement?.Text = _originalText;
79+
IsPopupOpen = false;
80+
}
81+
}

components/Adorners/src/Adorner.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue)
7474
OnDetachAction = (weakEventListener) => newfe.Unloaded -= weakEventListener.OnEvent // Use Local References Only
7575
};
7676
newfe.Unloaded += weakPropertyChangedListenerUnloaded.OnEvent;
77+
78+
OnAttached();
7779
}
7880
}
7981

@@ -102,6 +104,8 @@ private void OnUnloaded(object source, RoutedEventArgs eventArgs)
102104
{
103105
if (AdornerLayer is null) return;
104106

107+
OnDetaching();
108+
105109
AdornerLayer.RemoveAdorner(AdornerLayer, this);
106110
}
107111

@@ -114,4 +118,20 @@ public Adorner()
114118
{
115119
this.DefaultStyleKey = typeof(Adorner);
116120
}
121+
122+
/// <summary>
123+
/// Called after the <see cref="Adorner"/> is attached to the <see cref="AdornedElement"/>.
124+
/// </summary>
125+
/// <remarks>
126+
/// Override this method in a subclass to initiate functionality of the <see cref="Adorner"/>.
127+
/// </remarks>
128+
protected virtual void OnAttached() { }
129+
130+
/// <summary>
131+
/// Called when the <see cref="Adorner"/> is being detached from the <see cref="AdornedElement"/>.
132+
/// </summary>
133+
/// <remarks>
134+
/// Override this method to unhook functionality from the <see cref="AdornedElement"/>.
135+
/// </remarks>
136+
protected virtual void OnDetaching() { }
117137
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
namespace CommunityToolkit.WinUI;
6+
7+
/// <summary>
8+
/// A base class for <see cref="Adorner"/>s allowing for explicit types.
9+
/// </summary>
10+
/// <typeparam name="T">The object type to attach to</typeparam>
11+
public abstract class Adorner<T> : Adorner where T : UIElement
12+
{
13+
/// <inheritdoc/>
14+
public new T? AdornedElement
15+
{
16+
get { return base.AdornedElement as T; }
17+
}
18+
19+
/// <inheritdoc/>
20+
protected override void OnAttached()
21+
{
22+
base.OnAttached();
23+
24+
if (this.AdornedElement is null)
25+
{
26+
throw new InvalidOperationException($"AdornedElement {base.AdornedElement?.GetType().FullName} is not of type {typeof(T).FullName}");
27+
}
28+
}
29+
}

components/Adorners/src/Dependencies.props

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

1819
<!-- WinUI 3 / WinAppSdk / Uno -->
1920
<ItemGroup Condition="'$(IsWinAppSdk)' == 'true' OR ('$(IsUno)' == 'true' AND '$(WinUIMajorVersion)' == '3')">
21+
<PackageReference Include="CommunityToolkit.WinUI.Behaviors" Version="8.2.250402"/>
2022
<PackageReference Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402"/>
2123
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="8.2.250402"/>
2224
</ItemGroup>

0 commit comments

Comments
 (0)