From maui-skills
Guides creating and customizing .NET MAUI handlers for platform-specific native views using PropertyMapper, AppendToMapping, CommandMapper, and partial classes. Use for custom controls and renderers.
npx claudepluginhub davidortinau/maui-skills --plugin maui-skillsThis skill uses the workspace's default tool permissions.
| Scenario | Approach |
Guides calling platform-specific native APIs in .NET MAUI apps using partial classes, conditional compilation, multi-targeting, and DI patterns for Android, iOS, Mac Catalyst, Windows.
Configures dependency injection in .NET MAUI: registers services in MauiProgram.cs, selects lifetimes, enables constructor injection, Shell auto-resolution, and platform-specific implementations.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Share bugs, ideas, or general feedback.
| Scenario | Approach |
|---|---|
| Change how a built-in control looks/behaves on one platform | Customize — use AppendToMapping / PrependToMapping |
| Need the change on only some instances of a control | Customize — subclass the control + type-check in mapper |
| Need a completely new cross-platform control with native backing | Create new handler with partial classes |
⚠️ Prefer
AppendToMappingoverModifyMapping.ModifyMappingreplaces the default mapper action entirely — if the framework adds behaviour in a future release, your override silently drops it.
Every instance of the control is affected. Guard with a subclass check for instance-specific behaviour:
// ❌ Removes borders from EVERY Entry in the app
EntryHandler.Mapper.AppendToMapping("NoBorder", (handler, view) =>
{
#if ANDROID
handler.PlatformView.Background = null;
#endif
});
// ✅ Only affects BorderlessEntry instances
EntryHandler.Mapper.AppendToMapping("NoBorder", (handler, view) =>
{
if (view is not BorderlessEntry) return;
#if ANDROID
handler.PlatformView.Background = null;
#endif
});
HandlerChangingFailing to remove native event handlers causes memory leaks because the native view may outlive the managed wrapper.
// ❌ Subscribes but never unsubscribes — leaks
entry.HandlerChanged += (s, e) =>
{
#if ANDROID
((Entry)s!).Handler!.PlatformView.As<Android.Widget.EditText>()!
.FocusChange += OnNativeFocusChange;
#endif
};
// ✅ Pair subscribe in HandlerChanged with unsubscribe in HandlerChanging
entry.HandlerChanged += OnHandlerChanged;
entry.HandlerChanging += OnHandlerChanging;
Namespace and class name must match exactly across the shared handler file and every platform file. A mismatch silently creates separate classes — no compiler error, just a handler that does nothing on that platform.
using placementThe using PlatformView = ... aliases must be at the top of the shared
handler file (not the platform files) so the ViewHandler<TControl, TPlatformView>
base-class generic resolves correctly per platform.
// ✅ Top of Handlers/VideoPlayerHandler.cs
#if ANDROID
using PlatformView = Android.Widget.VideoView;
#elif IOS || MACCATALYST
using PlatformView = AVKit.AVPlayerViewController;
#elif WINDOWS
using PlatformView = Microsoft.UI.Xaml.Controls.MediaPlayerElement;
#endif
CreatePlatformView()Each platform partial must override CreatePlatformView(). Omitting it
produces a compile error — but the error message points at the base class,
not your handler, making it confusing to debug.
| Method | Risk | Use when |
|---|---|---|
AppendToMapping | Low — runs after default | Adding behaviour without breaking defaults |
PrependToMapping | Low — runs before default | Setting initial state that the default can override |
ModifyMapping | ⚠️ High — replaces default | You intentionally want to suppress the framework's mapper logic |
| Mapper | Purpose | Pattern |
|---|---|---|
PropertyMapper | Sync a bindable property to the native view | Runs whenever the property value changes |
CommandMapper | Fire-and-forget action from control → handler | Runs once per invocation, no return value |
⚠️ Don't put property sync logic in
CommandMapper— it won't re-run when the property changes, leading to stale native views.
View (or appropriate base)using PlatformView = ... aliasesViewHandler<TControl, PlatformView>PropertyMapper maps every bindable propertyCreatePlatformView()MauiProgram.cs via ConfigureMauiHandlersHandlerChanging