
Maui Custom Handlers
- 30 installs
- 163 repo stars
- Updated July 6, 2026
- davidortinau/maui-skills
Creates custom .NET MAUI handlers, customizes existing handlers with property mappers, and implements platform-specific native views.
About
Guides creating custom .NET MAUI handlers, customizing existing handlers via property mappers, and implementing platform-specific native views. A developer uses it when they need native-level control over how MAUI controls render on each platform.
- Custom handlers and property mapper customization
- Platform-specific native view implementation
Maui Custom Handlers by the numbers
- 30 all-time installs (skills.sh)
- Ranked #665 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidortinau/maui-skills --skill maui-custom-handlersAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 30 |
|---|---|
| repo stars | ★ 163 |
| Last updated | July 6, 2026 |
| Repository | davidortinau/maui-skills ↗ |
What it does
Creates custom .NET MAUI handlers, customizes existing handlers with property mappers, and implements platform-specific native views.
Files
.NET MAUI Custom Handlers
Decision: Customize Existing vs. Create New
| 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 `AppendToMapping` overModifyMapping.ModifyMappingreplaces
the default mapper action entirely — if the framework adds behaviour in a
future release, your override silently drops it.
---
Gotchas & Common Mistakes
Mapper customizations are global
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
});Unsubscribe native events in HandlerChanging
Failing 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;Partial class name/namespace mismatch
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.
Conditional using placement
The 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;
#endifMissing 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.
---
Mapper Method Selection
| 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 |
---
PropertyMapper vs. CommandMapper
| 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 whenthe property changes, leading to stale native views.
---
Checklist — New Handler
- [ ] Cross-platform control inherits
View(or appropriate base) - [ ] Shared handler file has conditional
using PlatformView = ...aliases - [ ] Handler inherits
ViewHandler<TControl, PlatformView> - [ ]
PropertyMappermaps every bindable property - [ ] Each platform partial overrides
CreatePlatformView() - [ ] Namespace + class name identical across all partial files
- [ ] Handler registered in
MauiProgram.csviaConfigureMauiHandlers - [ ] Native event subscriptions cleaned up in
HandlerChanging
Custom Handlers API Reference
Mapper Methods
| Method | When it runs |
|---|---|
PrependToMapping | Before the default mapper action |
ModifyMapping | Replaces the default mapper action |
AppendToMapping | After the default mapper action |
Basic Pattern
// In MauiProgram.cs or a startup helper
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping("NoBorder", (handler, view) =>
{
#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
});handler.PlatformView— the native view (AndroidEditText, iOSUITextField, etc.).handler.VirtualView— the cross-platform .NET MAUI control.
Instance-Specific Customization
Subclass the control and check the type inside the mapper:
public class BorderlessEntry : Entry { }
EntryHandler.Mapper.AppendToMapping("NoBorder", (handler, view) =>
{
if (view is not BorderlessEntry)
return;
#if ANDROID
handler.PlatformView.Background = null;
#endif
});Handler Lifecycle Events
Use HandlerChanged / HandlerChanging to subscribe and unsubscribe to native events on a per-instance basis:
var entry = new Entry();
entry.HandlerChanged += OnHandlerChanged;
entry.HandlerChanging += OnHandlerChanging;
void OnHandlerChanged(object? sender, EventArgs e)
{
if (sender is Entry { Handler.PlatformView: { } platformView })
{
#if ANDROID
platformView.FocusChange += OnNativeFocusChange;
#endif
}
}
void OnHandlerChanging(object? sender, HandlerChangingEventArgs e)
{
if (e.OldHandler?.PlatformView is { } oldView)
{
#if ANDROID
oldView.FocusChange -= OnNativeFocusChange;
#endif
}
}---
Creating a New Handler
Step 1 — Cross-Platform Control
namespace MyApp.Controls;
public class VideoPlayer : View
{
public static readonly BindableProperty SourceProperty =
BindableProperty.Create(nameof(Source), typeof(string), typeof(VideoPlayer));
public string? Source
{
get => (string?)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public event EventHandler? PlaybackCompleted;
internal void OnPlaybackCompleted() => PlaybackCompleted?.Invoke(this, EventArgs.Empty);
}Step 2 — Shared Handler with Mappers
Create a partial class so platform files can supply the native view:
// 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
namespace MyApp.Handlers;
public partial class VideoPlayerHandler : ViewHandler<VideoPlayer, PlatformView>
{
public static IPropertyMapper<VideoPlayer, VideoPlayerHandler> PropertyMapper =
new PropertyMapper<VideoPlayer, VideoPlayerHandler>(ViewMapper)
{
[nameof(VideoPlayer.Source)] = MapSource,
};
public static CommandMapper<VideoPlayer, VideoPlayerHandler> CommandMapper =
new(ViewCommandMapper);
public VideoPlayerHandler()
: base(PropertyMapper, CommandMapper) { }
// Each platform partial implements CreatePlatformView() and MapSource()
}Step 3 — Platform Implementations
Each platform file completes the partial class.
// Handlers/VideoPlayerHandler.Android.cs
namespace MyApp.Handlers;
public partial class VideoPlayerHandler
{
protected override PlatformView CreatePlatformView() => new(Context);
public static void MapSource(VideoPlayerHandler handler, VideoPlayer control)
{
if (!string.IsNullOrEmpty(control.Source))
{
handler.PlatformView.SetVideoURI(
Android.Net.Uri.Parse(control.Source));
}
}
}// Handlers/VideoPlayerHandler.iOS.cs
namespace MyApp.Handlers;
public partial class VideoPlayerHandler
{
protected override PlatformView CreatePlatformView() => new();
public static void MapSource(VideoPlayerHandler handler, VideoPlayer control)
{
if (!string.IsNullOrEmpty(control.Source))
{
var url = Foundation.NSUrl.FromString(control.Source);
handler.PlatformView.Player = new AVFoundation.AVPlayer(url);
}
}
}Step 4 — Register the Handler
// MauiProgram.cs
builder.ConfigureMauiHandlers(handlers =>
{
handlers.AddHandler<VideoPlayer, VideoPlayerHandler>();
});