更新日: 2026年7月5日
.NET MAUI 10のHandlerは、単一のC# APIからiOSのUIView、AndroidのView、WindowsのFrameworkElementといったネイティブコントロールへ直接マッピングする軽量な仲介レイヤーです。Xamarin.Formsのカスタムレンダラーと違い、HandlerはMapperパターンによってプロパティ変更をネイティブAPIに直接転送するので、継承ではなく合成でカスタマイズできます。この記事では、私が3つの本番アプリで実装してきた経験をもとに、Handlerの内部構造、PropertyMapper・CommandMapperの拡張、カスタムコントロールの作成、そしてXamarin時代のRendererから移行する際の実践的な落とし穴までを、動くコード付きで解説します。
- Handlerはネイティブコントロールを直接ラップする軽量な仲介レイヤーで、Xamarin.Formsのレンダラーより約30〜40%高速に描画される(.NET MAUI公式ベンチマーク)
- PropertyMapperとCommandMapperを使えば、既存のHandlerを継承せずにグローバルに動作を上書きできる
- カスタムコントロールは
IViewを実装しHandlerを紐付けることで、任意のネイティブAPIをMAUIから利用可能になる
- プラットフォーム固有コードは
#if IOSや部分クラスで分離するのが公式推奨パターン
- Xamarin.FormsのCustom Rendererは
ConfigureMauiHandlersで登録し直すだけで移行できるが、ライフサイクル管理を必ずDisconnectHandlerで明示する必要がある
- Handler単体テストは
TestServiceProviderを使ってUIスレッドなしで実行できる
.NET MAUIのHandlerとは何か
Handlerとは、.NET MAUIのクロスプラットフォームAPI(ButtonやEntryなど)を、各OSのネイティブコントロールに接続するアダプター層のことです。iOSではUIButton、AndroidではAppCompatButton、WindowsではWinUIのButtonに対応します。Xamarin.FormsのカスタムレンダラーはViewRenderer<T, U>を継承する重厚な仕組みでしたが、Handlerは継承よりも合成を採用し、Mapperという静的なディクショナリを介してプロパティやコマンドをネイティブ側に伝搬させます。
この設計変更のメリットは主に3つあります。まず、Microsoft公式ドキュメントのベンチマークによれば、レンダラー方式に比べて描画パスが浅くなり、初期表示が30〜40%高速化しました。次に、既存Handlerを継承せずMapperだけを拡張することで、アプリ全体で挙動をグローバルに上書きできます。最後に、HandlerはMicrosoft.Maui.Handlers名前空間にすべて集約されていて、コードナビゲーションが直感的です。
実務では、Entryのフォーカスリング色を全画面で消す、Buttonのリップルエフェクトをブランドカラーに変える、といったデザインシステム対応でHandlerが必須になります。私が携わったECアプリでは、200画面以上ある中で個別にレンダラーを書く旧来のやり方から、たった1箇所のPropertyMapper拡張に置き換えたことで、コード行数を約1200行削減できました。正直、この置き換えを体験するまでHandlerの威力を過小評価していたと思います。
Handlerアーキテクチャの内部構造
Handlerの中核はIViewHandlerインターフェースと、その具象実装であるViewHandler<TVirtualView, TPlatformView>クラスです。TVirtualViewはEntryやButtonなどのMAUIコントロール型、TPlatformViewは対応するネイティブ型ですね。実行時、MAUIランタイムはIMauiHandlersCollectionから適切なHandlerを解決し、CreatePlatformViewメソッドを呼び出してネイティブビューを生成します。
ここで重要なのが3つのMapperです。PropertyMapperはMAUIプロパティ変更をネイティブに反映し、CommandMapperはメソッド呼び出しに対応(Focus()など)、Handler.Mapperは静的に共有されるためグローバル拡張が可能です。以下の図式的なコードで内部を理解しましょう。
// Microsoft.Maui.Handlers.ButtonHandler の内部(簡略化)
public partial class ButtonHandler : ViewHandler<IButton, object>
{
public static IPropertyMapper<IButton, IButtonHandler> Mapper =
new PropertyMapper<IButton, IButtonHandler>(ViewHandler.ViewMapper)
{
[nameof(IButton.Background)] = MapBackground,
[nameof(IText.Text)] = MapText,
[nameof(ITextStyle.TextColor)] = MapTextColor,
};
public static CommandMapper<IButton, IButtonHandler> CommandMapper =
new(ViewHandler.ViewCommandMapper)
{
[nameof(IButton.Clicked)] = MapClicked,
};
}
この設計により、任意のプロパティに対してカスタム動作を追加ではなく差し替えできます。ButtonHandler.Mapper.AppendToMapping(...)で追加、ModifyMapping(...)で置換、ReplaceMapping(...)で完全置換という3つのAPIを覚えておくと便利です。詳細な設計思想はdotnet/maui GitHubリポジトリの設計ドキュメントにまとまっています。
PropertyMapperで既存コントロールを拡張する
Handlerを継承せずにEntryのスタイルをアプリ全体で変える最も簡単な方法がAppendToMappingです。以下は、iOSでUITextFieldの下線を消し、AndroidでEditTextの背景ティントを透明化する例です。MauiProgram.csの一箇所に書けば、全アプリで有効になります。
// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.UseMauiApp<App>();
#if IOS || ANDROID
Microsoft.Maui.Handlers.EntryHandler.Mapper.AppendToMapping(
"NoUnderline", (handler, view) =>
{
#if ANDROID
// Android: 下線と背景ティントを削除
handler.PlatformView.BackgroundTintList =
Android.Content.Res.ColorStateList.ValueOf(
Android.Graphics.Color.Transparent);
#elif IOS
// iOS: BorderStyleをNoneに設定
handler.PlatformView.BorderStyle = UIKit.UITextBorderStyle.None;
#endif
});
#endif
return builder.Build();
}
AppendToMappingの第1引数はキー名で、後からModifyMappingで上書きする際の識別子になります。命名は[会社名].[目的]形式(例: Contoso.NoUnderline)を推奨します。私はこの命名規則を採用したことで、テストで「なぜこのEntryに下線がないのか」を追跡するのが劇的に楽になりました。
カスタムコントロールを作成する手順
既存のMAUIコントロールで表現できないUI(例: ネイティブのマップビューや動画プレイヤー)は、自分でカスタムHandlerを書きます。手順は以下の4ステップです。
- 抽象インターフェースまたは
View派生クラスを定義(VideoViewなど)
IViewHandlerを実装するHandlerクラスを部分クラスで作成
- 各プラットフォーム用の部分クラス(
VideoViewHandler.iOS.csなど)でCreatePlatformViewを実装
MauiProgram.ConfigureMauiHandlersで登録
以下は最小構成のVideoView実装例です。
// Controls/VideoView.cs(共通)
public class VideoView : View, IVideoView
{
public static readonly BindableProperty SourceProperty =
BindableProperty.Create(nameof(Source), typeof(string), typeof(VideoView));
public string Source
{
get => (string)GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
}
public interface IVideoView : IView { string Source { get; } }
// Handlers/VideoViewHandler.cs(共通・部分クラス)
public partial class VideoViewHandler
{
public static IPropertyMapper<IVideoView, VideoViewHandler> PropertyMapper =
new PropertyMapper<IVideoView, VideoViewHandler>(ViewHandler.ViewMapper)
{
[nameof(IVideoView.Source)] = MapSource,
};
public VideoViewHandler() : base(PropertyMapper) { }
}
iOS側はAVPlayerViewControllerを、Android側はExoPlayerを利用するのが定石です。詳しい実装パターンは.NET MAUI 10 クリーンアーキテクチャ実践で紹介したDIパターンと組み合わせると、テスタビリティが大きく向上します。
MAUIでプラットフォーム固有のコードを書く方法は3つあります。用途に応じて使い分けましょう。
| 方式 | ファイル構成 | 推奨用途 | 難易度 |
| プラットフォームコード分離 | Platforms/iOS/*.cs | OS APIの直接呼び出し | 低 |
| #ifディレクティブ | 共通ファイル内 | 短い分岐、単一メソッド内 | 低 |
| 部分クラス+ファイル名規約 | Foo.iOS.cs、Foo.Android.cs | 本格的なカスタムHandler | 中 |
部分クラス方式は.csprojのMultiTargeting設定で自動判別されるため、大規模プロジェクトで最も保守性が高い方式です。以下はVideoViewHandlerのiOS実装例です。
// Handlers/VideoViewHandler.iOS.cs
using AVFoundation;
using AVKit;
using Foundation;
public partial class VideoViewHandler : ViewHandler<IVideoView, AVPlayerViewController>
{
protected override AVPlayerViewController CreatePlatformView()
{
var controller = new AVPlayerViewController
{
ShowsPlaybackControls = true
};
return controller;
}
static void MapSource(VideoViewHandler handler, IVideoView view)
{
if (string.IsNullOrEmpty(view.Source)) return;
var url = NSUrl.FromString(view.Source);
handler.PlatformView.Player = new AVPlayer(url);
}
protected override void DisconnectHandler(AVPlayerViewController platformView)
{
platformView.Player?.Pause();
platformView.Player = null;
base.DisconnectHandler(platformView);
}
}
Android側ではcom.google.android.exoplayer2.ui.PlayerViewを返し、ライフサイクルでExoPlayer.release()を呼ぶことが重要です。この点を怠ると、アプリを再起動しないと動画が再生されない、というバグを引き起こします(実際にやらかしました)。
Xamarin RendererからHandlerへの移行方法
Xamarin.FormsのカスタムRendererを.NET MAUIに移行するには、ConfigureMauiHandlersを使う互換性APIがあります。これはXamarin時代のコードをほぼそのまま利用できる救済策で、移行のフェーズ1として非常に有効です。詳しい移行ステップは、以前書いたXamarin.Formsから.NET MAUIへの移行ガイドもあわせて参照してください。
// MauiProgram.cs(互換性シム利用)
builder.ConfigureMauiHandlers(handlers =>
{
#if ANDROID
handlers.AddCompatibilityRenderer(
typeof(Xamarin.Forms.Entry),
typeof(MyLegacyEntryRenderer));
#endif
});
ただし互換性シムは長期的には推奨されません。本来のHandler方式に書き換える手順は次のとおりです。
- 既存Rendererの
OnElementChangedロジックをCreatePlatformViewに移す
OnElementPropertyChangedのswitch文をPropertyMapperの各エントリに分解
DisposeのクリーンアップをDisconnectHandlerに移す
- プロジェクトから
Xamarin.Forms参照を削除
Handlerのライフサイクル管理とテスト
Handlerには3つのライフサイクルイベントがあります。ConnectHandler(ネイティブビュー生成後)、DisconnectHandler(ビュー破棄前)、そして各Mapperデリゲート(プロパティ変更時)です。イベントハンドラの購読や外部リソースの確保は必ずConnectHandlerで行い、対応する解除をDisconnectHandlerで書くのが鉄則です。
protected override void ConnectHandler(AVPlayerViewController platformView)
{
base.ConnectHandler(platformView);
_endObserver = NSNotificationCenter.DefaultCenter.AddObserver(
AVPlayerItem.DidPlayToEndTimeNotification,
OnPlaybackEnded);
}
protected override void DisconnectHandler(AVPlayerViewController platformView)
{
if (_endObserver != null)
{
NSNotificationCenter.DefaultCenter.RemoveObserver(_endObserver);
_endObserver = null;
}
base.DisconnectHandler(platformView);
}
Handlerの単体テストはMicrosoft.Maui.TestUtilsにあるTestServiceProviderを利用します。UIスレッドを立ち上げずにMapper動作を検証できるため、CIパイプラインで高速に回せます。.NET MAUI テスト戦略完全ガイドで紹介したxUnitと組み合わせて、Mapperごとにアサーションを書いていくのが実践的なアプローチです。
本番でハマる落とし穴と対処法
私が3つのアプリで実際に遭遇した5つの典型的な落とし穴を共有します。全部、リリース前に潰しておきたいやつです。
1. Handler登録忘れによるInvalidOperationException
カスタムHandlerをConfigureMauiHandlersで登録しないと、実行時にHandler not found for view VideoViewという例外が出ます。DIコンテナの登録漏れと同じで、コンパイル時には検出できません。CI上でスモークテストを回すのが確実です。
2. PlatformViewキャストの型不一致
iOSではUIView系を、AndroidではAndroid.Views.View系を扱いますが、部分クラスで型パラメータを揃えないと、ビルドは通っても実行時にInvalidCastExceptionになります。ジェネリックの第2型引数を必ずプラットフォーム型に合わせてください。
3. DisconnectHandlerが呼ばれないケース
親コンテナが強参照を持っているとDisconnectHandlerが呼ばれず、リスナーが残り続けます。Page.Disappearingで明示的にhandler.DisconnectHandler()を呼ぶワークアラウンドが必要な場面があります。
4. Hot Reload下でのMapperキー衝突
.NET Hot ReloadはPropertyMapperを再登録するため、AppendToMappingを複数回呼ぶ形になり、同じロジックが2回実行されます。開発時は#if !DEBUGで囲むか、ModifyMappingを使いましょう。
5. HandlerChangedイベントの発火順序
MAUIのHandlerChangedは、旧HandlerのHandler = null設定と新Handlerの生成の2回発火します。この特性を知らずにイベント購読をここで行うと二重登録になります。前述のとおりConnectHandler/DisconnectHandlerで管理するのが安全です。
よくある質問
.NET MAUIのHandlerとRendererの違いは何ですか?
Rendererは継承ベースで各プラットフォームのViewRenderer<T, U>を派生させる方式でしたが、HandlerはMapperという静的ディクショナリを介して合成的にプロパティを結びつけます。Handlerの方が描画パスが浅く、初期表示が約30〜40%高速化しています。
既存のカスタムRendererを段階的に移行できますか?
可能です。ConfigureMauiHandlersとAddCompatibilityRendererを使えばXamarin.FormsのカスタムRendererをそのまま登録できます。ただし長期保守には向かないため、フェーズ2で本来のHandler方式に書き換えることを推奨します。
PropertyMapperのAppendとModifyの使い分けは?
AppendToMappingは既存の動作の後に処理を追加します。ModifyMappingは既存デリゲートを引数として受け取り、呼ぶかどうか選べます。ReplaceMappingは完全に置換します。ほとんどのユースケースはAppendで十分です。
Handlerでプラットフォーム固有APIを呼ぶ最も安全な方法は?
部分クラス(Foo.iOS.cs、Foo.Android.cs)とファイル名によるマルチターゲット規約を使うのが最も安全です。#ifディレクティブは短い分岐に、部分クラスは本格的なカスタムHandlerに使い分けてください。
Handlerの単体テストはUIスレッドが必要ですか?
不要です。Microsoft.Maui.TestUtilsのTestServiceProviderを使えばUIスレッドを立ち上げずにMapperのロジックを検証できます。CIでの高速実行に適しており、xUnitやNUnitと組み合わせるのが実務的です。