MAUI Custom Handlers
Use this skill when a MAUI visual element needs native platform behavior. Keep
handlers focused on views. Route non-visual APIs to platform services and route
large native SDK surfaces to slim bindings.
Response Checklist
- Use handler language explicitly:
AppendToMapping, PropertyMapper,
CommandMapper, and ViewHandler.
- For renderer migration, map subscription/setup to
ConnectHandler and cleanup
to DisconnectHandler.
- Keep mapper changes scoped to the intended control type, not all controls.
Decision Tree
| Need |
Prefer |
| Tweak an existing control for the whole app |
EntryHandler.Mapper.AppendToMapping in MauiProgram.cs (or the matching concrete control handler mapper) |
| Tweak only specific control instances |
Subclass the control and guard mapper logic with if (view is MyControl) |
| Add a reusable cross-platform control |
Custom ViewHandler<TVirtualView,TPlatformView> |
| Map bindable properties to native properties |
PropertyMapper entries |
| Invoke actions such as play/pause/scroll |
CommandMapper entries |
| Use camera, sensors, payment, health, or background APIs |
Platform service through DI |
| Wrap a third-party Android/iOS/macOS SDK |
Slim binding plus a platform service facade |
Workflow
Inspect the target frameworks and existing UI architecture.
Define the cross-platform view API first: bindable properties, commands, and
events that make sense to app code.
Choose global mapper customization, subclass-scoped mapper customization, or a
full custom handler.
Put native implementation in partial handler files or #if guarded blocks for
the exact platforms supported by the project.
Register handlers in MauiProgram.cs:
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<CameraPreview, CameraPreviewHandler>();
});
Use ConnectHandler to subscribe native events and allocate native resources.
Use DisconnectHandler to unsubscribe, stop timers, clear delegates, and
dispose only native objects owned by the handler. Call
base.DisconnectHandler(platformView) as the final statement so the base
handler clears its own state.
Build each targeted platform and verify the behavior with UI automation or
DevFlow when available.
Scoped Mapper Customization
For an existing MAUI control, avoid an unscoped global mapper when only one
control instance should change:
public class BorderlessEntry : Entry
{
}
EntryHandler.Mapper.AppendToMapping("Borderless", (handler, view) =>
{
if (view is not BorderlessEntry)
return;
#if ANDROID
handler.PlatformView.Background = null;
#elif IOS || MACCATALYST
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#elif WINDOWS
handler.PlatformView.BorderThickness = new Microsoft.UI.Xaml.Thickness(0);
#endif
});
Use a unique mapping key. Do not repeatedly append the same mapping from page
constructors; register once during app startup or from a guarded initialization
path.
Custom Handler Shape
public class MeterView : View
{
public static readonly BindableProperty ValueProperty =
BindableProperty.Create(nameof(Value), typeof(double), typeof(MeterView), 0d);
public double Value
{
get => (double)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
}
public partial class MeterViewHandler
{
public static readonly IPropertyMapper<MeterView, MeterViewHandler> Mapper =
new PropertyMapper<MeterView, MeterViewHandler>(ViewHandler.ViewMapper)
{
[nameof(MeterView.Value)] = MapValue
};
public MeterViewHandler() : base(Mapper)
{
}
public static partial void MapValue(MeterViewHandler handler, MeterView view);
}
Implement CreatePlatformView, ConnectHandler, DisconnectHandler, and
mapping partials per platform. Put the ViewHandler<TVirtualView, TPlatformView>
base class on the platform partial so native types stay out of shared files:
// Platforms/Android/MeterViewHandler.android.cs
public partial class MeterViewHandler : ViewHandler<MeterView, Android.Widget.FrameLayout>
{
protected override Android.Widget.FrameLayout CreatePlatformView() => new(Context);
public static partial void MapValue(MeterViewHandler handler, MeterView view)
{
// Update handler.PlatformView from view.Value.
}
}
Property and Command Mappers
- Use
PropertyMapper for state that should update when a bindable property
changes.
- Use
CommandMapper for imperative requests such as Play, Pause,
ScrollTo, or Reload.
- Mapper methods should be idempotent. They may be called more than once.
- Validate
handler.PlatformView and handler.VirtualView assumptions through
types, not broad try/catch blocks.
Renderer Migration Notes
When replacing Xamarin.Forms renderers:
- Move renderer logic that creates native controls into
CreatePlatformView.
- Move
OnElementChanged subscriptions into ConnectHandler.
- Move renderer cleanup into
DisconnectHandler, and end with
base.DisconnectHandler(platformView).
- Replace
OnElementPropertyChanged switch statements with PropertyMapper
entries.
- Replace renderer actions with
CommandMapper entries or view methods that
call Handler?.Invoke.
Validation Checklist
- The handler is registered in one startup location.
- Mapper keys are unique and scoped when customization should not be global.
- Native event subscriptions are unsubscribed in
DisconnectHandler.
- No platform namespace leaks into shared code unintentionally.
- The implementation builds for every target framework it is included in.
- Non-visual SDK work is not hidden in a handler.
1---2name: maui-custom-handlers3description: Implement or migrate MAUI visual handlers. USE FOR: handler mappers, type guards, unique mapper keys, renderer-to-handler migration, `ConnectHandler`/`DisconnectHandler`, `PropertyMapper`, `CommandMapper`, custom `ViewHandler`, platform partials, `CreatePlatformView`, `ConfigureMauiHandlers`. DO NOT USE FOR: non-visual APIs, full Xamarin migration, native SDK bindings.4---56# MAUI Custom Handlers78Use this skill when a MAUI visual element needs native platform behavior. Keep9handlers focused on views. Route non-visual APIs to platform services and route10large native SDK surfaces to slim bindings.1112## Response Checklist1314- Use handler language explicitly: `AppendToMapping`, `PropertyMapper`,15 `CommandMapper`, and `ViewHandler`.16- For renderer migration, map subscription/setup to `ConnectHandler` and cleanup17 to `DisconnectHandler`.18- Keep mapper changes scoped to the intended control type, not all controls.1920## Decision Tree2122| Need | Prefer |23| --- | --- |24| Tweak an existing control for the whole app | `EntryHandler.Mapper.AppendToMapping` in `MauiProgram.cs` (or the matching concrete control handler mapper) |25| Tweak only specific control instances | Subclass the control and guard mapper logic with `if (view is MyControl)` |26| Add a reusable cross-platform control | Custom `ViewHandler<TVirtualView,TPlatformView>` |27| Map bindable properties to native properties | `PropertyMapper` entries |28| Invoke actions such as play/pause/scroll | `CommandMapper` entries |29| Use camera, sensors, payment, health, or background APIs | Platform service through DI |30| Wrap a third-party Android/iOS/macOS SDK | Slim binding plus a platform service facade |3132## Workflow33341. Inspect the target frameworks and existing UI architecture.352. Define the cross-platform view API first: bindable properties, commands, and36 events that make sense to app code.373. Choose global mapper customization, subclass-scoped mapper customization, or a38 full custom handler.394. Put native implementation in partial handler files or `#if` guarded blocks for40 the exact platforms supported by the project.415. Register handlers in `MauiProgram.cs`:4243 ```csharp44 builder.ConfigureMauiHandlers(handlers =>45 {46 handlers.AddHandler<CameraPreview, CameraPreviewHandler>();47 });48 ```49506. Use `ConnectHandler` to subscribe native events and allocate native resources.517. Use `DisconnectHandler` to unsubscribe, stop timers, clear delegates, and52 dispose only native objects owned by the handler. Call53 `base.DisconnectHandler(platformView)` as the final statement so the base54 handler clears its own state.558. Build each targeted platform and verify the behavior with UI automation or56 DevFlow when available.5758## Scoped Mapper Customization5960For an existing MAUI control, avoid an unscoped global mapper when only one61control instance should change:6263```csharp64public class BorderlessEntry : Entry65{66}6768EntryHandler.Mapper.AppendToMapping("Borderless", (handler, view) =>69{70 if (view is not BorderlessEntry)71 return;7273#if ANDROID74 handler.PlatformView.Background = null;75#elif IOS || MACCATALYST76 handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;77#elif WINDOWS78 handler.PlatformView.BorderThickness = new Microsoft.UI.Xaml.Thickness(0);79#endif80});81```8283Use a unique mapping key. Do not repeatedly append the same mapping from page84constructors; register once during app startup or from a guarded initialization85path.8687## Custom Handler Shape8889```csharp90public class MeterView : View91{92 public static readonly BindableProperty ValueProperty =93 BindableProperty.Create(nameof(Value), typeof(double), typeof(MeterView), 0d);9495 public double Value96 {97 get => (double)GetValue(ValueProperty);98 set => SetValue(ValueProperty, value);99 }100}101102public partial class MeterViewHandler103{104 public static readonly IPropertyMapper<MeterView, MeterViewHandler> Mapper =105 new PropertyMapper<MeterView, MeterViewHandler>(ViewHandler.ViewMapper)106 {107 [nameof(MeterView.Value)] = MapValue108 };109110 public MeterViewHandler() : base(Mapper)111 {112 }113114 public static partial void MapValue(MeterViewHandler handler, MeterView view);115}116```117118Implement `CreatePlatformView`, `ConnectHandler`, `DisconnectHandler`, and119mapping partials per platform. Put the `ViewHandler<TVirtualView, TPlatformView>`120base class on the platform partial so native types stay out of shared files:121122```csharp123// Platforms/Android/MeterViewHandler.android.cs124public partial class MeterViewHandler : ViewHandler<MeterView, Android.Widget.FrameLayout>125{126 protected override Android.Widget.FrameLayout CreatePlatformView() => new(Context);127128 public static partial void MapValue(MeterViewHandler handler, MeterView view)129 {130 // Update handler.PlatformView from view.Value.131 }132}133```134135## Property and Command Mappers136137- Use `PropertyMapper` for state that should update when a bindable property138 changes.139- Use `CommandMapper` for imperative requests such as `Play`, `Pause`,140 `ScrollTo`, or `Reload`.141- Mapper methods should be idempotent. They may be called more than once.142- Validate `handler.PlatformView` and `handler.VirtualView` assumptions through143 types, not broad try/catch blocks.144145## Renderer Migration Notes146147When replacing Xamarin.Forms renderers:1481491. Move renderer logic that creates native controls into `CreatePlatformView`.1502. Move `OnElementChanged` subscriptions into `ConnectHandler`.1513. Move renderer cleanup into `DisconnectHandler`, and end with152 `base.DisconnectHandler(platformView)`.1534. Replace `OnElementPropertyChanged` switch statements with `PropertyMapper`154 entries.1555. Replace renderer actions with `CommandMapper` entries or view methods that156 call `Handler?.Invoke`.157158## Validation Checklist159160- The handler is registered in one startup location.161- Mapper keys are unique and scoped when customization should not be global.162- Native event subscriptions are unsubscribed in `DisconnectHandler`.163- No platform namespace leaks into shared code unintentionally.164- The implementation builds for every target framework it is included in.165- Non-visual SDK work is not hidden in a handler.