From 1fd66b5c24981965d0ec7da745c5c8172ba09a72 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Wed, 11 Mar 2026 10:51:45 +0100 Subject: [PATCH 1/7] [net11.0] Trimmable element handlers Squashed for clean rebase onto net11.0. --- docs/design/HandlerResolution.md | 39 ++- .../BordelessEntryServiceBuilder.cs | 2 + .../ActivityIndicator/ActivityIndicator.cs | 2 + .../Core/Application/Application.Mapper.cs | 6 +- .../src/Core/Application/Application.cs | 3 + src/Controls/src/Core/Border/Border.cs | 2 + src/Controls/src/Core/BoxView/BoxView.cs | 2 + src/Controls/src/Core/Button/Button.Mapper.cs | 7 +- src/Controls/src/Core/Button/Button.cs | 2 + src/Controls/src/Core/Cells/Cell.cs | 5 + src/Controls/src/Core/Cells/EntryCell.cs | 5 + src/Controls/src/Core/Cells/ImageCell.cs | 3 + src/Controls/src/Core/Cells/SwitchCell.cs | 5 + src/Controls/src/Core/Cells/TextCell.cs | 5 + src/Controls/src/Core/Cells/ViewCell.cs | 5 + .../src/Core/CheckBox/CheckBox.Mapper.cs | 8 +- src/Controls/src/Core/CheckBox/CheckBox.cs | 2 +- .../Core/ContentPage/ContentPage.Mapper.cs | 6 +- .../src/Core/ContentView/ContentView.cs | 2 + .../src/Core/DatePicker/DatePicker.Mapper.cs | 6 +- .../src/Core/DatePicker/DatePicker.cs | 13 + src/Controls/src/Core/Editor/Editor.Mapper.cs | 6 +- src/Controls/src/Core/Editor/Editor.cs | 8 + .../src/Core/Element/Element.Mapper.cs | 9 +- src/Controls/src/Core/Element/Element.cs | 41 +--- src/Controls/src/Core/Entry/Entry.Mapper.cs | 6 +- src/Controls/src/Core/Entry/Entry.cs | 8 + .../src/Core/FlyoutPage/FlyoutPage.Mapper.cs | 6 +- .../src/Core/FlyoutPage/FlyoutPage.cs | 6 + src/Controls/src/Core/Frame/Frame.cs | 5 + .../src/Core/GraphicsView/GraphicsView.cs | 2 + .../Core/Hosting/AppHostBuilderExtensions.cs | 224 +----------------- .../src/Core/HybridWebView/HybridWebView.cs | 3 + src/Controls/src/Core/Image/Image.cs | 2 + .../Core/ImageButton/ImageButton.Mapper.cs | 6 +- .../src/Core/ImageButton/ImageButton.cs | 2 + .../src/Core/IndicatorView/IndicatorView.cs | 2 + src/Controls/src/Core/Items/CarouselView.cs | 6 + src/Controls/src/Core/Items/CollectionView.cs | 5 + src/Controls/src/Core/Label/Label.Mapper.cs | 8 +- src/Controls/src/Core/Label/Label.cs | 7 + src/Controls/src/Core/Layout/Layout.Mapper.cs | 5 +- src/Controls/src/Core/Layout/Layout.cs | 2 + src/Controls/src/Core/ListView/ListView.cs | 6 + src/Controls/src/Core/Menu/MenuBar.cs | 1 + src/Controls/src/Core/Menu/MenuBarItem.cs | 1 + src/Controls/src/Core/Menu/MenuFlyout.cs | 3 + src/Controls/src/Core/Menu/MenuFlyoutItem.cs | 1 + .../src/Core/Menu/MenuFlyoutSeparator.cs | 1 + .../src/Core/Menu/MenuFlyoutSubItem.cs | 1 + .../NavigationPage/NavigationPage.Mapper.cs | 6 +- .../src/Core/NavigationPage/NavigationPage.cs | 5 + src/Controls/src/Core/Page/Page.cs | 2 + src/Controls/src/Core/Picker/Picker.Mapper.cs | 6 +- src/Controls/src/Core/Picker/Picker.cs | 2 + .../src/Core/ProgressBar/ProgressBar.cs | 2 + .../Core/RadioButton/RadioButton.Mapper.cs | 6 +- .../src/Core/RadioButton/RadioButton.cs | 13 + .../Core/RefreshView/RefreshView.Mapper.cs | 7 +- .../src/Core/RefreshView/RefreshView.cs | 1 + src/Controls/src/Core/RemappingHelper.cs | 25 ++ .../src/Core/ScrollView/ScrollView.Mapper.cs | 6 +- .../src/Core/ScrollView/ScrollView.cs | 2 + .../src/Core/SearchBar/SearchBar.Mapper.cs | 7 +- src/Controls/src/Core/SearchBar/SearchBar.cs | 2 + src/Controls/src/Core/Shape/Shape.Mapper.cs | 6 +- src/Controls/src/Core/Shapes/Ellipse.cs | 2 + src/Controls/src/Core/Shapes/Line.cs | 2 + src/Controls/src/Core/Shapes/Path.cs | 2 + src/Controls/src/Core/Shapes/Polygon.cs | 2 + src/Controls/src/Core/Shapes/Polyline.cs | 2 + src/Controls/src/Core/Shapes/Rectangle.cs | 2 + .../src/Core/Shapes/RoundRectangle.cs | 2 + src/Controls/src/Core/Shell/Shell.cs | 6 + src/Controls/src/Core/Shell/ShellContent.cs | 4 + src/Controls/src/Core/Shell/ShellItem.cs | 4 + src/Controls/src/Core/Shell/ShellSection.cs | 3 + src/Controls/src/Core/Slider/Slider.Mapper.cs | 6 +- src/Controls/src/Core/Slider/Slider.cs | 2 + .../src/Core/Stepper/Stepper.Mapper.cs | 6 +- src/Controls/src/Core/Stepper/Stepper.cs | 2 + src/Controls/src/Core/SwipeView/SwipeItem.cs | 2 + .../src/Core/SwipeView/SwipeItemView.cs | 2 + .../src/Core/SwipeView/SwipeView.Mapper.cs | 6 +- src/Controls/src/Core/SwipeView/SwipeView.cs | 1 + src/Controls/src/Core/Switch/Switch.cs | 12 + .../src/Core/TabbedPage/TabbedPage.Mapper.cs | 6 +- .../src/Core/TabbedPage/TabbedPage.cs | 6 + src/Controls/src/Core/TableView/TableView.cs | 5 + .../src/Core/TemplatedView/TemplatedView.cs | 2 +- .../src/Core/TimePicker/TimePicker.Mapper.cs | 6 +- .../src/Core/TimePicker/TimePicker.cs | 13 + .../src/Core/Toolbar/Toolbar.Mapper.cs | 2 +- src/Controls/src/Core/Toolbar/Toolbar.cs | 3 + .../VisualElement/VisualElement.Mapper.cs | 40 +--- .../src/Core/WebView/WebView.Mapper.cs | 6 +- src/Controls/src/Core/WebView/WebView.cs | 1 + src/Controls/src/Core/Window/Window.Mapper.cs | 6 +- src/Controls/src/Core/Window/Window.cs | 1 + .../Xaml/Hosting/AppHostBuilderExtensions.cs | 3 +- .../Core.UnitTests/VisualElementTests.cs | 17 -- .../ControlsDeviceTestExtensions.cs | 1 - .../Handlers/Button/ButtonHandler.Windows.cs | 2 + .../DatePicker/DatePickerHandler.Windows.cs | 2 + .../Element/ElementHandlerAttribute.cs | 17 +- .../src/Handlers/Element/RemappingHelper.cs | 26 -- src/Core/src/Handlers/IElementHandler.cs | 2 +- .../ImageButton/ImageButtonHandler.Windows.cs | 2 + .../RadioButton/RadioButtonHandler.Windows.cs | 2 + .../Handlers/Slider/SliderHandler.Windows.cs | 2 + .../Stepper/StepperHandler.Windows.cs | 2 + .../Handlers/Switch/SwitchHandler.Windows.cs | 2 + .../TimePicker/TimePickerHandler.Windows.cs | 2 + .../src/Handlers/View/ViewHandler.Windows.cs | 14 +- src/Core/src/Hosting/IMauiHandlersFactory.cs | 1 + .../Hosting/Internal/MauiHandlersFactory.cs | 79 ++++-- .../net-android/PublicAPI.Unshipped.txt | 3 + .../PublicAPI/net-ios/PublicAPI.Unshipped.txt | 3 + .../net-maccatalyst/PublicAPI.Unshipped.txt | 3 + .../net-tizen/PublicAPI.Unshipped.txt | 3 + .../net-windows/PublicAPI.Unshipped.txt | 3 + .../src/PublicAPI/net/PublicAPI.Unshipped.txt | 3 + .../netstandard/PublicAPI.Unshipped.txt | 3 + .../netstandard2.0/PublicAPI.Unshipped.txt | 3 + 124 files changed, 552 insertions(+), 426 deletions(-) create mode 100644 src/Controls/src/Core/RemappingHelper.cs delete mode 100644 src/Core/src/Handlers/Element/RemappingHelper.cs diff --git a/docs/design/HandlerResolution.md b/docs/design/HandlerResolution.md index 93a5e1db85a3..d131c69477cb 100644 --- a/docs/design/HandlerResolution.md +++ b/docs/design/HandlerResolution.md @@ -3,7 +3,20 @@ Handler Resolution # Introduction -Handlers are the platform components used to render a cross platform `View` on the screen. Every platform registers a handler against a .NET Maui type. +Handlers are the platform components used to render a cross-platform `View` on the screen. Each view type is associated with a handler that knows how to create and manage the corresponding platform-native control. + +## Declaring a Handler with `[ElementHandler]` + +Most built-in .NET MAUI views declare their handler using the `[ElementHandler]` attribute directly on the view class: + +```csharp +[ElementHandler(typeof(ButtonHandler))] +public partial class Button : View, IButton { ... } +``` + +This is the primary mechanism for associating views with handlers. It is trimmer-safe and AOT-friendly because the handler type is statically referenced. + +The attribute is declared with `Inherited = false`, so each view type must explicitly declare it. However, `MauiHandlersFactory` walks the type's base class hierarchy (`Type.BaseType`) when looking for the attribute, so a base class attribute acts as a fallback for derived types that don't declare their own. ## Registering a Handler in Code @@ -14,6 +27,26 @@ builder.ConfigureMauiHandlers(handlers => } ``` +DI registration should only be used to override an existing `[ElementHandler]` declaration or when the element type is an interface (e.g., `IScrollView`). DI-registered handlers take priority over `[ElementHandler]` attributes when registered for the exact same type. + +## Resolution Order + +Both `MauiHandlersFactory.GetHandler(Type)` and `MauiHandlersFactory.GetHandlerType(Type)` follow the same resolution order: + +1. **Exact DI registration** — checks if a handler was registered for this exact type via `AddHandler` +2. **`[ElementHandler]` attribute** — walks the type's base class hierarchy looking for the attribute +3. **Interface-based DI registration** — uses `RegisteredHandlerServiceTypeSet` to find the best matching interface registration (e.g., a handler registered for `IScrollView` matches a `ScrollView` instance) +4. **`IContentView` fallback** — returns `ContentViewHandler` for any `IContentView` implementation +5. **`GetHandlerType` returns `null`** / **`GetHandler` throws `HandlerNotFoundException`** — if none of the above matched + +### Handler Instantiation + +How a handler instance is created depends on how it was resolved: + +- **DI-registered handlers** (steps 1 & 3): Instantiated through `MauiFactory.GetService()`, which uses `Activator.CreateInstance` on the registered `ImplementationType`, or invokes the `ImplementationFactory` delegate if one was provided. +- **`[ElementHandler]` attribute** (step 2): Instantiated directly via `Activator.CreateInstance` — no DI involvement. +- **Fallback in `ElementExtensions.ToHandler()`**: When `Activator.CreateInstance` fails with a `MissingMethodException` (e.g., the handler requires constructor parameters), `ActivatorUtilities.CreateInstance` is used instead, which supports constructor injection from the DI container. + ## Types used in the resolution of Handlers to Views ### `MauiFactory` @@ -34,8 +67,7 @@ public class MauiHandlersFactory : MauiFactory, IMauiHandlersFactory - `MauiFactory` has support for `ctor` resolution but we currently have it disabled in all cases. - Handlers will currently attempt to instantiate through [Extensions.DependencyInjection.ActivatorUtilities.CreateInstance](https://github.com/dotnet/maui/blob/cc53f0979baf5d6bb8a5d6bf84b64f3cf591c56f/src/Core/src/Platform/ElementExtensions.cs#L34 ) if a default constructor hasn't been created. So the ctor resolution feature of `MauiFactory` probably doesn't have any currently useful purpose. - `MauiFactory` currently doesn't support Scoped Services which is the main reason why we switched to `Ms.Ext.DI` for our main implementation. .NET MAUI Blazor requires Scoped Services and we've started using Scoped Services as well for multi-window. -- `MauiFactory` retrieves all base types from the requested type and all implemented interfaces. It first iterates over base types and then if nothing is found it loops through the interfaces. The interface behavior currently leads to some odd behavior because everything implements `IView`. This means that if a handler isn't registered then `MauiFactory` just returns a random handler because technically every single handler is registered against a cross platform view that implements`IView`. https://github.com/dotnet/maui/issues/1298 - - We should probably remove the interface matching part of `MauiFactory` +- `MauiFactory` retrieves the handler type registered for the requested type. Interface-based registration matching is now handled by `RegisteredHandlerServiceTypeSet`, which finds the most specific matching interface to avoid ambiguity (the old behavior of matching any `IView`-implementing interface has been fixed — see https://github.com/dotnet/maui/issues/1298). ### IMauiHandlersFactory @@ -54,7 +86,6 @@ public interface IMauiHandlersFactory : IMauiFactory Type? GetHandlerType(Type iview); IElementHandler? GetHandler(Type type); IElementHandler? GetHandler() where T : IElement; - IMauiHandlersCollection GetCollection(); } ``` diff --git a/src/Controls/samples/Controls.Sample/Controls/BordelessEntry/BordelessEntryServiceBuilder.cs b/src/Controls/samples/Controls.Sample/Controls/BordelessEntry/BordelessEntryServiceBuilder.cs index 647bd41eafe8..7599234bf010 100644 --- a/src/Controls/samples/Controls.Sample/Controls/BordelessEntry/BordelessEntryServiceBuilder.cs +++ b/src/Controls/samples/Controls.Sample/Controls/BordelessEntry/BordelessEntryServiceBuilder.cs @@ -57,7 +57,9 @@ public void Initialize(IServiceProvider services) } } +#pragma warning disable CS0618 // Obsolete BordelessEntryServiceBuilder.HandlersCollection ??= services.GetRequiredService().GetCollection(); +#pragma warning restore CS0618 if (BordelessEntryServiceBuilder.PendingHandlers.Count > 0) { diff --git a/src/Controls/src/Core/ActivityIndicator/ActivityIndicator.cs b/src/Controls/src/Core/ActivityIndicator/ActivityIndicator.cs index 0498db4a2c80..8ea14d06a9bb 100644 --- a/src/Controls/src/Core/ActivityIndicator/ActivityIndicator.cs +++ b/src/Controls/src/Core/ActivityIndicator/ActivityIndicator.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Handlers; namespace Microsoft.Maui.Controls { @@ -13,6 +14,7 @@ namespace Microsoft.Maui.Controls /// This control gives a visual clue to the user that something is happening, without information about its progress. /// [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] + [ElementHandler(typeof(ActivityIndicatorHandler))] public partial class ActivityIndicator : View, IColorElement, IElementConfiguration, IActivityIndicator { /// Bindable property for . diff --git a/src/Controls/src/Core/Application/Application.Mapper.cs b/src/Controls/src/Core/Application/Application.Mapper.cs index e67bb20e1151..a186f4f2c8c4 100644 --- a/src/Controls/src/Core/Application/Application.Mapper.cs +++ b/src/Controls/src/Core/Application/Application.Mapper.cs @@ -6,8 +6,12 @@ namespace Microsoft.Maui.Controls { public partial class Application { - internal static new void RemapForControls() + static Application() { + // Force Element's static constructor to run first so base-level + // mapper remappings are applied before these Control-specific ones. + RemappingHelper.EnsureBaseTypeRemapped(typeof(Application), typeof(Element)); + // Adjust the mappings to preserve Controls.Application legacy behaviors #if ANDROID // There is also a mapper on Window for this property since this property is relevant at the window level for diff --git a/src/Controls/src/Core/Application/Application.cs b/src/Controls/src/Core/Application/Application.cs index 5e58016a9eb0..d4635c4258c8 100644 --- a/src/Controls/src/Core/Application/Application.cs +++ b/src/Controls/src/Core/Application/Application.cs @@ -13,11 +13,14 @@ using Microsoft.Maui.Graphics; using Microsoft.Maui.Handlers; +using Microsoft.Maui.Handlers; + namespace Microsoft.Maui.Controls { /// /// Represents the main application class that provides lifecycle management, resources, and theming. /// + [ElementHandler(typeof(ApplicationHandler))] public partial class Application : Element, IResourcesProvider, IApplicationController, IElementConfiguration, IVisualTreeElement, IApplication { readonly WeakEventManager _weakEventManager = new WeakEventManager(); diff --git a/src/Controls/src/Core/Border/Border.cs b/src/Controls/src/Core/Border/Border.cs index eef7d2615adf..4d6e4aa6ade4 100644 --- a/src/Controls/src/Core/Border/Border.cs +++ b/src/Controls/src/Core/Border/Border.cs @@ -6,6 +6,7 @@ using Microsoft.Maui.Controls.Shapes; using Microsoft.Maui.Graphics; using Microsoft.Maui.Layouts; +using Microsoft.Maui.Handlers; namespace Microsoft.Maui.Controls { @@ -17,6 +18,7 @@ namespace Microsoft.Maui.Controls /// background, shape, padding, and more to create visually rich containers. /// [ContentProperty(nameof(Content))] + [ElementHandler(typeof(BorderHandler))] public class Border : View, IContentView, IBorderView, IPaddingElement, ISafeAreaElement, ISafeAreaView2 { float[]? _strokeDashPattern; diff --git a/src/Controls/src/Core/BoxView/BoxView.cs b/src/Controls/src/Core/BoxView/BoxView.cs index 8e802f72a35b..8242c51ee2ea 100644 --- a/src/Controls/src/Core/BoxView/BoxView.cs +++ b/src/Controls/src/Core/BoxView/BoxView.cs @@ -3,12 +3,14 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Controls.Handlers; namespace Microsoft.Maui.Controls { /// /// A used to draw a solid colored rectangle. /// + [ElementHandler(typeof(BoxViewHandler))] public partial class BoxView : View, IColorElement, ICornerElement, IElementConfiguration, IShapeView, IShape { /// Bindable property for . diff --git a/src/Controls/src/Core/Button/Button.Mapper.cs b/src/Controls/src/Core/Button/Button.Mapper.cs index 53bbdb65d5e9..f0c3b3113b7d 100644 --- a/src/Controls/src/Core/Button/Button.Mapper.cs +++ b/src/Controls/src/Core/Button/Button.Mapper.cs @@ -11,9 +11,12 @@ namespace Microsoft.Maui.Controls public partial class Button { // IButton does not include the ContentType property, so we map it here to handle Image Positioning - - internal new static void RemapForControls() + static Button() { + // Force VisualElement's static constructor to run first so base-level + // mapper remappings are applied before these Control-specific ones. + RemappingHelper.EnsureBaseTypeRemapped(typeof(Button), typeof(VisualElement)); + ButtonHandler.Mapper.ReplaceMapping(nameof(ContentLayout), MapContentLayout); #if IOS ButtonHandler.Mapper.ReplaceMapping(nameof(Padding), MapPadding); diff --git a/src/Controls/src/Core/Button/Button.cs b/src/Controls/src/Core/Button/Button.cs index cc87be1f60c8..4696dea90fde 100644 --- a/src/Controls/src/Core/Button/Button.cs +++ b/src/Controls/src/Core/Button/Button.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Windows.Input; using Microsoft.Maui.Controls.Internals; +using Microsoft.Maui.Handlers; using Microsoft.Maui.Graphics; @@ -15,6 +16,7 @@ namespace Microsoft.Maui.Controls /// A button that reacts to touch events. /// [DebuggerDisplay("{GetDebuggerDisplay(), nq}")] + [ElementHandler(typeof(ButtonHandler))] public partial class Button : View, IFontElement, ITextElement, IBorderElement, IButtonController, IElementConfiguration