diff --git a/components/Adorners/OpenSolution.bat b/components/Adorners/OpenSolution.bat
new file mode 100644
index 000000000..814a56d4b
--- /dev/null
+++ b/components/Adorners/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/Adorners/samples/Adorners.Samples.csproj b/components/Adorners/samples/Adorners.Samples.csproj
new file mode 100644
index 000000000..b721e9f86
--- /dev/null
+++ b/components/Adorners/samples/Adorners.Samples.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ Adorners
+ preview
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/Adorners.md b/components/Adorners/samples/Adorners.md
new file mode 100644
index 000000000..5553b4e0e
--- /dev/null
+++ b/components/Adorners/samples/Adorners.md
@@ -0,0 +1,87 @@
+---
+title: Adorners
+author: michael-hawker
+description: Adorners let you overlay content on top of your XAML components in a separate layer on top of everything else.
+keywords: Adorners, Control, Layout
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+discussion-id: 278
+issue-id: 0
+icon: assets/icon.png
+---
+
+# Adorners
+
+Adorners allow a developer to overlay any content on top of another UI element in a separate layer that resides on top of everything else.
+
+## Background
+
+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.
+
+### Without Adorners
+
+Imagine a scenario where you have a button or tab that checks a user's e-mail, and you'd like it to display the number of new e-mails that have arrived.
+
+You could try and incorporate a [`InfoBadge`](https://learn.microsoft.com/windows/apps/design/controls/info-badge) into your Visual Tree in order to display this as part of your icon, but that requires you to modify quite a bit of your content, as in this example:
+
+> [!SAMPLE InfoBadgeWithoutAdorner]
+
+It also, by default, gets confined to the perimeter of the button and clipped, as seen above.
+
+### With Adorners
+
+However, with an Adorner instead, you can abstract this behavior from the content of your control. You can even more easily place the notification outside the bounds of the original element, like so:
+
+> [!SAMPLE AdornersInfoBadgeSample]
+
+You can see how Adorners react to more dynamic content with this more complete example here:
+
+> [!SAMPLE AdornersTabBadgeSample]
+
+The above example shows how to leverage XAML animations and data binding alongside the XAML-based Adorner with a `TabViewItem` which can also move or disappear.
+
+## Highlight Example
+
+Adorners can be used in a variety of scenarios. For instance, if you wanted to highlight an element and show it's alignment to other elements in a creativity app:
+
+> [!SAMPLE ElementHighlightAdornerSample]
+
+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.
+
+## Custom Adorner Example
+
+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 click and edit a piece of text in place.
+The following example uses `IEditableObject` to control the editing lifecycle coordinated with a typical MVVM pattern binding:
+
+> [!SAMPLE InPlaceTextEditorAdornerSample]
+
+Adorners are template-based controls, but you can use a class-backed resource dictionary to better enable usage of x:Bind for easier creation and binding to the `AdornedElement`, as seen here.
+
+## Input Validation Example
+
+The custom adorner example above can be further extended to provide input validation feedback to the user using the standard `INotifyDataErrorInfo` interface.
+We use the `ObservableValidator` class from the `CommunityToolkit.Mvvm` package to provide validation rules for our view model properties.
+When the user submits invalid input, the adorner displays a red border around the text box and shows a tooltip with the validation error message:
+
+> [!SAMPLE InputValidationAdornerSample]
+
+## TODO: Resize Example
+
+Another common use case for adorners is to allow a user to resize a visual element.
+
+// TODO: Make an example here for this w/ custom Adorner class...
+
+## Migrating from WPF
+
+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.
+
+### Concepts
+
+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.
+
+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.
+
+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.
diff --git a/components/Adorners/samples/AdornersInfoBadgeSample.xaml b/components/Adorners/samples/AdornersInfoBadgeSample.xaml
new file mode 100644
index 000000000..3d6b8468e
--- /dev/null
+++ b/components/Adorners/samples/AdornersInfoBadgeSample.xaml
@@ -0,0 +1,24 @@
+
+
+
+
+
diff --git a/components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs b/components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs
new file mode 100644
index 000000000..2c5a47783
--- /dev/null
+++ b/components/Adorners/samples/AdornersInfoBadgeSample.xaml.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace AdornersExperiment.Samples;
+
+[ToolkitSampleBoolOption("IsAdornerVisible", true, Title = "Is Adorner Visible")]
+
+[ToolkitSample(id: nameof(AdornersInfoBadgeSample), "InfoBadge w/ Adorner", description: "A sample for showing how add an infobadge to a component via an Adorner.")]
+public sealed partial class AdornersInfoBadgeSample : Page
+{
+ public AdornersInfoBadgeSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml b/components/Adorners/samples/AdornersTabBadgeSample.xaml
new file mode 100644
index 000000000..261319959
--- /dev/null
+++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs
new file mode 100644
index 000000000..a4c0f5ad1
--- /dev/null
+++ b/components/Adorners/samples/AdornersTabBadgeSample.xaml.cs
@@ -0,0 +1,22 @@
+// 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.
+
+namespace AdornersExperiment.Samples;
+
+[ToolkitSampleBoolOption("IsAdornerVisible", true, Title = "Is Adorner Visible")]
+[ToolkitSampleNumericOption("BadgeValue", 3, 1, 5, 1, true, Title = "Badge Value")]
+
+[ToolkitSample(id: nameof(AdornersTabBadgeSample), "InfoBadge w/ Adorner in TabView", description: "A sample for showing how add an InfoBadge to a TabViewItem via an Adorner.")]
+public sealed partial class AdornersTabBadgeSample : Page
+{
+ public AdornersTabBadgeSample()
+ {
+ this.InitializeComponent();
+ }
+
+ private void TabView_TabCloseRequested(MUXC.TabView sender, MUXC.TabViewTabCloseRequestedEventArgs args)
+ {
+ sender.TabItems.Remove(args.Tab);
+ }
+}
diff --git a/components/Adorners/samples/Assets/icon.png b/components/Adorners/samples/Assets/icon.png
new file mode 100644
index 000000000..8435bcaa9
Binary files /dev/null and b/components/Adorners/samples/Assets/icon.png differ
diff --git a/components/Adorners/samples/Dependencies.props b/components/Adorners/samples/Dependencies.props
new file mode 100644
index 000000000..4a797a706
--- /dev/null
+++ b/components/Adorners/samples/Dependencies.props
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/ElementHighlightAdornerSample.xaml b/components/Adorners/samples/ElementHighlightAdornerSample.xaml
new file mode 100644
index 000000000..048991b91
--- /dev/null
+++ b/components/Adorners/samples/ElementHighlightAdornerSample.xaml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs b/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs
new file mode 100644
index 000000000..6d60cd912
--- /dev/null
+++ b/components/Adorners/samples/ElementHighlightAdornerSample.xaml.cs
@@ -0,0 +1,19 @@
+// 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.
+
+namespace AdornersExperiment.Samples;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+[ToolkitSampleBoolOption("IsAdornerVisible", false, Title = "Is Adorner Visible")]
+
+[ToolkitSample(id: nameof(ElementHighlightAdornerSample), "Highlighting an Element w/ Adorner", description: "A sample for showing how to highlight an element's bounds with an Adorner.")]
+public sealed partial class ElementHighlightAdornerSample : Page
+{
+ public ElementHighlightAdornerSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml
new file mode 100644
index 000000000..815b0739c
--- /dev/null
+++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml.cs b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml.cs
new file mode 100644
index 000000000..9a202dc0d
--- /dev/null
+++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdorner.xaml.cs
@@ -0,0 +1,17 @@
+// 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.
+
+namespace AdornersExperiment.Samples.InPlaceTextEditor;
+
+public sealed partial class InPlaceTextEditorAdornerResources : ResourceDictionary
+{
+ // NOTICE
+ // This file only exists to enable x:Bind in the resource dictionary.
+ // Do not add code here.
+ // Instead, add code-behind to your templated control.
+ public InPlaceTextEditorAdornerResources()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml
new file mode 100644
index 000000000..fd336138f
--- /dev/null
+++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs
new file mode 100644
index 000000000..e50298789
--- /dev/null
+++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs
@@ -0,0 +1,135 @@
+// 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.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.WinUI;
+
+namespace AdornersExperiment.Samples.InPlaceTextEditor;
+
+[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.")]
+public sealed partial class InPlaceTextEditorAdornerSample : Page
+{
+ public MyViewModel ViewModel { get; } = new();
+
+ public InPlaceTextEditorAdornerSample()
+ {
+ this.InitializeComponent();
+ }
+}
+
+///
+/// ViewModel that shows using in conjunction with an Adorner.
+///
+public partial class MyViewModel : ObservableObject, IEditableObject
+{
+ [ObservableProperty]
+ public partial string MyText { get; set; } = "Hello, World!";
+
+ bool _isEditing = false;
+ private string _backupText = string.Empty;
+
+ public void BeginEdit()
+ {
+ if (!_isEditing)
+ {
+ _backupText = MyText;
+ _isEditing = true;
+ }
+ }
+
+ public void CancelEdit()
+ {
+ if (_isEditing)
+ {
+ MyText = _backupText;
+ _isEditing = false;
+ }
+ }
+
+ public void EndEdit()
+ {
+ if (_isEditing)
+ {
+ _backupText = MyText;
+ _isEditing = false;
+ }
+ }
+}
+
+///
+/// An Adorner that shows a popup TextBox for editing a TextBlock's text.
+/// If that TextBlock's DataContext implements ,
+/// it will be used to manage the editing session.
+///
+public sealed partial class InPlaceTextEditorAdorner : Adorner
+{
+ ///
+ /// Gets or sets whether the popup is open.
+ ///
+ public bool IsPopupOpen
+ {
+ get { return (bool)GetValue(IsPopupOpenProperty); }
+ set { SetValue(IsPopupOpenProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty IsPopupOpenProperty =
+ DependencyProperty.Register(nameof(IsPopupOpen), typeof(bool), typeof(InPlaceTextEditorAdorner), new PropertyMetadata(false));
+
+ public InPlaceTextEditorAdorner()
+ {
+ this.DefaultStyleKey = typeof(InPlaceTextEditorAdorner);
+
+ // Uno workaround
+ DataContext = this;
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ }
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ AdornedElement?.Tapped += AdornedElement_Tapped;
+ }
+
+ protected override void OnDetaching()
+ {
+ base.OnDetaching();
+
+ AdornedElement?.Tapped -= AdornedElement_Tapped;
+ }
+
+ private void AdornedElement_Tapped(object sender, TappedRoutedEventArgs e)
+ {
+ if (AdornedElement?.DataContext is IEditableObject editableObject)
+ {
+ editableObject.BeginEdit();
+ }
+ IsPopupOpen = true;
+ }
+
+ public void ConfirmButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (AdornedElement?.DataContext is IEditableObject editableObject)
+ {
+ editableObject.EndEdit();
+ }
+ IsPopupOpen = false;
+ }
+
+ public void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (AdornedElement?.DataContext is IEditableObject editableObject)
+ {
+ editableObject.CancelEdit();
+ }
+ IsPopupOpen = false;
+ }
+}
diff --git a/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml
new file mode 100644
index 000000000..31bbe7796
--- /dev/null
+++ b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml
@@ -0,0 +1,23 @@
+
+
+
+
+
diff --git a/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs
new file mode 100644
index 000000000..247603cdb
--- /dev/null
+++ b/components/Adorners/samples/InfoBadgeWithoutAdorner.xaml.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace AdornersExperiment.Samples;
+
+[ToolkitSampleBoolOption("IsNotificationVisible", true, Title = "Is Notification Visible")]
+
+[ToolkitSample(id: nameof(InfoBadgeWithoutAdorner), "InfoBadge w/o Adorner", description: "A sample for showing how one adds an infobadge to a component without an Adorner (from WinUI Gallery app).")]
+public sealed partial class InfoBadgeWithoutAdorner : Page
+{
+ public InfoBadgeWithoutAdorner()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml
new file mode 100644
index 000000000..e23bf4c23
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs
new file mode 100644
index 000000000..fc6a0a51b
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdorner.xaml.cs
@@ -0,0 +1,17 @@
+// 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.
+
+namespace AdornersExperiment.Samples.InputValidation;
+
+public sealed partial class InputValidationAdornerResources : ResourceDictionary
+{
+ // NOTICE
+ // This file only exists to enable x:Bind in the resource dictionary.
+ // Do not add code here.
+ // Instead, add code-behind to your templated control.
+ public InputValidationAdornerResources()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml
new file mode 100644
index 000000000..2bd0859fe
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs
new file mode 100644
index 000000000..23813af5d
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs
@@ -0,0 +1,194 @@
+// 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.
+
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using CommunityToolkit.WinUI;
+
+using System.ComponentModel.DataAnnotations;
+
+using INotifyDataErrorInfo = System.ComponentModel.INotifyDataErrorInfo;
+using DataErrorsChangedEventArgs = System.ComponentModel.DataErrorsChangedEventArgs;
+
+namespace AdornersExperiment.Samples.InputValidation;
+
+[ToolkitSample(id: nameof(InputValidationAdornerSample), "Input Validation Adorner", description: "A sample for showing how to use an Adorner for any Input Validation with INotifyDataErrorInfo.")]
+public sealed partial class InputValidationAdornerSample : Page
+{
+ public ValidationFormWidgetViewModel ViewModel { get; } = new();
+
+ public InputValidationAdornerSample()
+ {
+ this.InitializeComponent();
+ }
+}
+
+///
+/// ViewModel that shows using in conjunction with an Adorner.
+/// Via the base class from the MVVM Toolkit.
+/// Example modified from the MVVM Toolkit Sample App.
+///
+public partial class ValidationFormWidgetViewModel : ObservableValidator
+{
+ public event EventHandler? FormSubmissionCompleted;
+ public event EventHandler? FormSubmissionFailed;
+
+ [ObservableProperty]
+ [Required]
+ [MinLength(2)]
+ [MaxLength(100)]
+ public partial string? FirstName { get; set; }
+
+ [ObservableProperty]
+ [Required]
+ [MinLength(2)]
+ [MaxLength(100)]
+ public partial string? LastName { get; set; }
+
+ [ObservableProperty]
+ [Required]
+ [EmailAddress]
+ public partial string? Email { get; set; }
+
+ [ObservableProperty]
+ [Required]
+ [Phone]
+ public partial string? PhoneNumber { get; set; }
+
+ [ObservableProperty]
+ [Required]
+ [Range(13, 120)]
+ public partial int Age { get; set; }
+
+ [RelayCommand]
+ private void Submit()
+ {
+ ValidateAllProperties();
+
+ if (HasErrors)
+ {
+ FormSubmissionFailed?.Invoke(this, EventArgs.Empty);
+ }
+ else
+ {
+ FormSubmissionCompleted?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
+
+///
+/// An Adorner that shows an error message if Data Validation fails.
+/// The adorned control's must implement . It assumes that the return of is a collection.
+///
+public sealed partial class InputValidationAdorner : Adorner
+{
+ ///
+ /// Gets or sets the name of the property this adorner should look for errors on.
+ ///
+ public string PropertyName
+ {
+ get { return (string)GetValue(PropertyNameProperty); }
+ set { SetValue(PropertyNameProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty PropertyNameProperty =
+ DependencyProperty.Register(nameof(PropertyName), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null));
+
+ ///
+ /// Gets or sets whether the popup is open.
+ ///
+ public bool HasValidationFailed
+ {
+ get { return (bool)GetValue(HasValidationFailedProperty); }
+ set { SetValue(HasValidationFailedProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty HasValidationFailedProperty =
+ DependencyProperty.Register(nameof(HasValidationFailed), typeof(bool), typeof(InputValidationAdorner), new PropertyMetadata(false));
+
+ ///
+ /// Gets or sets the validation message for this failed property.
+ ///
+ public string ValidationMessage
+ {
+ get { return (string)GetValue(ValidationMessageProperty); }
+ set { SetValue(ValidationMessageProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ValidationMessageProperty =
+ DependencyProperty.Register(nameof(ValidationMessage), typeof(string), typeof(InputValidationAdorner), new PropertyMetadata(null));
+
+ private INotifyDataErrorInfo? _dataErrorInfo;
+
+ public InputValidationAdorner()
+ {
+ this.DefaultStyleKey = typeof(InputValidationAdorner);
+
+ // Uno workaround
+ DataContext = this;
+ }
+
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ }
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ if (AdornedElement?.DataContext is INotifyDataErrorInfo iError)
+ {
+ _dataErrorInfo = iError;
+ _dataErrorInfo.ErrorsChanged += this.INotifyDataErrorInfo_ErrorsChanged;
+ }
+ }
+
+ private void INotifyDataErrorInfo_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e)
+ {
+ if (_dataErrorInfo is not null)
+ {
+ // Reset state
+ if (!_dataErrorInfo.HasErrors)
+ {
+ HasValidationFailed = false;
+ ValidationMessage = string.Empty;
+ return;
+ }
+
+ if (e.PropertyName == PropertyName)
+ {
+ HasValidationFailed = true;
+
+ StringBuilder message = new();
+ foreach (ValidationResult result in _dataErrorInfo.GetErrors(e.PropertyName))
+ {
+ message.AppendLine(result.ErrorMessage);
+ }
+
+ ValidationMessage = message.ToString().Trim();
+ }
+ }
+ }
+
+ protected override void OnDetaching()
+ {
+ if (_dataErrorInfo is not null)
+ {
+ _dataErrorInfo.ErrorsChanged -= this.INotifyDataErrorInfo_ErrorsChanged;
+ _dataErrorInfo = null;
+ }
+
+ base.OnDetaching();
+ }
+}
diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs
new file mode 100644
index 000000000..fa94415cf
--- /dev/null
+++ b/components/Adorners/src/Adorner.cs
@@ -0,0 +1,137 @@
+// 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.
+
+using CommunityToolkit.WinUI.Helpers;
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// A class which represents a that decorates a .
+///
+///
+/// An adorner is a custom element which is bound to a specific and can
+/// provide additional visual cues to the user. Adorners are rendered in an
+/// , a special layer that is on top of the adorned element or a collection
+/// of adorned elements. Rendering of an adorner is independent of the UIElement it is bound to. An
+/// adorner is typically positioned relative to the element it is bound to based on the upper-left
+/// coordinate origin of the adorned element.
+///
+/// Note: The parent of an is always an and not the element being adorned.
+///
+public partial class Adorner : ContentControl
+{
+ ///
+ /// Gets the element being adorned by this .
+ ///
+ public UIElement? AdornedElement
+ {
+ get;
+ internal set
+ {
+ var oldvalue = field;
+ field = value;
+ OnAdornedElementChanged(oldvalue, value);
+ }
+ }
+
+ private void OnAdornedElementChanged(UIElement? oldvalue, UIElement? newvalue)
+ {
+ if (oldvalue is not null
+ && oldvalue is FrameworkElement oldfe)
+ {
+ // TODO: Should we explicitly detach the WEL here?
+ }
+
+ if (newvalue is not null
+ && newvalue is FrameworkElement newfe)
+ {
+ // Track changes to the AdornedElement's size
+ var weakPropertyChangedListenerSize = new WeakEventListener(this)
+ {
+ OnEventAction = static (instance, source, eventArgs) => instance.OnSizeChanged(source, eventArgs),
+ OnDetachAction = (weakEventListener) => newfe.SizeChanged -= weakEventListener.OnEvent // Use Local References Only
+ };
+ newfe.SizeChanged += weakPropertyChangedListenerSize.OnEvent;
+
+ // Track changes to the AdornedElement's layout
+ // Note: This is pretty spammy, thinking we don't need this?
+ /*var weakPropertyChangedListenerLayout = new WeakEventListener(this)
+ {
+ OnEventAction = static (instance, source, eventArgs) => instance.OnLayoutUpdated(source, eventArgs),
+ OnDetachAction = (weakEventListener) => newfe.LayoutUpdated -= weakEventListener.OnEvent // Use Local References Only
+ };
+ newfe.LayoutUpdated += weakPropertyChangedListenerLayout.OnEvent;*/
+
+ // Initial size & layout update
+ OnSizeChanged(null, null!);
+ OnLayoutUpdated(null, null!);
+
+ // Track if AdornedElement is unloaded
+ var weakPropertyChangedListenerUnloaded = new WeakEventListener(this)
+ {
+ OnEventAction = static (instance, source, eventArgs) => instance.OnUnloaded(source, eventArgs),
+ OnDetachAction = (weakEventListener) => newfe.Unloaded -= weakEventListener.OnEvent // Use Local References Only
+ };
+ newfe.Unloaded += weakPropertyChangedListenerUnloaded.OnEvent;
+
+ OnAttached();
+ }
+ }
+
+ private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ if (AdornedElement is null) return;
+
+ Width = AdornedElement.ActualSize.X;
+ Height = AdornedElement.ActualSize.Y;
+ }
+
+ internal void OnLayoutUpdated(object? sender, object e)
+ {
+ // Note: Also called by the parent AdornerLayer when its size changes
+ if (AdornerLayer is not null
+ && AdornedElement is not null)
+ {
+ var coord = AdornerLayer.CoordinatesTo(AdornedElement);
+
+ Canvas.SetLeft(this, coord.X);
+ Canvas.SetTop(this, coord.Y);
+ }
+ }
+
+ private void OnUnloaded(object source, RoutedEventArgs eventArgs)
+ {
+ if (AdornerLayer is null) return;
+
+ OnDetaching();
+
+ AdornerLayer.RemoveAdorner(AdornerLayer, this);
+ }
+
+ internal AdornerLayer? AdornerLayer { get; set; }
+
+ ///
+ /// Constructs a new instance of .
+ ///
+ public Adorner()
+ {
+ this.DefaultStyleKey = typeof(Adorner);
+ }
+
+ ///
+ /// Called after the is attached to the .
+ ///
+ ///
+ /// Override this method in a subclass to initiate functionality of the .
+ ///
+ protected virtual void OnAttached() { }
+
+ ///
+ /// Called when the is being detached from the .
+ ///
+ ///
+ /// Override this method to unhook functionality from the .
+ ///
+ protected virtual void OnDetaching() { }
+}
diff --git a/components/Adorners/src/Adorner.xaml b/components/Adorners/src/Adorner.xaml
new file mode 100644
index 000000000..09bea3a0d
--- /dev/null
+++ b/components/Adorners/src/Adorner.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
diff --git a/components/Adorners/src/AdornerDecorator.cs b/components/Adorners/src/AdornerDecorator.cs
new file mode 100644
index 000000000..f82ac7f19
--- /dev/null
+++ b/components/Adorners/src/AdornerDecorator.cs
@@ -0,0 +1,51 @@
+// 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.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// Helper class to hold content with an . Use this to wrap another and direct where the should sit. This class is helpful to constrain the or in cases where an appropriate location for the layer can't be automatically determined.
+///
+[TemplatePart(Name = PartAdornerLayer, Type = typeof(AdornerLayer))]
+[ContentProperty(Name = nameof(Child))]
+public sealed partial class AdornerDecorator : Control
+{
+ private const string PartAdornerLayer = "AdornerLayer";
+
+ ///
+ /// Gets or sets the single child element of the .
+ ///
+ public UIElement Child
+ {
+ get { return (UIElement)GetValue(ContentProperty); }
+ set { SetValue(ContentProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty ContentProperty =
+ DependencyProperty.Register(nameof(Child), typeof(UIElement), typeof(AdornerDecorator), new PropertyMetadata(null));
+
+ ///
+ /// Gets the contained within this .
+ ///
+ internal AdornerLayer? AdornerLayer { get; private set; }
+
+ ///
+ /// Constructs a new instance of .
+ ///
+ public AdornerDecorator()
+ {
+ this.DefaultStyleKey = typeof(AdornerDecorator);
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+
+ AdornerLayer = GetTemplateChild(PartAdornerLayer) as AdornerLayer;
+ }
+}
diff --git a/components/Adorners/src/AdornerDecorator.xaml b/components/Adorners/src/AdornerDecorator.xaml
new file mode 100644
index 000000000..05dbd08ba
--- /dev/null
+++ b/components/Adorners/src/AdornerDecorator.xaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
diff --git a/components/Adorners/src/AdornerLayer.cs b/components/Adorners/src/AdornerLayer.cs
new file mode 100644
index 000000000..2ef9410ff
--- /dev/null
+++ b/components/Adorners/src/AdornerLayer.cs
@@ -0,0 +1,264 @@
+// 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.
+
+using CommunityToolkit.WinUI.Future;
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// An adornment layer which can hold content to show on top of other components.
+/// If none is specified, one will be injected into your app content for you.
+/// If a suitable location can't be automatically found, you can also use an
+/// to specify where the should be placed.
+///
+public partial class AdornerLayer : Canvas
+{
+ ///
+ /// Gets the of a . Use this to retrieve any attached adorner from another .
+ ///
+ /// The to retrieve the adorner from.
+ /// The attached as an adorner.
+ public static UIElement GetXaml(FrameworkElement obj)
+ {
+ return (UIElement)obj.GetValue(XamlProperty);
+ }
+
+ ///
+ /// Sets the of a . Use this to attach any as an adorner to another . Requires that an is available in the visual tree above the adorned element.
+ ///
+ /// The to adorn.
+ /// The to attach as an adorner.
+ public static void SetXaml(FrameworkElement obj, UIElement value)
+ {
+ obj.SetValue(XamlProperty, value);
+ }
+
+ ///
+ /// Identifies the Xaml Attached Property.
+ ///
+ public static readonly DependencyProperty XamlProperty =
+ DependencyProperty.RegisterAttached("Xaml", typeof(UIElement), typeof(AdornerLayer), new PropertyMetadata(null, OnXamlPropertyChanged));
+
+ ///
+ /// Constructs a new instance of .
+ ///
+ public AdornerLayer()
+ {
+ SizeChanged += AdornerLayer_SizeChanged;
+ }
+
+ private void AdornerLayer_SizeChanged(object sender, SizeChangedEventArgs e)
+ {
+ foreach (var adornerXaml in Children)
+ {
+ if (adornerXaml is Adorner adorner)
+ {
+ // Notify each adorner that our general layout has updated.
+ adorner.OnLayoutUpdated(null, EventArgs.Empty);
+ }
+ }
+ }
+
+ private static async void OnXamlPropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)
+ {
+ if (dependencyObject is FrameworkElement fe)
+ {
+ if (!fe.IsLoaded || fe.Parent is null)
+ {
+ fe.Loaded += XamlPropertyFrameworkElement_Loaded;
+ }
+ else if (args.NewValue is UIElement adorner)
+ {
+ var layer = await GetAdornerLayerAsync(fe);
+
+ if (layer is not null)
+ {
+ AttachAdorner(layer, fe, adorner);
+ }
+ }
+ else if (args.NewValue == null && args.OldValue is UIElement oldAdorner)
+ {
+ var layer = await GetAdornerLayerAsync(fe);
+
+ if (layer is not null)
+ {
+ RemoveAdorner(layer, oldAdorner);
+ }
+ }
+ }
+ }
+
+ private static async void XamlPropertyFrameworkElement_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement fe)
+ {
+ fe.Loaded -= XamlPropertyFrameworkElement_Loaded;
+
+ var layer = await GetAdornerLayerAsync(fe);
+
+ if (layer is not null)
+ {
+ var adorner = GetXaml(fe);
+
+ if (adorner == null) return;
+
+ AttachAdorner(layer, fe, adorner);
+ }
+ }
+ }
+
+ ///
+ /// Retrieves the closest (or creates an) for the given element. If awaited, the retrieved adorner layer is guaranteed to be loaded. This is to assist adorners with being able to be positioned in relation to the loaded element.
+ /// There may be multiple s within an application, as each should have one to enable relational scrolling along content that may be outside of the viewport.
+ ///
+ /// Element to adorn.
+ /// Loaded responsible for that element.
+ public static async Task GetAdornerLayerAsync(FrameworkElement adornedElement)
+ {
+ // 1. Find Adorner Layer for element or top-most element
+ FrameworkElement? lastElement = null;
+
+ var adornerLayerOrTopMostElement = adornedElement.FindAscendant((element) =>
+ {
+ lastElement = element; // TODO: should this be after our if, does it matter?
+
+ if (element is AdornerDecorator)
+ {
+ return true;
+ }
+ else if (element is AdornerLayer)
+ {
+ return true;
+ }
+ else if (element is ScrollViewer)
+ {
+ return true;
+ }
+ // TODO: Need to figure out porting new DO toolkit helpers to Uno, only needed for custom adorner layer placement...
+ /*else
+ {
+ // TODO: Use BreadthFirst Search w/ Depth Limited?
+ var child = element.FindFirstLevelDescendants();
+
+ if (child != null)
+ {
+ lastElement = child;
+ return true;
+ }
+ }*/
+
+ return false;
+ }) ?? lastElement;
+
+ // Check cases where we may have found a child that we want to use instead of the element returned by search.
+ if (lastElement is AdornerLayer || lastElement is AdornerDecorator)
+ {
+ adornerLayerOrTopMostElement = lastElement;
+ }
+
+ if (adornerLayerOrTopMostElement is AdornerDecorator decorator)
+ {
+ await decorator.WaitUntilLoadedAsync();
+
+ return decorator.AdornerLayer;
+ }
+ else if (adornerLayerOrTopMostElement is AdornerLayer layer)
+ {
+ await layer.WaitUntilLoadedAsync();
+
+ // If we just have an adorner layer now, we're done!
+ return layer;
+ }
+ else
+ {
+ // TODO: Windows.UI.Xaml.Internal.RootScrollViewer is a maybe different and what was causing issues before I looked for ScrollViewers along the way?
+ // It's an internal unexposed type, so maybe it inherits from ScrollViewer? Not sure yet, but might need to detect and
+ // do something different here?
+
+ // ScrollViewers need AdornerLayers so they can provide adorners that scroll with the adorned elements (as it worked in WPF).
+ // Note: ScrollViewers and the Window were the main AdornerLayer integration points in WPF.
+ if (adornerLayerOrTopMostElement is ScrollViewer scroller)
+ {
+ var content = scroller.Content as FrameworkElement;
+
+ // Extra code for RootScrollViewer TODO: Can we detect this better?
+ if (scroller.Parent == null)
+ {
+ //// XamlMarkupHelper.UnloadObject doesn't work here (throws an invalid value exception) does content need a name?
+ // TODO: Figure out this scenario?
+ throw new NotImplementedException("RootScrollViewer attachment isn't supported, add a AdornerDecorator or ScrollViewer manually to the top-level of your application.");
+ }
+
+ scroller.Content = null;
+
+ var layerContainer = new AdornerDecorator()
+ {
+ Child = content!,
+ };
+
+ scroller.Content = layerContainer;
+
+ await layerContainer.WaitUntilLoadedAsync();
+
+ return layerContainer.AdornerLayer;
+ }
+ // Grid seems like the easiest place for us to inject AdornerLayers automatically at the top-level (if needed) - not sure how common this will be?
+ else if (adornerLayerOrTopMostElement is Grid grid)
+ {
+ // TODO: Not sure how we want to handle AdornerDecorator in this scenario...
+ var adornerLayer = new AdornerLayer();
+
+ // TODO: Handle if grid row/columns change.
+ Grid.SetRowSpan(adornerLayer, grid.RowDefinitions.Count);
+ Grid.SetColumnSpan(adornerLayer, grid.ColumnDefinitions.Count);
+ grid.Children.Add(adornerLayer);
+
+ await adornerLayer.WaitUntilLoadedAsync();
+
+ return adornerLayer;
+ }
+ }
+
+ return null;
+ }
+
+ // TODO: Temp helper? Build into 'Adorner' base class?
+ private static void AttachAdorner(AdornerLayer layer, FrameworkElement adornedElement, UIElement adornerXaml)
+ {
+ if (adornerXaml is Adorner adorner)
+ {
+ // We already have an adorner type, use it directly.
+ }
+ else
+ {
+ adorner = new Adorner()
+ {
+ Content = adornerXaml,
+ };
+ }
+
+ // Add adorner XAML content to the Adorner Layer
+ adorner.AdornerLayer = layer;
+ adorner.AdornedElement = adornedElement;
+
+ layer.Children.Add(adorner);
+ }
+
+ internal static void RemoveAdorner(AdornerLayer layer, UIElement adornerXaml)
+ {
+ var adorner = adornerXaml.FindAscendantOrSelf();
+
+ if (adorner != null)
+ {
+ adorner.AdornedElement = null;
+ adorner.AdornerLayer = null;
+
+ layer.Children.Remove(adorner);
+
+#if !HAS_UNO
+ VisualTreeHelper.DisconnectChildrenRecursive(adorner);
+#endif
+ }
+ }
+}
diff --git a/components/Adorners/src/AdornerOfT.cs b/components/Adorners/src/AdornerOfT.cs
new file mode 100644
index 000000000..b638a8bb7
--- /dev/null
+++ b/components/Adorners/src/AdornerOfT.cs
@@ -0,0 +1,29 @@
+// 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.
+
+namespace CommunityToolkit.WinUI;
+
+///
+/// A base class for s allowing for explicit types.
+///
+/// The object type to attach to
+public abstract partial class Adorner : Adorner where T : UIElement
+{
+ ///
+ public new T? AdornedElement
+ {
+ get { return base.AdornedElement as T; }
+ }
+
+ ///
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ if (this.AdornedElement is null)
+ {
+ throw new InvalidOperationException($"AdornedElement {base.AdornedElement?.GetType().FullName} is not of type {typeof(T).FullName}");
+ }
+ }
+}
diff --git a/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj
new file mode 100644
index 000000000..548041b9e
--- /dev/null
+++ b/components/Adorners/src/CommunityToolkit.WinUI.Adorners.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+ Adorners
+ This package contains Adorners. A Modern WinUI XAML based take on WPF Adorners.
+
+
+ CommunityToolkit.WinUI.Controls.AdornersRns
+ preview
+
+
+
+
+
diff --git a/components/Adorners/src/Dependencies.props b/components/Adorners/src/Dependencies.props
new file mode 100644
index 000000000..5acb20497
--- /dev/null
+++ b/components/Adorners/src/Dependencies.props
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs
new file mode 100644
index 000000000..34d39c1fc
--- /dev/null
+++ b/components/Adorners/src/Helpers/FrameworkElementExtensions.WaitUntilLoaded.cs
@@ -0,0 +1,47 @@
+// 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.
+
+namespace CommunityToolkit.WinUI.Future;
+
+///
+/// Helper extensions for .
+///
+public static partial class FrameworkElementExtensions
+{
+ ///
+ /// A extension which can be used in asynchronous scenarios to
+ /// wait until an element has loaded before proceeding using a
+ /// that listens to the event. In the event the element
+ /// is already loaded (), the method will return immediately.
+ ///
+ /// The element to await loading.
+ ///
+ /// True if the element is loaded.
+ public static Task WaitUntilLoadedAsync(this FrameworkElement element, TaskCreationOptions? options = null)
+ {
+ if (element.IsLoaded && element.Parent != null)
+ {
+ return Task.FromResult(true);
+ }
+
+ var taskCompletionSource = options.HasValue ? new TaskCompletionSource(options.Value)
+ : new TaskCompletionSource();
+ try
+ {
+ void LoadedCallback(object sender, RoutedEventArgs args)
+ {
+ element.Loaded -= LoadedCallback;
+ taskCompletionSource.SetResult(true);
+ }
+
+ element.Loaded += LoadedCallback;
+ }
+ catch (Exception e)
+ {
+ taskCompletionSource.SetException(e);
+ }
+
+ return taskCompletionSource.Task;
+ }
+}
diff --git a/components/Adorners/src/MultiTarget.props b/components/Adorners/src/MultiTarget.props
new file mode 100644
index 000000000..b11c19426
--- /dev/null
+++ b/components/Adorners/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;wasdk;wpf;wasm;linuxgtk;macos;ios;android;
+
+
\ No newline at end of file
diff --git a/components/Adorners/src/Themes/Generic.xaml b/components/Adorners/src/Themes/Generic.xaml
new file mode 100644
index 000000000..42a3fc901
--- /dev/null
+++ b/components/Adorners/src/Themes/Generic.xaml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
diff --git a/components/Adorners/tests/Adorners.Tests.projitems b/components/Adorners/tests/Adorners.Tests.projitems
new file mode 100644
index 000000000..9b22684ce
--- /dev/null
+++ b/components/Adorners/tests/Adorners.Tests.projitems
@@ -0,0 +1,23 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 95E1FE34-BF6D-4E5C-8F44-51C8B9348212
+
+
+ AdornersTests
+
+
+
+
+ ExampleAdornersTestPage.xaml
+
+
+
+
+ Designer
+ MSBuild:Compile
+
+
+
\ No newline at end of file
diff --git a/components/Adorners/tests/Adorners.Tests.shproj b/components/Adorners/tests/Adorners.Tests.shproj
new file mode 100644
index 000000000..95aeb7961
--- /dev/null
+++ b/components/Adorners/tests/Adorners.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 95E1FE34-BF6D-4E5C-8F44-51C8B9348212
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/tests/ExampleAdornersTestClass.cs b/components/Adorners/tests/ExampleAdornersTestClass.cs
new file mode 100644
index 000000000..acf716aae
--- /dev/null
+++ b/components/Adorners/tests/ExampleAdornersTestClass.cs
@@ -0,0 +1,134 @@
+// 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.
+
+using CommunityToolkit.Tooling.TestGen;
+using CommunityToolkit.Tests;
+using CommunityToolkit.WinUI.Controls;
+
+namespace AdornersTests;
+
+[TestClass]
+public partial class ExampleAdornersTestClass : VisualUITestBase
+{
+ // If you don't need access to UI objects directly or async code, use this pattern.
+ [TestMethod]
+ public void SimpleSynchronousExampleTest()
+ {
+ var assembly = typeof(AdornerLayer).Assembly;
+ var type = assembly.GetType(typeof(AdornerLayer).FullName ?? string.Empty);
+
+ Assert.IsNotNull(type, "Could not find Adorners type.");
+ Assert.AreEqual(typeof(AdornerLayer), type, "Type of Adorners does not match expected type.");
+ }
+
+ // If you don't need access to UI objects directly, use this pattern.
+ [TestMethod]
+ public async Task SimpleAsyncExampleTest()
+ {
+ await Task.Delay(250);
+
+ Assert.IsTrue(true);
+ }
+
+ // Example that shows how to check for exception throwing.
+ [TestMethod]
+ public void SimpleExceptionCheckTest()
+ {
+ // If you need to check exceptions occur for invalid inputs, etc...
+ // Use Assert.ThrowsException to limit the scope to where you expect the error to occur.
+ // Otherwise, using the ExpectedException attribute could swallow or
+ // catch other issues in setup code.
+ Assert.ThrowsException(() => throw new NotImplementedException());
+ }
+
+ // The UIThreadTestMethod automatically dispatches to the UI for us to work with UI objects.
+ [UIThreadTestMethod]
+ public void SimpleUIAttributeExampleTest()
+ {
+ var component = new AdornerLayer();
+ Assert.IsNotNull(component);
+ }
+
+ // The UIThreadTestMethod can also easily grab a XAML Page for us by passing its type as a parameter.
+ // This lets us actually test a control as it would behave within an actual application.
+ // The page will already be loaded by the time your test is called.
+ [UIThreadTestMethod]
+ public void SimpleUIExamplePageTest(ExampleAdornersTestPage page)
+ {
+ // You can use the Toolkit Visual Tree helpers here to find the component by type or name:
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+
+ var componentByName = page.FindDescendant("AdornersControl");
+
+ Assert.IsNotNull(componentByName);
+ }
+
+ // You can still do async work with a UIThreadTestMethod as well.
+ [UIThreadTestMethod]
+ public async Task SimpleAsyncUIExamplePageTest(ExampleAdornersTestPage page)
+ {
+ // This helper can be used to wait for a rendering pass to complete.
+ // Note, this is already done by loading a Page with the [UIThreadTestMethod] helper.
+ await CompositionTargetHelper.ExecuteAfterCompositionRenderingAsync(() => { });
+
+ var component = page.FindDescendant();
+
+ Assert.IsNotNull(component);
+ }
+
+ //// ----------------------------- ADVANCED TEST SCENARIOS -----------------------------
+
+ // If you need to use DataRow, you can use this pattern with the UI dispatch still.
+ // Otherwise, checkout the UIThreadTestMethod attribute above.
+ // See https://github.com/CommunityToolkit/Labs-Windows/issues/186
+ [TestMethod]
+ public async Task ComplexAsyncUIExampleTest()
+ {
+ await EnqueueAsync(() =>
+ {
+ var component = new AdornerLayer();
+ Assert.IsNotNull(component);
+ });
+ }
+
+ // If you want to load other content not within a XAML page using the UIThreadTestMethod above.
+ // Then you can do that using the Load/UnloadTestContentAsync methods.
+ [TestMethod]
+ public async Task ComplexAsyncLoadUIExampleTest()
+ {
+ await EnqueueAsync(async () =>
+ {
+ var component = new AdornerLayer();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ });
+ }
+
+ // You can still use the UIThreadTestMethod to remove the extra layer for the dispatcher as well:
+ [UIThreadTestMethod]
+ public async Task ComplexAsyncLoadUIExampleWithoutDispatcherTest()
+ {
+ var component = new AdornerLayer();
+ Assert.IsNotNull(component);
+ Assert.IsFalse(component.IsLoaded);
+
+ await LoadTestContentAsync(component);
+
+ Assert.IsTrue(component.IsLoaded);
+
+ await UnloadTestContentAsync(component);
+
+ Assert.IsFalse(component.IsLoaded);
+ }
+}
diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml b/components/Adorners/tests/ExampleAdornersTestPage.xaml
new file mode 100644
index 000000000..5bac4081a
--- /dev/null
+++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/components/Adorners/tests/ExampleAdornersTestPage.xaml.cs b/components/Adorners/tests/ExampleAdornersTestPage.xaml.cs
new file mode 100644
index 000000000..5022a14c9
--- /dev/null
+++ b/components/Adorners/tests/ExampleAdornersTestPage.xaml.cs
@@ -0,0 +1,16 @@
+// 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.
+
+namespace AdornersTests;
+
+///
+/// An empty page that can be used on its own or navigated to within a Frame.
+///
+public sealed partial class ExampleAdornersTestPage : Page
+{
+ public ExampleAdornersTestPage()
+ {
+ this.InitializeComponent();
+ }
+}