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..eeacef555
--- /dev/null
+++ b/components/Adorners/samples/Adorners.md
@@ -0,0 +1,75 @@
+---
+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, InfoBadge, AdornerLayer, AdornerDecorator, Adorner, Input Validation, Resize, Highlighting
+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.
+
+You can see other example of custom adorners with the other Adorner help topics for the built-in adorners provided in this package, such as the `InputValidationAdorner` and `ResizeElementAdorner`.
+
+## 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..1eb5c98e3
--- /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..28171f7f0
--- /dev/null
+++ b/components/Adorners/samples/InPlaceTextEditor/InPlaceTextEditorAdornerSample.xaml.cs
@@ -0,0 +1,141 @@
+// 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 the object being edited.
+ ///
+ public IEditableObject EditableObject
+ {
+ get { return (IEditableObject)GetValue(EditableObjectProperty); }
+ set { SetValue(EditableObjectProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty EditableObjectProperty =
+ DependencyProperty.Register(nameof(EditableObject), typeof(IEditableObject), typeof(InPlaceTextEditorAdorner), new PropertyMetadata(null));
+
+ ///
+ /// 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)
+ {
+ EditableObject?.BeginEdit();
+ IsPopupOpen = true;
+ }
+
+ public void ConfirmButton_Click(object sender, RoutedEventArgs e)
+ {
+ EditableObject?.EndEdit();
+ IsPopupOpen = false;
+ }
+
+ public void CloseButton_Click(object sender, RoutedEventArgs e)
+ {
+ 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/InputValidationAdornerSample.xaml b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml
new file mode 100644
index 000000000..bd0082c52
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs
new file mode 100644
index 000000000..828f0a3c2
--- /dev/null
+++ b/components/Adorners/samples/InputValidation/InputValidationAdornerSample.xaml.cs
@@ -0,0 +1,78 @@
+// 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);
+ }
+ }
+}
diff --git a/components/Adorners/samples/InputValidationAdorner.md b/components/Adorners/samples/InputValidationAdorner.md
new file mode 100644
index 000000000..4fa8682db
--- /dev/null
+++ b/components/Adorners/samples/InputValidationAdorner.md
@@ -0,0 +1,45 @@
+---
+title: InputValidationAdorner
+author: michael-hawker
+description: An InputValidationAdorner provides input validation to any element implementing INotifyDataErrorInfo to provide feedback to a user.
+keywords: Adorners, Input Validation, INotifyDataErrorInfo, MVVM, CommunityToolkit.Mvvm
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+discussion-id: 278
+issue-id: 0
+icon: assets/icon.png
+---
+
+# InputValidationAdorner
+
+The `InputValidationAdorner` provides input validation to any element implementing `INotifyDataErrorInfo` to provide feedback to a user.
+
+## Background
+
+Input Validation existed in WPF and was available in a couple of ways. See the Migrating from WPF section below for more details on the differences between WPF and WinUI Input Validation.
+
+## Input Validation Example
+
+The `InputValidationAdorner` can be attached to any element and triggered to be shown automatically based on validation provided by the `INotifyDataErrorInfo` interface set on the `NotifyDataErrorInfo` property of the adorner.
+
+The custom adorner will automatically display the validation message for the specified `PropertyName` is marked as invalid by the `INotifyDataErrorInfo` implementation.
+
+For the example below, we use the `ObservableValidator` class from the `CommunityToolkit.Mvvm` package to provide automatic validation of the rules within 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]
+
+## Migrating from WPF
+
+Input Validation within WinUI is handled as a mix of both of WPF's [Binding Validation](https://learn.microsoft.com/dotnet/desktop/wpf/data/how-to-implement-binding-validation) and [Custom Object Validation](https://learn.microsoft.com/dotnet/desktop/wpf/data/how-to-implement-validation-logic-on-custom-objects).
+
+> [!WARNING]
+> That the WinUI Adorner uses the `INotifyDataErrorInfo` interface for validation feedback, whereas WPF's Custom Object Validation uses the `IDataErrorInfo` interface. You will need to adapt your validation logic accordingly when migrating from WPF to WinUI.
+
+> [!NOTE]
+> The `ValidationRule` Binding concept from WPF is not supported in WinUI. You will need to implement validation logic within your view model or data model using the `INotifyDataErrorInfo` interface instead.
+> You can still specify a custom error template by styling the `InputValidationAdorner` control.
+
+When paired with the validation provided by the `CommunityToolkit.Mvvm` package, you can achieve similar functionality to WPF's Input Validation with less boilerplate code.
diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml
new file mode 100644
index 000000000..ef1650a09
--- /dev/null
+++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
diff --git a/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml.cs b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml.cs
new file mode 100644
index 000000000..a70f5e5c5
--- /dev/null
+++ b/components/Adorners/samples/ResizeElement/ResizeElementAdornerCanvasSample.xaml.cs
@@ -0,0 +1,15 @@
+// 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.ResizeElement;
+
+[ToolkitSampleBoolOption("IsAdornerVisible", false, Title = "Is Adorner Visible")]
+[ToolkitSample(id: nameof(ResizeElementAdornerCanvasSample), "Resize Element Adorner on Canvas", description: "A sample for showing how to use an Adorner for resizing an element within a Canvas.")]
+public sealed partial class ResizeElementAdornerCanvasSample : Page
+{
+ public ResizeElementAdornerCanvasSample()
+ {
+ this.InitializeComponent();
+ }
+}
diff --git a/components/Adorners/samples/ResizeElementAdorner.md b/components/Adorners/samples/ResizeElementAdorner.md
new file mode 100644
index 000000000..66030ec62
--- /dev/null
+++ b/components/Adorners/samples/ResizeElementAdorner.md
@@ -0,0 +1,27 @@
+---
+title: ResizeElementAdorner
+author: michael-hawker
+description: A ResizeElementAdorner provides resizing functionality to FrameworkElement.
+keywords: Adorners, Resize, FrameworkElement, Layout, Controls
+dev_langs:
+ - csharp
+category: Controls
+subcategory: Layout
+discussion-id: 278
+issue-id: 0
+icon: assets/icon.png
+---
+
+# ResizeElementAdorner
+
+The `ResizeElementAdorner` provides resizing functionality to any `FrameworkElement`.
+
+## Usage Example
+
+The `ResizeElementAdorner` can be attached to any element and displayed to allow a user to resize the adorned element by dragging the resize handles.
+
+> [!SAMPLE ResizeElementAdornerCanvasSample]
+
+This can be done above within a `Canvas` layout control, or with general layout using Margins as well:
+
+// TODO: Add Margin example here
diff --git a/components/Adorners/src/Adorner.cs b/components/Adorners/src/Adorner.cs
new file mode 100644
index 000000000..4808edae7
--- /dev/null
+++ b/components/Adorners/src/Adorner.cs
@@ -0,0 +1,148 @@
+// 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);
+
+ // Also update size
+ OnSizeChanged(this, null!);
+ }
+ }
+
+ 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() { }
+
+ ///
+ public new void UpdateLayout()
+ {
+ OnLayoutUpdated(this, null!);
+
+ base.UpdateLayout();
+ }
+}
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/InputValidation/InputValidationAdorner.cs b/components/Adorners/src/InputValidation/InputValidationAdorner.cs
new file mode 100644
index 000000000..6d9477a12
--- /dev/null
+++ b/components/Adorners/src/InputValidation/InputValidationAdorner.cs
@@ -0,0 +1,160 @@
+// 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 System.Collections;
+using System.ComponentModel.DataAnnotations;
+
+using INotifyDataErrorInfo = System.ComponentModel.INotifyDataErrorInfo;
+using DataErrorsChangedEventArgs = System.ComponentModel.DataErrorsChangedEventArgs;
+
+namespace CommunityToolkit.WinUI.Adorners;
+
+///
+/// An Adorner that shows an error message if Data Validation fails.
+/// Set the with the object that must implement . It assumes that the return of is a or string collection.
+/// Adorner is shown automatically when the event is raised and the matches the invalid property of the event arguments.
+///
+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, (s, e) => (s as InputValidationAdorner)?.RefreshErrors()));
+
+ ///
+ /// Gets or sets the context object to use for validation.
+ ///
+ public INotifyDataErrorInfo NotifyDataErrorInfo
+ {
+ get { return (INotifyDataErrorInfo)GetValue(NotifyDataErrorInfoProperty); }
+ set { SetValue(NotifyDataErrorInfoProperty, value); }
+ }
+
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly DependencyProperty NotifyDataErrorInfoProperty =
+ DependencyProperty.Register(nameof(NotifyDataErrorInfo), typeof(INotifyDataErrorInfo), typeof(InputValidationAdorner), new PropertyMetadata(null, (s, e) => (s as InputValidationAdorner)?.RefreshErrors()));
+
+ ///
+ /// Gets or sets whether the validation adorners is displayed (handled automatically).
+ ///
+ 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, set automatically by the adorner.
+ ///
+ 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));
+
+ // TODO: Do we consider an InfoBar style Severity property?
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public InputValidationAdorner()
+ {
+ this.DefaultStyleKey = typeof(InputValidationAdorner);
+
+ // Uno workaround
+ DataContext = this;
+ }
+
+ ///
+ protected override void OnApplyTemplate()
+ {
+ base.OnApplyTemplate();
+ }
+
+ ///
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ NotifyDataErrorInfo?.ErrorsChanged += this.INotifyDataErrorInfo_ErrorsChanged;
+ }
+
+ private void INotifyDataErrorInfo_ErrorsChanged(object? sender, DataErrorsChangedEventArgs e)
+ {
+ RefreshErrors();
+ }
+
+ private void RefreshErrors()
+ {
+ // Check if we have any errors for our specified property
+ if (NotifyDataErrorInfo is not null
+ && PropertyName is not null
+ && NotifyDataErrorInfo.GetErrors(PropertyName) is IEnumerable errors
+ && errors.Cast