Xamarin.Formsから.NET MAUIへの移行ガイド:レンダラー・DI・プロジェクト構造の実践手順

Xamarin.Formsから.NET MAUIへの移行方法を包括的に解説。プロジェクト構造の変更、カスタムレンダラーからハンドラーへの変換、DependencyServiceからDIへの移行、Upgrade Assistantの活用法まで、実践的なコード例とともに段階的な移行手順を紹介します。

はじめに:Xamarinサポート終了から1年、まだ移行してないなら今すぐ動こう

Xamarin.Formsのサポートが2024年5月1日に正式終了してから、もう1年以上が経ちました。正直なところ、筆者の周りにもまだ移行を完了していないチームは結構います。「動いているものは触りたくない」——その気持ち、痛いほど分かります。でも、放置するリスクは日に日に大きくなっているんですよね。

具体的に何がマズいのか。Google PlayではAndroid 14(APIレベル34)以上をターゲットにしないとアプリのアップデートを公開できなくなっていますが、Xamarin.FormsがサポートしているのはAndroid 13まで。iOSも同じ状況で、iOS 17以降のターゲティングが求められる一方、Xamarinの対象はiOS 16止まりです。つまり、今のままではストアにアップデートすら出せないという状態に追い込まれつつあります。

セキュリティパッチも当然もう出ません。新たな脆弱性が見つかってもMicrosoft側からの修正は期待できない。ビジネスアプリケーションにとって、これは正直かなり致命的です。

ただ、悪い話ばかりでもないんです。.NET MAUIへの移行は、単なる「やらなきゃいけない作業」ではなく、アプリのアーキテクチャを見直してパフォーマンスを大幅に改善するチャンスでもあります。実際、移行後のアプリで起動時間が最大40%短縮、アプリサイズが25%削減されたという報告もあるくらいです。

この記事では、Xamarin.Formsから.NET MAUIへの移行を包括的に解説していきます。プロジェクト構造の変更、名前空間のマッピング、カスタムレンダラーからハンドラーへの変換、NuGetパッケージの互換性確認、そして.NET Upgrade Assistantの活用法まで——実践的なコード例を交えながら、一つずつ見ていきましょう。

移行前の準備:まずは現状を把握しよう

いきなりコードを書き換え始めるのは絶対にやめましょう。過去にそれで痛い目を見たことがあります(苦笑)。まずは現在のプロジェクトの状態を正確に把握することからスタートです。

依存関係の棚卸し

最初にやるべきは、プロジェクトが依存しているNuGetパッケージの.NET MAUI互換性チェックです。以下のコマンドで全パッケージ参照を洗い出せます。

dotnet list package --outdated

主要なXamarin向けライブラリの.NET MAUI対応状況をまとめておきます。

  • Xamarin.Essentials → .NET MAUIに統合済み(Microsoft.Maui.Essentials)
  • Xamarin.CommunityToolkit → CommunityToolkit.Maui に移行済み
  • Prism.Forms → Prism.Maui として利用可能
  • ReactiveUI.XamForms → ReactiveUI.Maui として利用可能
  • FFImageLoading → 公式メンテナンスは終了。代替としてMicrosoft.Maui.Controls.ImageSourceやComet.ImageSourceLoaderあたりを検討
  • Rg.Plugins.Popup → CommunityToolkit.Maui のPopup機能で代替可能

MAUI版が存在しないライブラリがあったら、その部分のコードは書き直しになります。移行計画の段階でこれを把握しておくことが本当に重要で、ここを怠ると後で大きな手戻りが発生します。

カスタムレンダラーの棚卸し

Xamarin.Formsのカスタムレンダラーは、.NET MAUIではハンドラーに置き換える必要があります。まずはプロジェクト内にいくつあるのか、どれくらい複雑なのかを把握しましょう。

# プロジェクト内のカスタムレンダラーを検索
grep -r "ExportRenderer" --include="*.cs" -l ./YourProject/

ちなみに、.NET MAUIにはXamarin.Formsのカスタムレンダラーとの互換性シムが用意されているので、段階的な移行も可能です。とはいえ、長期的にはハンドラーへの完全移行を目指すべきですね。

プラットフォーム固有コードの確認

DependencyServiceを使っている箇所もチェックしておきましょう。.NET MAUIでは、組み込みのDI(依存性注入)コンテナがその役割を引き継ぎます。

# DependencyServiceの使用箇所を検索
grep -r "DependencyService" --include="*.cs" -l ./YourProject/

プロジェクト構造の変更:マルチプロジェクトからシングルプロジェクトへ

Xamarin.Formsとの最も大きな構造的な違い——それはプロジェクト構成です。Xamarin.Formsでは共有プロジェクトと各プラットフォーム用プロジェクト(iOS、Android、UWP)が別々に存在していましたよね。

# Xamarin.Forms のプロジェクト構造
MySolution/
├── MyApp/                          # 共有プロジェクト(.NET Standard)
│   ├── MyApp.csproj
│   ├── App.xaml
│   ├── App.xaml.cs
│   ├── Views/
│   ├── ViewModels/
│   └── Services/
├── MyApp.Android/                  # Androidプロジェクト
│   ├── MyApp.Android.csproj
│   ├── MainActivity.cs
│   └── Resources/
├── MyApp.iOS/                      # iOSプロジェクト
│   ├── MyApp.iOS.csproj
│   ├── AppDelegate.cs
│   └── Resources/
└── MyApp.UWP/                      # UWPプロジェクト
    ├── MyApp.UWP.csproj
    └── MainPage.xaml

.NET MAUIでは、これがシングルプロジェクトに統合されます。最初は少し違和感があるかもしれませんが、慣れるとこちらの方がずっと管理しやすいです。

# .NET MAUI のプロジェクト構造
MySolution/
└── MyApp/
    ├── MyApp.csproj                # マルチターゲットの単一プロジェクト
    ├── MauiProgram.cs              # アプリの起動設定
    ├── App.xaml
    ├── App.xaml.cs
    ├── MainPage.xaml
    ├── Platforms/
    │   ├── Android/
    │   │   ├── AndroidManifest.xml
    │   │   └── MainActivity.cs
    │   ├── iOS/
    │   │   ├── AppDelegate.cs
    │   │   └── Info.plist
    │   ├── MacCatalyst/
    │   └── Windows/
    ├── Resources/
    │   ├── Images/
    │   ├── Fonts/
    │   └── Raw/
    ├── Views/
    ├── ViewModels/
    └── Services/

csprojファイルの書き換え

プロジェクトファイルはSDK形式のcsprojに変換する必要があります。Xamarin.Formsの古い形式と比較してみましょう。

<!-- .NET MAUI の csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
      $(TargetFrameworks);net9.0-windows10.0.19041.0
    </TargetFrameworks>
    <OutputType>Exe</OutputType>
    <RootNamespace>MyApp</RootNamespace>
    <UseMaui>true</UseMaui>
    <SingleProject>true</SingleProject>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>

    <!-- アプリ情報 -->
    <ApplicationTitle>MyApp</ApplicationTitle>
    <ApplicationIdGuid>YOUR-GUID-HERE</ApplicationIdGuid>
    <ApplicationId>com.yourcompany.myapp</ApplicationId>
    <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
    <ApplicationVersion>1</ApplicationVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Maui.Controls" Version="9.0.*" />
    <PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="9.0.*" />
    <PackageReference Include="CommunityToolkit.Maui" Version="9.*" />
    <PackageReference Include="CommunityToolkit.Mvvm" Version="8.*" />
  </ItemGroup>
</Project>

ここで注目してほしいのがMicrosoft.Maui.Controls.Compatibilityパッケージ。これがあることで、Xamarin.Formsの古いレンダラーを一時的にそのまま使えます。段階的移行の生命線とも言えるパッケージなので、最初は必ず含めておいてください。

名前空間のマッピング:何がどこに移ったのか

移行作業で地味に一番手間がかかるのが、名前空間の変更への対応だったりします。.NET MAUIでは多くの名前空間が再構成されているので、一つずつ確認していきましょう。

XAML名前空間

<!-- Xamarin.Forms -->
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

<!-- .NET MAUI -->
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"

C#名前空間の主な変更

// Xamarin.Forms → Microsoft.Maui 名前空間マッピング
// ============================================

// コア
Xamarin.Forms              → Microsoft.Maui.Controls
Xamarin.Forms.Xaml          → Microsoft.Maui.Controls.Xaml

// プラットフォーム
Xamarin.Forms.Platform.iOS  → Microsoft.Maui.Controls.Compatibility.Platform.iOS
Xamarin.Forms.Platform.Android → Microsoft.Maui.Controls.Compatibility.Platform.Android

// グラフィックス
Xamarin.Forms.Shapes        → Microsoft.Maui.Controls.Shapes

// Essentials
Xamarin.Essentials           → Microsoft.Maui.Devices
                             → Microsoft.Maui.Devices.Sensors
                             → Microsoft.Maui.ApplicationModel
                             → Microsoft.Maui.Media
                             → Microsoft.Maui.Networking
                             → Microsoft.Maui.Storage

Xamarin.Essentialsは.NET MAUIに統合されたんですが、機能ごとに名前空間が細かく分かれているのがちょっと厄介です。例えば、Xamarin.Essentials.ConnectivityMicrosoft.Maui.Networking.Connectivityに移動しています。最初は「どこに行ったんだ…」と探し回ることになるかもしれません。

一括置換のためのスクリプト

名前空間の変更は数が多いので、手作業だと絶対に漏れます。以下のようなスクリプトで一括置換するのがおすすめです。

#!/bin/bash
# namespace-migration.sh
# Xamarin.Forms から .NET MAUI への名前空間一括置換スクリプト

find . -name "*.cs" -type f | while read file; do
    # C# 名前空間の置換
    sed -i '' 's/using Xamarin\.Forms;/using Microsoft.Maui.Controls;/g' "$file"
    sed -i '' 's/using Xamarin\.Forms\.Xaml;/using Microsoft.Maui.Controls.Xaml;/g' "$file"
    sed -i '' 's/using Xamarin\.Essentials;/using Microsoft.Maui.ApplicationModel;\nusing Microsoft.Maui.Devices;\nusing Microsoft.Maui.Networking;\nusing Microsoft.Maui.Storage;/g' "$file"
    echo "処理完了: $file"
done

find . -name "*.xaml" -type f | while read file; do
    # XAML 名前空間の置換
    sed -i '' 's|http://xamarin.com/schemas/2014/forms|http://schemas.microsoft.com/dotnet/2021/maui|g' "$file"
    echo "処理完了: $file"
done

ただし、スクリプトで一括置換した後は、必ずコンパイルして問題がないか確認してください。特にXamarin.Essentialsは複数の名前空間に分割されているため、実際に使われているクラスに応じて不要なusingを整理する必要があります。

.NET Upgrade Assistantを活用しよう

.NET Upgrade Assistantは、Microsoftが提供してくれている移行支援ツールです。Visual Studioの拡張機能としても、CLIツールとしても使えます。手動移行の手間をかなり減らしてくれるので、使わない手はありません。

インストールと実行

# .NET Upgrade Assistant のインストール
dotnet tool install -g upgrade-assistant

# プロジェクトの分析
upgrade-assistant analyze ./MyApp.sln

# 移行の実行
upgrade-assistant upgrade ./MyApp.sln

Upgrade Assistantが自動でやってくれることは結構多いです。

  • csprojファイルのSDK形式への変換
  • ターゲットフレームワークの更新(net9.0-android、net9.0-ios等)
  • NuGetパッケージの互換性チェックと更新
  • 一部の名前空間変更の自動適用
  • プロジェクト構造の再編成

ただし、万能ではないです。特にカスタムレンダラーの変換やプラットフォーム固有コードの移行は、手動での対応が必要になる場面がかなりあります。ツールの出力を確認しながら、手動で仕上げていくのが現実的なアプローチでしょう。

カスタムレンダラーからハンドラーへ:移行の本丸

さて、ここからが移行作業の本丸です。Xamarin.Formsで最もカスタマイズが多かったであろうカスタムレンダラー。.NET MAUIでは、これがハンドラーアーキテクチャに置き換わりました。ハンドラーはレンダラーよりもシンプルで、パフォーマンスにも優れた設計になっています。

レンダラーとハンドラーの根本的な違い

両者の設計哲学の違いを先に理解しておきましょう。

  • レンダラー:クロスプラットフォームコントロール全体をラップし、ネイティブコントロールのライフサイクル全体を管理する。いわゆる密結合な設計。
  • ハンドラー:クロスプラットフォームコントロールとネイティブコントロールの間のマッピングだけを担当。疎結合な設計で、PropertyMapperを通じてプロパティの同期を行う。

この違いは実際にコードを見ると一目瞭然です。

具体例:ボーダー付きEntryのカスタマイズ

Xamarin.FormsでボーダーのスタイルをカスタマイズしたEntryの例を、レンダラーからハンドラーに書き換えてみます。

Xamarin.Forms(カスタムレンダラー)

// カスタムコントロール
public class BorderlessEntry : Entry
{
    public static readonly BindableProperty BorderColorProperty =
        BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(BorderlessEntry), Color.Default);

    public Color BorderColor
    {
        get => (Color)GetValue(BorderColorProperty);
        set => SetValue(BorderColorProperty, value);
    }
}

// Android カスタムレンダラー
[assembly: ExportRenderer(typeof(BorderlessEntry), typeof(BorderlessEntryRenderer))]
namespace MyApp.Droid.Renderers
{
    public class BorderlessEntryRenderer : EntryRenderer
    {
        public BorderlessEntryRenderer(Context context) : base(context) { }

        protected override void OnElementChanged(ElementChangedEventArgs<Entry> e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.Background = null;
                var element = (BorderlessEntry)Element;
                if (element.BorderColor != Color.Default)
                {
                    var shape = new GradientDrawable();
                    shape.SetCornerRadius(8);
                    shape.SetStroke(2, element.BorderColor.ToAndroid());
                    Control.Background = shape;
                }
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == BorderlessEntry.BorderColorProperty.PropertyName)
            {
                UpdateBorderColor();
            }
        }

        private void UpdateBorderColor()
        {
            // ボーダー色の更新ロジック
        }
    }
}

.NET MAUI(ハンドラー)

// カスタムコントロール(インターフェースベース)
public interface IBorderlessEntry : IEntry
{
    Color BorderColor { get; }
}

public class BorderlessEntry : Entry, IBorderlessEntry
{
    public static readonly BindableProperty BorderColorProperty =
        BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(BorderlessEntry));

    public Color BorderColor
    {
        get => (Color)GetValue(BorderColorProperty);
        set => SetValue(BorderColorProperty, value);
    }
}

// ハンドラー(共通部分)
public partial class BorderlessEntryHandler : EntryHandler
{
    public static new IPropertyMapper<IBorderlessEntry, BorderlessEntryHandler> Mapper =
        new PropertyMapper<IBorderlessEntry, BorderlessEntryHandler>(EntryHandler.Mapper)
        {
            [nameof(IBorderlessEntry.BorderColor)] = MapBorderColor
        };

    public BorderlessEntryHandler() : base(Mapper) { }
}

// Android 用ハンドラー(Platforms/Android/ に配置)
public partial class BorderlessEntryHandler
{
    private static void MapBorderColor(BorderlessEntryHandler handler, IBorderlessEntry entry)
    {
        if (handler.PlatformView == null) return;

        var shape = new GradientDrawable();
        shape.SetCornerRadius(8);

        if (entry.BorderColor != null)
        {
            shape.SetStroke(2, entry.BorderColor.ToPlatform());
        }

        handler.PlatformView.Background = shape;
    }
}

// MauiProgram.cs での登録
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureMauiHandlers(handlers =>
            {
                handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
            });

        return builder.Build();
    }
}

ハンドラーではPropertyMapperがプロパティ変更の管理を一手に引き受けてくれます。OnElementPropertyChangedのような手動のプロパティ変更追跡が不要になって、コードがだいぶスッキリしているのが分かるかと思います。

互換性シムで段階的に移行する

すべてのカスタムレンダラーを一度にハンドラーへ変換するのは、大規模プロジェクトでは現実的じゃありません。ありがたいことに、.NET MAUIには互換性シムが用意されていて、既存のレンダラーをそのまま使い続けることができます。

// 互換性シムを使用して既存のレンダラーを登録
using Microsoft.Maui.Controls.Compatibility.Hosting;

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseMauiCompatibility()          // 互換性モードを有効化
            .ConfigureMauiHandlers(handlers =>
            {
                // 新しいハンドラー
                handlers.AddHandler<BorderlessEntry, BorderlessEntryHandler>();
            })
            .ConfigureCompatibilityHandlers(handlers =>
            {
                // 旧レンダラー(まだ移行していないもの)
                // handlers.AddCompatibilityRenderer(typeof(OldCustomControl),
                //     typeof(OldCustomControlRenderer));
            });

        return builder.Build();
    }
}

この方法なら、優先度の高いレンダラーから順番にハンドラーへ変換していけます。ただし、互換性シムにはパフォーマンス上のオーバーヘッドがあるので、最終的にはすべてハンドラーに移行するのがゴールです。

DependencyServiceからDIコンテナへの移行

Xamarin.Formsでは、プラットフォーム固有の機能にアクセスするためにDependencyServiceパターンが広く使われていました。.NET MAUIでは、Microsoft.Extensions.DependencyInjectionベースの組み込みDIコンテナが使えるようになっています。

個人的に、この変更はかなり嬉しいポイントです。

移行前(Xamarin.Forms)

// インターフェース定義
public interface IDeviceInfoService
{
    string GetDeviceModel();
    string GetOsVersion();
}

// Android 実装
[assembly: Dependency(typeof(DeviceInfoService))]
namespace MyApp.Droid.Services
{
    public class DeviceInfoService : IDeviceInfoService
    {
        public string GetDeviceModel() => Build.Model;
        public string GetOsVersion() => Build.VERSION.Release;
    }
}

// 使用箇所
var deviceInfo = DependencyService.Get<IDeviceInfoService>();
var model = deviceInfo.GetDeviceModel();

移行後(.NET MAUI)

// インターフェースはそのまま使える
public interface IDeviceInfoService
{
    string GetDeviceModel();
    string GetOsVersion();
}

// Android 実装(Platforms/Android/ に配置)
namespace MyApp.Platforms.Android.Services
{
    public class DeviceInfoService : IDeviceInfoService
    {
        public string GetDeviceModel() => Build.Model ?? "Unknown";
        public string GetOsVersion() => Build.VERSION.Release ?? "Unknown";
    }
}

// MauiProgram.cs で登録
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder.UseMauiApp<App>();

    // プラットフォーム固有のサービス登録
#if ANDROID
    builder.Services.AddSingleton<IDeviceInfoService, Platforms.Android.Services.DeviceInfoService>();
#elif IOS
    builder.Services.AddSingleton<IDeviceInfoService, Platforms.iOS.Services.DeviceInfoService>();
#endif

    return builder.Build();
}

// ViewModel でコンストラクタインジェクションを使用
public class MainViewModel
{
    private readonly IDeviceInfoService _deviceInfo;

    public MainViewModel(IDeviceInfoService deviceInfo)
    {
        _deviceInfo = deviceInfo;
    }

    public string DeviceModel => _deviceInfo.GetDeviceModel();
}

DIコンテナ方式のメリットは、テスタビリティが段違いに良くなること。モックの注入が簡単になるので、ユニットテストが格段に書きやすくなります。DependencyServiceの頃は正直テストが面倒でしたからね…。

App.xaml.csとアプリケーションライフサイクルの変更

アプリのエントリーポイントもかなり変わっています。ここは一度理解してしまえば難しくないので、サクッと見ていきましょう。

移行前(Xamarin.Forms)

// App.xaml.cs
public partial class App : Xamarin.Forms.Application
{
    public App()
    {
        InitializeComponent();
        DependencyService.Register<MockDataStore>();
        MainPage = new NavigationPage(new MainPage());
    }

    protected override void OnStart() { }
    protected override void OnSleep() { }
    protected override void OnResume() { }
}

// Android - MainActivity.cs
[Activity(Label = "MyApp", Theme = "@style/MainTheme", MainLauncher = true)]
public class MainActivity : FormsAppCompatActivity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);
        Forms.Init(this, savedInstanceState);
        LoadApplication(new App());
    }
}

移行後(.NET MAUI)

// MauiProgram.cs(新しいエントリーポイント)
public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });

        // サービスの登録
        builder.Services.AddSingleton<IDataStore, MockDataStore>();
        builder.Services.AddTransient<MainViewModel>();

        return builder.Build();
    }
}

// App.xaml.cs
public partial class App : Application
{
    public App()
    {
        InitializeComponent();
    }

    // .NET MAUI 9以降では CreateWindow をオーバーライド
    protected override Window CreateWindow(IActivationState? activationState)
    {
        return new Window(new NavigationPage(new MainPage()));
    }
}

// Android - MainActivity.cs
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
    // 追加の初期化コードは通常不要
}

MauiProgram.csがアプリ全体の構成を担う新しいエントリーポイントになったのが最大の変更点です。ASP.NET Coreのビルダーパターンに馴染みがある方なら、「あ、これ同じパターンだ」と直感的に理解できるはずです。

レイアウトの変更と注意点

.NET MAUIではレイアウトエンジンが再設計されています。大部分は互換性が保たれていますが、微妙に挙動が変わっている部分もあるので油断は禁物です。

押さえておくべき変更点

  • StackLayout:そのまま使えますが、新しくVerticalStackLayoutHorizontalStackLayoutが追加されました。こちらの方が軽量でパフォーマンスが良いので、新規コードでは積極的に使いましょう。
  • Grid:基本的な使い方は同じですが、内部のレイアウト計算ロジックが変更されています。marginやpaddingの計算結果が微妙に異なる場合があるので、目視確認は必須。
  • RelativeLayout:非推奨です。GridやAbsoluteLayoutで代替してください。
  • Frame:こちらも非推奨。代わりにBorderコントロールを使いましょう。
<!-- Xamarin.Forms -->
<Frame CornerRadius="10" HasShadow="True" Padding="10">
    <StackLayout>
        <Label Text="タイトル" FontSize="Large" />
        <Label Text="コンテンツ" />
    </StackLayout>
</Frame>

<!-- .NET MAUI(推奨) -->
<Border StrokeShape="RoundRectangle 10" Padding="10">
    <Border.Shadow>
        <Shadow Brush="Black" Offset="2,2" Radius="4" Opacity="0.3" />
    </Border.Shadow>
    <VerticalStackLayout>
        <Label Text="タイトル" FontSize="Large" />
        <Label Text="コンテンツ" />
    </VerticalStackLayout>
</Border>

リソースと画像の管理

.NET MAUIでは、リソース管理の仕組みも大幅に改善されました。これは素直に嬉しい変更です。

画像リソース

Xamarin.Formsでは各プラットフォームのResourcesフォルダに個別に画像を配置する必要がありましたよね。あれ、地味に面倒でした。.NET MAUIでは、Resources/Imagesフォルダに画像を置くだけで、ビルド時に各プラットフォーム向けに自動で変換・最適化してくれます。

<!-- csproj での画像リソース設定 -->
<ItemGroup>
    <!-- Resources/Images 以下のすべての画像を自動的に含む -->
    <MauiImage Include="Resources\Images\*" />

    <!-- 特定の画像にベースサイズを指定 -->
    <MauiImage Include="Resources\Images\app_icon.svg" BaseSize="168,168" />

    <!-- スプラッシュスクリーン -->
    <MauiSplashScreen Include="Resources\Splash\splash.svg"
                      Color="#512BD4"
                      BaseSize="128,128" />
</ItemGroup>

SVG画像をソースとして使えるのも地味にありがたいポイント。ビルド時にPNGに変換されて、各プラットフォームに適切な解像度で生成されます。

フォントリソース

// MauiProgram.cs でフォントを登録
builder.ConfigureFonts(fonts =>
{
    fonts.AddFont("NotoSansJP-Regular.ttf", "NotoSansJP");
    fonts.AddFont("NotoSansJP-Bold.ttf", "NotoSansJPBold");
    fonts.AddFont("MaterialIcons-Regular.ttf", "MaterialIcons");
});

.NET 10への展望と将来に備える

.NET MAUI 9での移行を完了したら、次は.NET 10へのアップグレードが視野に入ります。2025年11月にリリースされた.NET 10はLTS(Long Term Support)リリースなので、安定性を重視するプロジェクトにとっては重要なマイルストーンです。

.NET 10で押さえておきたいポイント

  • グローバルXAML名前空間:CLRとXAML名前空間をグローバルMAUI名前空間にマッピングでき、XAMLのルートノードがシンプルになります。
  • CollectionView / CarouselView:.NET 9ではオプションだった新しいハンドラーが、.NET 10ではデフォルトに。
  • 非推奨APIFadeToRotateToScaleToTranslateToなどのアニメーションメソッドが非推奨となり、async版に置き換え。
  • Android APIレベル:APIレベル35および36のサポートが追加されました。
<!-- .NET 10 への TargetFramework 更新 -->
<PropertyGroup>
    <TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
    <TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">
        $(TargetFrameworks);net10.0-windows10.0.19041.0
    </TargetFrameworks>
</PropertyGroup>

なお、.NET 10のターゲティングにはVisual Studio 2026が必要です。iOS/Mac開発にはXcode 26も必要になるので、開発環境のアップデートも計画に入れておきましょう。

移行のベストプラクティスとよくあるハマりポイント

段階的移行のススメ

大規模プロジェクトでは、ビッグバン方式(全部いっぺんに移行する)は避けた方が無難です。筆者のおすすめは段階的アプローチです。

  1. フェーズ1:プロジェクト構造とcsprojの変換。互換性パッケージを使って既存コードをそのまま動かす。
  2. フェーズ2:名前空間の変更と、DependencyServiceからDIコンテナへの切り替え。
  3. フェーズ3:カスタムレンダラーからハンドラーへの変換。優先度の高いものから順に。
  4. フェーズ4:非推奨コントロール(Frame、RelativeLayout等)の置き換えとレイアウトの最適化。
  5. フェーズ5:互換性パッケージの削除と最終テスト。

各フェーズの間にしっかりテストを挟むことで、問題の切り分けがしやすくなります。

よくあるハマりポイント

実際の移行作業で遭遇しがちな落とし穴をまとめておきます。

  • InitializeComponentの呼び出し忘れ:XAMLページのコードビハインドでInitializeComponent()の呼び出しが漏れていると、ランタイムエラーに。意外とやりがちです。
  • Implicit Usingsによる名前の衝突:.NET MAUIではImplicit Usingsが有効になっているため、既存のusingステートメントと衝突することがあります。
  • ハンドラーの登録忘れ:カスタムハンドラーを作成しても、MauiProgram.csで登録しないとデフォルトのハンドラーが使われてしまいます。「あれ、カスタマイズが反映されない…」となったらまずここを疑ってみてください。
  • プラットフォーム固有コードの配置ミスPlatforms/Android/Platforms/iOS/フォルダに正しく配置しないと、他プラットフォームのコードとしてコンパイルされてエラーになります。
  • リソースパスの変更:画像やフォントのパスが変わっているため、ランタイムで「リソースが見つからない」エラーが発生しがち。

移行後のチェックリスト

移行が完了したら、以下の項目を一つずつ確認していきましょう。ここを怠ると、リリース後に思わぬバグが見つかります。

  1. 全プラットフォームでのビルド確認:Android、iOS、Windows、macOSのすべてのターゲットでビルドが通ることを確認。
  2. UIの目視確認:レイアウトエンジンの違いで、見た目が微妙に変わっている箇所がないかチェック。特にマージンとパディングまわりは要注意です。
  3. 機能テスト:すべての画面遷移、データバインディング、プラットフォーム固有機能の動作確認。
  4. パフォーマンステスト:起動時間、メモリ使用量、スクロールの滑らかさを移行前と比較。
  5. アクセシビリティテスト:SemanticPropertiesが正しく設定されているか(AutomationPropertiesからの移行漏れがないか)。
  6. NuGetパッケージの整理:不要なXamarin関連パッケージが残っていないか確認。

まとめ:移行は「投資」——先延ばしするほどコストは膨らむ

Xamarin.Formsから.NET MAUIへの移行は、正直言って小さな作業ではありません。プロジェクトの規模によっては数週間から数ヶ月かかることもあるでしょう。

でも、移行を先延ばしにすればするほど、状況は悪くなる一方です。セキュリティリスクは高まり、ストアへの公開制約は厳しくなり、利用できるライブラリのエコシステムからもどんどん取り残されていきます。

この記事で紹介したように、移行には明確な手順とツールが揃っています。.NET Upgrade Assistantによる自動化、互換性シムによる段階的移行、ハンドラーアーキテクチャへの計画的な置き換え——これらを組み合わせることで、リスクを最小限に抑えながら着実に移行を進められます。

移行が完了すれば、最大40%の起動時間短縮、25%のアプリサイズ削減、そして.NET 10以降の最新機能やセキュリティアップデートへの継続的なアクセスが手に入ります。これを「コスト」ではなく「投資」と捉えて、ぜひ今すぐ移行計画を始めてみてください。

著者について Editorial Team

Our team of expert writers and editors.