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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +