maui-custom-handlers について
maui-custom-handlersは、.NET MAUIのハンドラーをカスタマイズ、作成する開発者向けのリファレンススキルです。マッパーの選択やプラットフォーム別の構成に加え、全体への適用やネイティブイベントのリークなどの注意点を説明します。
主な機能
- 01既存ハンドラーの変更と新規作成を選択
- 02AppendToMapping、PrependToMapping、ModifyMappingを比較
- 03PropertyMapperとCommandMapperの違いを解説
- 04プラットフォーム別partialでハンドラーを構成
- 05注意点と実装チェックリストを提示
活用シーン
- 01特定プラットフォームで組み込みコントロールを調整
- 02選択したコントロールだけにハンドラー動作を適用
- 03ネイティブ実装を持つクロスプラットフォームコントロールを作成
- 04ハンドラー登録やプラットフォーム別partialの問題を調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ davidortinau/ maui-skills --skill maui-custom-handlersインストール後、Claude Code で/maui-custom-handlersを入力して読み込まれたか確認します。
同梱ファイルreferences/custom-handlers-api.md
詳細
.NET MAUI カスタムハンドラー
判断:既存のものをカスタマイズするか、新しく作成するか
| シナリオ | 方法 |
|---|---|
| あるプラットフォームで、組み込みコントロールの外観や動作を変更する | カスタマイズ — AppendToMapping / PrependToMapping を使用します |
| コントロールの一部のインスタンスだけに変更を適用する | カスタマイズ — コントロールをサブクラス化し、マッパーで型をチェックします |
| ネイティブ側の実装を持つ、まったく新しいクロスプラットフォームコントロールが必要 | 新しいハンドラーを部分クラスで作成します |
⚠️
ModifyMappingよりもAppendToMappingを優先してください。ModifyMappingはデフォルトのマッパーアクションを完全に置き換えます。将来のリリースでフレームワークの動作が追加されても、オーバーライドによってそれが暗黙のうちに失われます。
注意点とよくある間違い
マッパーのカスタマイズはグローバルに適用されます
コントロールのすべてのインスタンスに影響します。インスタンス固有の動作にするには、サブクラスのチェックを使用します。
// ❌ 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
});
HandlerChanging でネイティブイベントの購読を解除します
ネイティブイベントハンドラーを削除しないと、ネイティブビューがマネージドラッパーより長く存続する可能性があるため、メモリリークが発生します。
// ❌ 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;
部分クラスの名前/名前空間の不一致
共有ハンドラーファイルと各プラットフォームファイルで、名前空間とクラス名を完全に一致させる必要があります。不一致があると、別々のクラスが暗黙のうちに作成されます。コンパイルエラーは発生せず、そのプラットフォームではハンドラーが何もしません。
条件付き using の配置
using PlatformView = ... エイリアスは、共有ハンドラーファイルの先頭(プラットフォームファイルではありません)に配置してください。これにより、ViewHandler<TControl, TPlatformView> 基底クラスのジェネリックが、プラットフォームごとに正しく解決されます。
// ✅ 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() の不足
各プラットフォームの部分クラスは、必ず CreatePlatformView() をオーバーライドする必要があります。省略するとコンパイルエラーになりますが、エラーメッセージはハンドラーではなく基底クラスを指すため、デバッグが難しくなります。
マッパーメソッドの選択
| メソッド | リスク | 使用する場面 |
|---|---|---|
AppendToMapping | 低 — デフォルトの後に実行 | デフォルトを損なわずに動作を追加する場合 |
PrependToMapping | 低 — デフォルトの前に実行 | デフォルトで上書きされる可能性のある初期状態を設定する場合 |
ModifyMapping | ⚠️ 高 — デフォルトを置き換える | フレームワークのマッパーロジックを意図的に抑制する場合 |
PropertyMapper と CommandMapper
| マッパー | 目的 | 動作パターン |
|---|---|---|
PropertyMapper | バインド可能なプロパティをネイティブビューに同期する | プロパティ値が変更されるたびに実行されます |
CommandMapper | コントロールからハンドラーへの、結果を返さないアクションを実行する | 呼び出しごとに一度実行され、戻り値はありません |
⚠️
CommandMapperにプロパティ同期ロジックを入れないでください。プロパティが変更されても再実行されないため、ネイティブビューの状態が古いままになります。
チェックリスト — 新しいハンドラー
- クロスプラットフォームコントロールが
View(または適切な基底クラス)を継承している - 共有ハンドラーファイルに条件付き
using PlatformView = ...エイリアスがある - ハンドラーが
ViewHandler<TControl, PlatformView>を継承している -
PropertyMapperがすべてのバインド可能なプロパティをマッピングしている - 各プラットフォームの部分クラスが
CreatePlatformView()をオーバーライドしている - すべての部分クラスで名前空間とクラス名が一致している
-
MauiProgram.csのConfigureMauiHandlersでハンドラーが登録されている -
HandlerChangingでネイティブイベントの購読が解除されている