maui-custom-handlers
npx skills add https://github.com/davidortinau/maui-skills --skill maui-custom-handlers
Agent 安装分布
Skill 文档
.NET MAUI Custom Handlers
Use this skill when creating or customizing .NET MAUI handlersâthe layer that connects cross-platform controls to platform-specific native views.
Workflow 1 â Customize Existing Handlers
Mapper methods let you change how any existing control renders on each platform without subclassing the handler itself.
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.- Customizations are global: every instance of the control is affected.
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
}
}
Always unsubscribe in
HandlerChangingto prevent memory leaks when the handler is disconnected or replaced.
Workflow 2 â Create a New Handler
Use this when you need a completely new cross-platform control backed by platform-specific native views.
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);
}
// Use events or commands for actions the native view triggers
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>();
});
Gotchas & Best Practices
- Mapper customizations are global â they affect every instance of the control. Use a subclass check for instance-specific behavior.
- Unsubscribe in
HandlerChangingâ failing to remove native event handlers causes memory leaks because the native view may outlive the managed wrapper. - Namespace and class name must match across all partial files (shared + each platform). A mismatch silently creates separate classes.
- Conditional
usingforPlatformViewmust be at the top of the shared handler file so the base class generic resolves per platform. CreatePlatformView()is required in each platform partial â the handler won’t compile without it.- Use
PropertyMapperfor bindable properties andCommandMapperfor fire-and-forget actions sent from the control to the handler.