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, andViewHandler. - For renderer migration, map subscription/setup to
ConnectHandlerand cleanup toDisconnectHandler. - 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
#ifguarded blocks for the exact platforms supported by the project. -
Register handlers in
MauiProgram.cs:builder.ConfigureMauiHandlers(handlers => { handlers.AddHandler<CameraPreview, CameraPreviewHandler>(); }); -
Use
ConnectHandlerto subscribe native events and allocate native resources. -
Use
DisconnectHandlerto unsubscribe, stop timers, clear delegates, and dispose only native objects owned by the handler. Callbase.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
PropertyMapperfor state that should update when a bindable property changes. - Use
CommandMapperfor imperative requests such asPlay,Pause,ScrollTo, orReload. - Mapper methods should be idempotent. They may be called more than once.
- Validate
handler.PlatformViewandhandler.VirtualViewassumptions 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
OnElementChangedsubscriptions intoConnectHandler. - Move renderer cleanup into
DisconnectHandler, and end withbase.DisconnectHandler(platformView). - Replace
OnElementPropertyChangedswitch statements withPropertyMapperentries. - Replace renderer actions with
CommandMapperentries or view methods that callHandler?.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.