Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions docs/design/HandlerResolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,6 +27,31 @@ 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.

> **Note:** Handlers registered via `[ElementHandler]` must have a public parameterless constructor.
> They are instantiated with `Activator.CreateInstance()`, not through the DI container.
> The `ActivatorUtilities.CreateInstance()` fallback only applies to DI-registered handlers
> resolved through `ElementExtensions.ToHandler()`.

## Types used in the resolution of Handlers to Views

### `MauiFactory`
Expand All @@ -34,8 +72,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

Expand All @@ -54,7 +91,6 @@ public interface IMauiHandlersFactory : IMauiFactory
Type? GetHandlerType(Type iview);
IElementHandler? GetHandler(Type type);
IElementHandler? GetHandler<T>() where T : IElement;
IMauiHandlersCollection GetCollection();
}
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public void Initialize(IServiceProvider services)
}
}

#pragma warning disable CS0618 // Obsolete
BordelessEntryServiceBuilder.HandlersCollection ??= services.GetRequiredService<IMauiHandlersFactory>().GetCollection();
#pragma warning restore CS0618

if (BordelessEntryServiceBuilder.PendingHandlers.Count > 0)
{
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/ActivityIndicator/ActivityIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Diagnostics;

using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;

namespace Microsoft.Maui.Controls
{
Expand All @@ -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.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
[ElementHandler(typeof(ActivityIndicatorHandler))]
public partial class ActivityIndicator : View, IColorElement, IElementConfiguration<ActivityIndicator>, IActivityIndicator
{
/// <summary>Bindable property for <see cref="IsRunning"/>.</summary>
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/src/Core/Application/Application.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Core/Application/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;

using Microsoft.Maui.Handlers;

namespace Microsoft.Maui.Controls
{
/// <summary>
/// Represents the main application class that provides lifecycle management, resources, and theming.
/// </summary>
[ElementHandler(typeof(ApplicationHandler))]
public partial class Application : Element, IResourcesProvider, IApplicationController, IElementConfiguration<Application>, IVisualTreeElement, IApplication
{
readonly WeakEventManager _weakEventManager = new WeakEventManager();
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/Border/Border.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -17,6 +18,7 @@ namespace Microsoft.Maui.Controls
/// background, shape, padding, and more to create visually rich containers.
/// </remarks>
[ContentProperty(nameof(Content))]
[ElementHandler(typeof(BorderHandler))]
public class Border : View, IContentView, IBorderView, IPaddingElement, ISafeAreaElement, ISafeAreaView2
{
float[]? _strokeDashPattern;
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/BoxView/BoxView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Controls.Handlers;

namespace Microsoft.Maui.Controls
{
/// <summary>
/// A <see cref="View" /> used to draw a solid colored rectangle.
/// </summary>
[ElementHandler(typeof(BoxViewHandler))]
public partial class BoxView : View, IColorElement, ICornerElement, IElementConfiguration<BoxView>, IShapeView, IShape
{
/// <summary>Bindable property for <see cref="Color"/>.</summary>
Expand Down
7 changes: 5 additions & 2 deletions src/Controls/src/Core/Button/Button.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Button, IButtonHandler>(nameof(ContentLayout), MapContentLayout);
#if IOS
ButtonHandler.Mapper.ReplaceMapping<Button, IButtonHandler>(nameof(Padding), MapPadding);
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/Button/Button.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -15,6 +16,7 @@ namespace Microsoft.Maui.Controls
/// A button <see cref="View" /> that reacts to touch events.
/// </summary>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
[ElementHandler(typeof(ButtonHandler))]
public partial class Button : View, IFontElement, ITextElement, IBorderElement, IButtonController, IElementConfiguration<Button>, IPaddingElement, IImageController, IViewController, IButtonElement, ICommandElement, IImageElement, IButton, ITextButton, IImageButton
{
const double DefaultSpacing = 10;
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Cells/Cell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Microsoft.Maui.Controls
{
// Don't add IElementConfiguration<Cell> because it kills performance on UWP structures that use Cells
/// <summary>Provides base class and capabilities for all Microsoft.Maui.Controls cells. Cells are elements meant to be added to <see cref="Microsoft.Maui.Controls.ListView"/> or <see cref="Microsoft.Maui.Controls.TableView"/>.</summary>
#if WINDOWS || ANDROID || IOS || MACCATALYST
#pragma warning disable CS0618 // Type or member is obsolete
[ElementHandler(typeof(Handlers.Compatibility.CellRenderer))]
#pragma warning restore CS0618 // Type or member is obsolete
#endif
public abstract class Cell : Element, ICellController, IFlowDirectionController, IPropertyPropagationController, IVisualController, IWindowController, IVisualTreeElement
{
/// <summary>The default height of cells.</summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Cells/EntryCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ namespace Microsoft.Maui.Controls
{
/// <summary>A <see cref="Microsoft.Maui.Controls.Cell"/> with a label and a single line text entry field.</summary>
[Obsolete("The controls which use EntryCell (ListView and TableView) are obsolete. Please use CollectionView instead.")]
#if WINDOWS || ANDROID || IOS || MACCATALYST
#pragma warning disable CS0618 // Type or member is obsolete
[ElementHandler(typeof(Handlers.Compatibility.EntryCellRenderer))]
#pragma warning restore CS0618 // Type or member is obsolete
#endif
public class EntryCell : Cell, ITextAlignmentElement, IEntryCellController, ITextAlignment
{
/// <summary>Bindable property for <see cref="Text"/>.</summary>
Expand Down
3 changes: 3 additions & 0 deletions src/Controls/src/Core/Cells/ImageCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ namespace Microsoft.Maui.Controls
{
/// <summary>A <see cref="Microsoft.Maui.Controls.TextCell"/> that has an image.</summary>
[Obsolete("The controls which use ImageCell (ListView and TableView) are obsolete. Please use CollectionView instead.")]
#if WINDOWS || ANDROID || IOS || MACCATALYST
[ElementHandler(typeof(Handlers.Compatibility.ImageCellRenderer))]
#endif
public class ImageCell : TextCell
{
/// <summary>Bindable property for <see cref="ImageSource"/>.</summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Cells/SwitchCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ namespace Microsoft.Maui.Controls
{
/// <summary>A <see cref="Microsoft.Maui.Controls.Cell"/> with a label and an on/off switch.</summary>
[Obsolete("The controls which use SwitchCell (ListView and TableView) are obsolete. Please use CollectionView instead.")]
#if WINDOWS || ANDROID || IOS || MACCATALYST || TIZEN
#pragma warning disable CS0618 // Type or member is obsolete
[ElementHandler(typeof(Handlers.Compatibility.SwitchCellRenderer))]
#pragma warning restore CS0618 // Type or member is obsolete
#endif
public class SwitchCell : Cell
{
/// <summary>Bindable property for <see cref="On"/>.</summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Cells/TextCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Microsoft.Maui.Controls
{
/// <summary>A <see cref="Microsoft.Maui.Controls.Cell"/> with primary <see cref="Microsoft.Maui.Controls.TextCell.Text"/> and <see cref="Microsoft.Maui.Controls.TextCell.Detail"/> text.</summary>
[Obsolete("The controls which use TextCell (ListView and TableView) are obsolete. Please use CollectionView instead.")]
#if WINDOWS || ANDROID || IOS || MACCATALYST
#pragma warning disable CS0618 // Type or member is obsolete
[ElementHandler(typeof(Handlers.Compatibility.TextCellRenderer))]
#pragma warning restore CS0618 // Type or member is obsolete
#endif
public class TextCell : Cell, ICommandElement
{
/// <summary>Bindable property for <see cref="Command"/>.</summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Controls/src/Core/Cells/ViewCell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Microsoft.Maui.Controls
/// <summary>A <see cref="Microsoft.Maui.Controls.Cell"/> containing a developer-defined <see cref="Microsoft.Maui.Controls.View"/>.</summary>
[Obsolete("The controls which use ViewCell (ListView and TableView) are obsolete. Please use CollectionView instead.")]
[ContentProperty("View")]
#if WINDOWS || IOS || MACCATALYST || ANDROID || TIZEN
#pragma warning disable CS0618 // Type or member is obsolete
[ElementHandler(typeof(Handlers.Compatibility.ViewCellRenderer))]
#pragma warning restore CS0618 // Type or member is obsolete
#endif
public class ViewCell : Cell
{
View _view;
Expand Down
8 changes: 3 additions & 5 deletions src/Controls/src/Core/CheckBox/CheckBox.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@ static CheckBox()
// Register dependency: Command depends on CommandParameter for CanExecute evaluation
// See https://github.com/dotnet/maui/issues/31939
CommandProperty.DependsOn(CommandParameterProperty);
RemapForControls();
}

private new static void RemapForControls()
{
VisualElement.RemapForControls();
// Force VisualElement's static constructor to run first so base-level
// mapper remappings are applied before these Control-specific ones.
RemappingHelper.EnsureBaseTypeRemapped(typeof(CheckBox), typeof(VisualElement));

CheckBoxHandler.Mapper.ReplaceMapping<ICheckBox, ICheckBoxHandler>(nameof(Color), MapColor);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Controls/src/Core/CheckBox/CheckBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ namespace Microsoft.Maui.Controls
/// Use the <see cref="IsChecked"/> property to determine or set the state.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
[ElementHandler<CheckBoxHandler>]
[ElementHandler(typeof(CheckBoxHandler))]
public partial class CheckBox : View, IElementConfiguration<CheckBox>, IBorderElement, IColorElement, ICheckBox, ICommandElement
{
readonly Lazy<PlatformConfigurationRegistry<CheckBox>> _platformConfigurationRegistry;
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/src/Core/ContentPage/ContentPage.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ namespace Microsoft.Maui.Controls
{
public partial class ContentPage
{
internal new static void RemapForControls()
static ContentPage()
{
// Force VisualElement's static constructor to run first so base-level
// mapper remappings are applied before these Control-specific ones.
RemappingHelper.EnsureBaseTypeRemapped(typeof(ContentPage), typeof(VisualElement));

PageHandler.Mapper.ReplaceMapping<ContentPage, IPageHandler>(nameof(ContentPage.HideSoftInputOnTapped), MapHideSoftInputOnTapped);
#if IOS
PageHandler.Mapper.ReplaceMapping<ContentPage, IPageHandler>(PlatformConfiguration.iOSSpecific.Page.PrefersHomeIndicatorAutoHiddenProperty.PropertyName, MapPrefersHomeIndicatorAutoHidden);
Expand Down
2 changes: 2 additions & 0 deletions src/Controls/src/Core/ContentView/ContentView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Diagnostics;
using Microsoft.Maui;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;
using Microsoft.Maui.Layouts;

namespace Microsoft.Maui.Controls
Expand All @@ -15,6 +16,7 @@ namespace Microsoft.Maui.Controls
/// </remarks>
[ContentProperty("Content")]
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
[ElementHandler(typeof(ContentViewHandler))]
public partial class ContentView : TemplatedView, IContentView, ISafeAreaView2, ISafeAreaElement
{
/// <summary>Bindable property for <see cref="Content"/>.</summary>
Expand Down
6 changes: 5 additions & 1 deletion src/Controls/src/Core/DatePicker/DatePicker.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ namespace Microsoft.Maui.Controls
{
public partial class DatePicker
{
internal static new void RemapForControls()
static DatePicker()
{
// Force VisualElement's static constructor to run first so base-level
// mapper remappings are applied before these Control-specific ones.
RemappingHelper.EnsureBaseTypeRemapped(typeof(DatePicker), typeof(VisualElement));

// Adjust the mappings to preserve Controls.DatePicker legacy behaviors
#if IOS
DatePickerHandler.Mapper.ReplaceMapping<DatePicker, IDatePickerHandler>(PlatformConfiguration.iOSSpecific.DatePicker.UpdateModeProperty.PropertyName, MapUpdateMode);
Expand Down
13 changes: 13 additions & 0 deletions src/Controls/src/Core/DatePicker/DatePicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Diagnostics;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Graphics;
using Microsoft.Maui.Handlers;

namespace Microsoft.Maui.Controls
{
Expand All @@ -15,6 +16,7 @@ namespace Microsoft.Maui.Controls
/// specified by <see cref="MinimumDate"/> and <see cref="MaximumDate"/>. The selected date is stored in the <see cref="Date"/> property.
/// </remarks>
[DebuggerDisplay("{GetDebuggerDisplay(), nq}")]
[ElementHandler(typeof(DatePickerHandler))]
public partial class DatePicker : View, IFontElement, ITextElement, IElementConfiguration<DatePicker>, IDatePicker
{
/// <summary>Bindable property for <see cref="Format"/>.</summary>
Expand Down Expand Up @@ -346,5 +348,16 @@ private protected override string GetDebuggerDisplay()
{
return $"{base.GetDebuggerDisplay()}, Date = {Date}";
}

internal override bool TrySetValue(string text)
{
if (DateTime.TryParse(text, out DateTime dpResult))
{
Date = dpResult;
return true;
}

return false;
}
}
}
6 changes: 5 additions & 1 deletion src/Controls/src/Core/Editor/Editor.Mapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ namespace Microsoft.Maui.Controls
{
public partial class Editor
{
internal static new void RemapForControls()
static Editor()
{
// Force VisualElement's static constructor to run first so base-level
// mapper remappings are applied before these Control-specific ones.
RemappingHelper.EnsureBaseTypeRemapped(typeof(Editor), typeof(VisualElement));

// Adjust the mappings to preserve Controls.Editor legacy behaviors
#if WINDOWS
EditorHandler.Mapper.ReplaceMapping<Editor, IEditorHandler>(PlatformConfiguration.WindowsSpecific.InputView.DetectReadingOrderFromContentProperty.PropertyName, MapDetectReadingOrderFromContent);
Expand Down
Loading
Loading