.NET MAUI 10 Handler カスタマイズ完全ガイド:カスタムコントロール・プラットフォーム固有UI実装手順(2026年版)

.NET MAUI 10のHandlerアーキテクチャを、PropertyMapper活用・カスタムコントロール実装・Xamarin Rendererからの移行までまとめて解説。3本の本番アプリで得た落とし穴と対処法も収録。

.NET MAUI 10 Handler カスタム実装ガイド 2026

更新日: 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(ButtonEntryなど)を、各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はEntryButtonなどの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ステップです。

  1. 抽象インターフェースまたはView派生クラスを定義(VideoViewなど)
  2. IViewHandlerを実装するHandlerクラスを部分クラスで作成
  3. 各プラットフォーム用の部分クラス(VideoViewHandler.iOS.csなど)でCreatePlatformViewを実装
  4. 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パターンと組み合わせると、テスタビリティが大きく向上します。

プラットフォーム固有UIを実装するには

MAUIでプラットフォーム固有のコードを書く方法は3つあります。用途に応じて使い分けましょう。

方式ファイル構成推奨用途難易度
プラットフォームコード分離Platforms/iOS/*.csOS APIの直接呼び出し
#ifディレクティブ共通ファイル内短い分岐、単一メソッド内
部分クラス+ファイル名規約Foo.iOS.cs、Foo.Android.cs本格的なカスタムHandler

部分クラス方式は.csprojMultiTargeting設定で自動判別されるため、大規模プロジェクトで最も保守性が高い方式です。以下は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方式に書き換える手順は次のとおりです。

  1. 既存RendererのOnElementChangedロジックをCreatePlatformViewに移す
  2. OnElementPropertyChangedのswitch文をPropertyMapperの各エントリに分解
  3. DisposeのクリーンアップをDisconnectHandlerに移す
  4. プロジェクトから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を段階的に移行できますか?

可能です。ConfigureMauiHandlersAddCompatibilityRendererを使えばXamarin.FormsのカスタムRendererをそのまま登録できます。ただし長期保守には向かないため、フェーズ2で本来のHandler方式に書き換えることを推奨します。

PropertyMapperのAppendとModifyの使い分けは?

AppendToMappingは既存の動作のに処理を追加します。ModifyMappingは既存デリゲートを引数として受け取り、呼ぶかどうか選べます。ReplaceMappingは完全に置換します。ほとんどのユースケースはAppendで十分です。

Handlerでプラットフォーム固有APIを呼ぶ最も安全な方法は?

部分クラス(Foo.iOS.csFoo.Android.cs)とファイル名によるマルチターゲット規約を使うのが最も安全です。#ifディレクティブは短い分岐に、部分クラスは本格的なカスタムHandlerに使い分けてください。

Handlerの単体テストはUIスレッドが必要ですか?

不要です。Microsoft.Maui.TestUtilsTestServiceProviderを使えばUIスレッドを立ち上げずにMapperのロジックを検証できます。CIでの高速実行に適しており、xUnitやNUnitと組み合わせるのが実務的です。

Marcus Chen
著者について Marcus Chen

Senior mobile architect with a decade of cross-platform experience. Spent the last five years going deep on .NET MAUI in production.