Xamarin.Forms から .NET MAUI へ――7プロジェクト構成を「単一プロジェクト構成」に集約する実践ガイド
本記事は、Xamarin Forms 時代に 共通ロジック/共通 UI/各 Android・iOS ヘッドを含む 7 プロジェクト構成で運用していたソリューションを、.NET MAUI の 単一プロジェクト構成へ最短距離で移行するための、手順と設計指針をまとめたものです。移行対象の代表例として、既存の Project A(ビジネスロジック)・Project C(共通 UI) と、新規に作成する Project B(.NET MAUI App) に統合するシナリオを扱います。
移行の目的と全体像
.NET MAUI では、単一プロジェクト内に各プラットフォーム固有コードを集約する「Single Project」思想が中核です。Android/iOS 用の別プロジェクトを廃し、Platforms/Android と Platforms/iOS フォルダに統合することで、参照関係の簡素化・ビルド高速化・資産の見通し改善を一気に実現できます。既存の Xamarin Forms ソリューションを以下の 3 プロジェクトへ再編するのが基本戦略です。
<table>
<thead>
<tr>
<th>移行後の役割</th>
<th>推奨テンプレート</th>
<th>主な内容・注意点</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Project B(メインアプリ)</strong></td>
<td><strong>.NET MAUI App</strong></td>
<td>エントリーポイント。旧 <em>ProjectB.Android / ProjectB.iOS</em> のコード・リソースは <code>Platforms/Android</code>・<code>Platforms/iOS</code> へ移植(ヘッドプロジェクトは廃止)。<br><code><TargetFrameworks></code> に <code>net8.0-android;net8.0-ios</code>(必要に応じて <code>net8.0-maccatalyst</code> や <code>net8.0-windows10.0.19041.0</code>)を指定。</td>
</tr>
<tr>
<td><strong>Project A(ビジネスロジック)</strong></td>
<td><strong>.NET Class Library</strong></td>
<td>モデル/サービス/ユーティリティなど <strong>UI 非依存</strong>コードを保持。テストや CI/CD での再利用性が高い。プラットフォーム API が必要な箇所は DI で抽象化。</td>
</tr>
<tr>
<td><strong>Project C(共通 UI/カスタムコントロール)</strong></td>
<td><strong>.NET MAUI Class Library</strong></td>
<td>XAML・ハンドラー・付随するプラットフォーム固有実装を 1 プロジェクトに集約。旧 <em>ProjectC.Android / ProjectC.iOS</em> の実装は <code>Platforms/Android</code>/<code>Platforms/iOS</code> フォルダへ移行。</td>
</tr>
</tbody>
</table>
<p>最終的な推奨ソリューション構成は以下の通りです。</p>
<pre><code>ProjectB.sln
├── ProjectA (.NET Class Library) ├── ProjectB (.NET MAUI App) │ └── Platforms/ │ ├── Android/ ← 旧 ProjectB.Android の資産 │ └── iOS/ ← 旧 ProjectB.iOS の資産 └── ProjectC (.NET MAUI Class Library) └── Platforms/ ├── Android/ ← 旧 ProjectC.Android の資産 └── iOS/ ← 旧 ProjectC.iOS の資産
MAUI への移行に伴う概念の置き換え
Xamarin Forms と .NET MAUI では、名称や推奨パターンがいくつか刷新されています。移行で迷いやすいポイントを表にまとめます。
<table>
<thead>
<tr>
<th>XF の概念/実装</th>
<th>MAUI での対応</th>
<th>移行要点</th>
</tr>
</thead>
<tbody>
<tr>
<td>DependencyService</td>
<td>Host ベース DI(<code>MauiProgram</code>の <code>builder.Services</code>)</td>
<td>IF を定義して <code>AddSingleton</code>/<code>AddTransient</code> 登録。テスト容易性が向上。</td>
</tr>
<tr>
<td>Custom Renderer</td>
<td>Handler(ハンドラー)</td>
<td>プロパティマッピング中心へ転換。<code>Mapper</code> でネイティブビューへ反映。</td>
</tr>
<tr>
<td>Xamarin.Essentials</td>
<td>MAUI Essentials(<code>Microsoft.Maui.Essentials</code>)</td>
<td>名前空間変更。概ね呼び出しは互換。</td>
</tr>
<tr>
<td>NavigationPage / Prism 等</td>
<td>Shell(推奨)</td>
<td>URI ベースのルーティング。複雑な階層ナビも宣言的に。</td>
</tr>
<tr>
<td>Android/iOS 別ヘッド</td>
<td>Single Project の <code>Platforms/*</code></td>
<td>マニフェストや Info.plist、エントリポイントを各フォルダへ。</td>
</tr>
<tr>
<td>画像・フォントの個別設定</td>
<td><code>MauiImage</code>/<code>MauiFont</code> などのビルドアクション</td>
<td>リサイズはビルドが自動生成。宣言は <code>.csproj</code> に集約。</td>
</tr>
</tbody>
</table>
移行手順(フル版)
<h3>1. ブランチ作成とバックアップ</h3>
<ul>
<li>大規模なファイル移動が発生するため、<strong>専用ブランチ</strong>で作業し、いつでもロールバックできるようバックアップを取得します。</li>
<li>CI のビルド定義がある場合は、新旧を並行運用できるようジョブ名を分けておくと安全です。</li>
</ul>
<h3>2. 新規 .NET MAUI App(Project B)を作成</h3>
<pre><code>dotnet new maui -n ProjectB
dotnet new sln -n ProjectB dotnet sln ProjectB.sln add .\ProjectB\ProjectB.csproj
Windows, Mac Catalyst を併用する場合は、後述の TargetFrameworks に追記します。
<h3>3. 既存ライブラリの再配置(Project A / Project C)</h3>
<p>ビジネスロジック(UI に依存しない部分)は <strong>.NET Class Library</strong> として維持/再作成します。共通 UI は <strong>.NET MAUI Class Library</strong> のテンプレートを採用し、<code>Platforms</code> フォルダを持つ構成に統合します。</p>
<pre><code># 既存プロジェクトをソリューションへ追加
dotnet sln add ..\ProjectA\ProjectA.csproj dotnet sln add ..\ProjectC\ProjectC.csproj # 参照関係を設定(B が A/C を参照) dotnet add .\ProjectB\ProjectB.csproj reference ..\ProjectA\ProjectA.csproj dotnet add .\ProjectB\ProjectB.csproj reference ..\ProjectC\ProjectC.csproj
<h3>4. 旧 Android/iOS ヘッドの統合</h3>
<p>各ヘッドプロジェクトで以下を確認し、Project B(または Project C)の <code>Platforms/*</code> へ移植します。</p>
<table>
<thead>
<tr>
<th>種類</th>
<th>Xamarin.Forms の場所</th>
<th>.NET MAUI の移行先</th>
<th>補足</th>
</tr>
</thead>
<tbody>
<tr>
<td>Android エントリ</td>
<td><code>MainActivity.cs</code>, <code>MainApplication.cs</code></td>
<td><code>Platforms/Android</code></td>
<td>クラス継承は <code>MauiAppCompatActivity</code> / <code>MauiApplication</code> を採用。</td>
</tr>
<tr>
<td>Android 署名・ID</td>
<td><code>AndroidManifest.xml</code></td>
<td><code>Platforms/Android/AndroidManifest.xml</code>(または csproj のプロパティ)</td>
<td><code><ApplicationId></code> を <code>ProjectB.csproj</code> に記載。</td>
</tr>
<tr>
<td>iOS 設定</td>
<td><code>Info.plist</code>, Entitlements</td>
<td><code>Platforms/iOS/Info.plist</code>, <code>Entitlements.plist</code></td>
<td>スキームや権限は Plist に統合。</td>
</tr>
<tr>
<td>リソース(画像・音)</td>
<td><code>Resources/drawable/*</code>、<code>Assets/*</code>、<code>Resources/*</code></td>
<td><code>Resources/Images</code>, <code>Resources/Raw</code> 等</td>
<td>ビルドアクションを <code>MauiImage</code>/<code>MauiAsset</code> に変更。</td>
</tr>
</tbody>
</table>
<h3>5. Project B の <code>.csproj</code> 設定例</h3>
<p>以下は、Android・iOS・Mac Catalyst・Windows をターゲットにする例です。必要なプラットフォームに絞って問題ありません。</p>
<pre><code><Project Sdk="Microsoft.NET.Sdk">
net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041.0 Exe true enable enable
<ApplicationTitle>ProjectB</ApplicationTitle>
<ApplicationId>com.example.projectb</ApplicationId>
<ApplicationIdGuid>{00000000-0000-0000-0000-000000000000}</ApplicationIdGuid>
<h3>6. Project C(MAUI Class Library)の <code>.csproj</code> 例</h3>
<pre><code><Project Sdk="Microsoft.NET.Sdk">
net8.0-android;net8.0-ios;net8.0-maccatalyst;net8.0-windows10.0.19041.0 true enable enable MSBuild:Compile
<h3>7. Project A(.NET Class Library)の <code>.csproj</code> 例</h3>
<p>UI・MAUI に依存しないことが重要です。プラットフォーム依存 API が必要な場合は、<strong>インターフェース + DI</strong> で抽象化します。</p>
<pre><code><Project Sdk="Microsoft.NET.Sdk">
net8.0 enable enable
<h3>8. <code>MauiProgram</code> の DI 登録とブートストラップ</h3>
<pre><code>public static class MauiProgram
{ public static MauiApp CreateMauiApp() { var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// ProjectA のサービス登録
builder.Services.AddSingleton<IDataStore, SqliteDataStore>();
builder.Services.AddTransient<ILoginService, LoginService>();
// View / ViewModel 登録(例)
builder.Services.AddTransient<LoginPage>();
builder.Services.AddTransient<LoginViewModel>();
return builder.Build();
}
}
<h3>9. <code>DependencyService</code> から DI への置換(サンプル)</h3>
<p>XF の <code>DependencyService.Get<IEnvService>()</code> は、MAUI ではコンストラクタインジェクションに置き換えます。</p>
<pre><code>// XF: var env = DependencyService.Get<IEnvService>();
// MAUI(ViewModel) public class AboutViewModel { private readonly IEnvService _env;
public AboutViewModel(IEnvService env)
{
_env = env;
AppVersion = _env.GetVersion();
}
public string AppVersion { get; }
}
<h3>10. カスタムレンダラー → ハンドラーへの移行(最小例)</h3>
<p>レンダラーの多くは「プロパティをネイティブへ反映する」ことが本質です。MAUI では <strong>Mapper</strong> を使って宣言的に記述します。</p>
<pre><code>// 共通コントロール
public class BorderlessEntry : Entry { } // プラットフォームハンドラー(Android) #if ANDROID using Microsoft.Maui.Handlers; using Android.Graphics.Drawables; public class BorderlessEntryHandler : EntryHandler { protected override void ConnectHandler(Android.Widget.EditText platformView) { base.ConnectHandler(platformView); platformView.Background = new ColorDrawable(Android.Graphics.Color.Transparent); } } public static class HandlerRegistration { public static void Register() { EntryHandler.Mapper.AppendToMapping(nameof(BorderlessEntry), (handler, view) => { if (view is BorderlessEntry) { handler.PlatformView.Background = new ColorDrawable(Android.Graphics.Color.Transparent); } }); } } #endif
MauiProgram.CreateMauiApp 内で HandlerRegistration.Register() を初期化時に呼び出して適用します。
<h3>11. Shell ベースのナビゲーションへ移行</h3>
<p>ナビゲーション構成を <code>AppShell.xaml</code> に宣言すると、タブ/フライアウト/階層遷移を統一管理できます。</p>
<pre><code><AppShell xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:ProjectB.Views"
x:Class="ProjectB.AppShell">
// ページ遷移
await Shell.Current.GoToAsync("settings");
<h3>12. リソース(画像・フォント・スプラッシュ)の整理</h3>
<table>
<thead>
<tr>
<th>旧(XF)</th>
<th>新(MAUI)</th>
<th>設定方法</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Resources/drawable</code> などに生 PNG 群</td>
<td><code>Resources/Images</code> に 1 枚(SVG/大 PNG)</td>
<td><code>MauiImage</code> で自動リサイズ。プラットフォーム別フォルダは不要。</td>
</tr>
<tr>
<td>フォントを各 OS に個別配置</td>
<td><code>Resources/Fonts</code> に集約</td>
<td><code>.csproj</code> の <code><MauiFont></code> に列挙し <code>Alias</code> を付与。</td>
</tr>
<tr>
<td>スプラッシュ各サイズ管理</td>
<td><code>Resources/Splash/splash.svg</code> など 1 枚</td>
<td>ビルド時に各解像度へ展開。</td>
</tr>
</tbody>
</table>
<h3>13. プラットフォーム API の抽象化(部分クラス/IF 併用)</h3>
<p>どうしてもプラットフォーム API を直接叩く必要がある箇所は、<strong>インターフェース + 部分クラス</strong>で吸収します。</p>
<pre><code>// ProjectA(共通 IF)
public interface IAppVersionProvider { string GetVersion(); } // ProjectB(各プラットフォーム実装) public class AppVersionProvider : IAppVersionProvider { public string GetVersion() { #if ANDROID return Android.App.Application.Context.PackageManager! .GetPackageInfo(Android.App.Application.Context.PackageName, 0)! .VersionName!; #elif IOS return Foundation.NSBundle.MainBundle.InfoDictionary[“CFBundleShortVersionString”].ToString(); #else return “1.0.0”; #endif } }
// 登録(MauiProgram)
builder.Services.AddSingleton();
<h3>14. ビルド & 実行</h3>
<p>ワークロードが未導入なら先にセットアップします。</p>
dotnet workload install maui
```
<p>ビルド・デプロイはターゲットフレームワークごとに実行します。</p> <pre><code># Android
dotnet build .\ProjectB\ProjectB.csproj -t:Run -f net8.0-android
# iOS(Mac)
dotnet build .\ProjectB\ProjectB.csproj -t:Run -f net8.0-ios </code></pre>
15. よくある変換ポイントとリネーム一覧
| 旧(XF) | 新(MAUI) | メモ |
|---|---|---|
using Xamarin.Forms; | using Microsoft.Maui;, Microsoft.Maui.Controls; | 名前空間を一括置換。 |
| XamlCompilation 属性 | 不要 | MAUI は XAML コンパイルがデフォルト。 |
| FFImageLoading 等 | MAUI Image / Community Toolkit | 互換パッケージに置換。機能差は要確認。 |
| ZXing(バーコード) | ZXing.Net.Maui | ハンドラー登録と権限設定を追加。 |
| MessagingCenter | CommunityToolkit.Mvvm の Messenger | 型安全・テスト容易。 |
16. トリミング/AOT と反射の注意
- Release ビルドでは トリミングが有効になります。反射で到達する型は
DynamicallyAccessedMembers属性やTrimmerRootAssembly設定で保護します。 - 起動高速化のため AOT を有効化する場合はビルド時間や APK サイズとのトレードオフを評価します。
17. 既存リソースのビルドアクション変換早見表
| XF のビルドアクション | MAUI のビルドアクション | 対象フォルダ |
|---|---|---|
| AndroidResource / BundleResource | MauiImage | Resources/Images |
| Content / None(データ・音) | MauiAsset | Resources/Raw |
| EmbeddedResource(XAML) | MauiXaml | XAML ファイル直下(自動検出) |
| Font | MauiFont | Resources/Fonts |
18. CI/CD の初期サンプル(GitHub Actions 抜粋)
name: build-maui-android
```
on:
push:
branches: [ main ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Install MAUI workload
run: dotnet workload install maui
- name: Restore
run: dotnet restore ProjectB/ProjectB.csproj
- name: Build Android
run: dotnet build ProjectB/ProjectB.csproj -c Release -f net8.0-android </code></pre>
```
<h3>19. 動作確認チェックリスト</h3>
<ul>
<li><strong>起動直後のクラッシュ</strong>:ワークロード未導入/<code>ApplicationId</code> の重複/マニフェスト権限不足を確認。</li>
<li><strong>画像が表示されない</strong>:<code>MauiImage</code> の Include パターンと配置フォルダを再確認。</li>
<li><strong>iOS の権限ダイアログが出ない</strong>:<code>Info.plist</code> の使用理由キー(例:カメラ、位置情報)を追加。</li>
<li><strong>反射を使う JSON シリアライザーが失敗</strong>:トリマー属性でプロパティ保持を指示。</li>
<li><strong>古い NuGet のまま</strong>:MAUI 対応パッケージへ置換(CommunityToolkit.Maui など)。</li>
</ul>
<h3>20. ファイル移行の実務ステップ(サンプル作業ログ)</h3>
<ol>
<li>旧 <em>ProjectB.Android / ProjectB.iOS</em> から <code>MainActivity.cs</code>・<code>MainApplication.cs</code>・<code>Info.plist</code> を取り出し、<code>Platforms</code> へ移動。不要な using を整理。</li>
<li>画像・音声・証明書類を <code>Resources/Images</code>・<code>Resources/Raw</code> に集約し、<code>.csproj</code> の <code><MauiImage></code>/<code><MauiAsset></code> を追加。</li>
<li>依存パッケージを MAUI 版へ置換し、<code>using Xamarin.Forms</code> を <code>Microsoft.Maui.Controls</code> へ置換。</li>
<li>DI コンテナへビジネスロジック(Project A)を登録。ViewModel は <em>CommunityToolkit.Mvvm</em> で簡潔化。</li>
<li>レンダラーは段階的にハンドラーへ移行。まずは既存レンダラーで動かし、後でリファクタする「二段移行」も有効。</li>
<li>Shell を導入し、ルーティング/タブ構成を <code>AppShell.xaml</code> に宣言。</li>
<li>各 OS の権限・署名・バージョン設定を最終確認して Release ビルドを通す。</li>
</ol>
<h3>21. 設計の最適化:プロジェクト境界の見直し</h3>
<p>移行を機に、責務境界を次の方針で再整理すると保守性が上がります。</p>
<ul>
<li><strong>Project A</strong>:純粋ドメイン・アプリケーションサービス。ファイル I/O やセンサーアクセス等は <em>IF で抽象化</em> し、実装は Project B 側に置く。</li>
<li><strong>Project C</strong>:共通 UI・カスタムコントロール・リソース辞書。<em>ハンドラーのプラットフォーム実装</em>は <code>Platforms/*</code> に併設。</li>
<li><strong>Project B</strong>:アプリ構成(DI、Shell、テーマ、エントリーポイント)。UI 面の依存を集約し、上位層から下位層(A/C)へのみ参照。</li>
</ul>
<h3>22. Visual Studio/XAML ホットリロードの活用</h3>
<p>MAUI は XAML ホットリロードが標準で強力です。MVVM と組み合わせることで、ViewModel の状態を保ったまま UI 修正を確認できます。移行中は「まず起動して画面を出す」ことを最優先にし、レイアウト調整をホットリロードで高速反復するのがコツです。</p>
<h3>23. まとめ:最小コストで「単一プロジェクト構成」へ</h3>
<p>本記事の推奨構成に沿えば、<strong>Project B = .NET MAUI App</strong> にヘッド資産を集約し、<strong>Project A = 純ロジック</strong>、<strong>Project C = 共通 UI</strong> として役割分担を明確化できます。特に次の 3 点が効果的です。</p>
<ol>
<li>Android/iOS それぞれのプロジェクトを廃止し、<code>Platforms/*</code> に一本化。</li>
<li>ビジネスロジックは .NET クラシックライブラリで <strong>UI 依存をゼロ</strong>に。</li>
<li>共通 UI は MAUI クラスライブラリで <strong>ハンドラー/XAML/プラットフォーム実装</strong>を同居。</li>
</ol>
<p>この再編により、メンテナンスコストは大幅に低減し、将来的な <strong>Blazor Hybrid</strong> 併用、Windows/Mac 対応の拡大、パッケージ分割などの拡張にも強い構造が得られます。</p>
```
</section>
<section>
<h2>Appendix:コマンド・スニペット集(そのまま使える)</h2>
<h3>ソリューション操作</h3>
<pre><code>cd ProjectB
dotnet sln add ..\ProjectA\ProjectA.csproj
dotnet sln add ..\ProjectC\ProjectC.csproj
<h3>NuGet の代表置換</h3>
<table>
<thead>
<tr>
<th>XF パッケージ</th>
<th>MAUI 置換例</th>
<th>備考</th>
</tr>
</thead>
<tbody>
<tr>
<td>Xamarin.Forms</td>
<td>(不要)</td>
<td>MAUI へ移行。</td>
</tr>
<tr>
<td>Xamarin.Essentials</td>
<td>(MAUI に統合)</td>
<td>名前空間のみ調整。</td>
</tr>
<tr>
<td>Xamarin.CommunityToolkit</td>
<td>CommunityToolkit.Maui</td>
<td>登録:<code>.UseMauiCommunityToolkit()</code></td>
</tr>
<tr>
<td>ZXing.Net.Mobile</td>
<td>ZXing.Net.Maui</td>
<td>権限の宣言が必要。</td>
</tr>
</tbody>
</table>
<h3>Directory.Build.props でルール統一(推奨)</h3>
<pre><code><Project>
latest enable true
<h3>App.xaml(テーマ・マージ辞書の例)</h3>
<pre><code><Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ProjectB.App">
#512BD4
以上で、Xamarin Forms の 7 プロジェクト構成を .NET MAUI の単一プロジェクトへ集約するための全手順と設計指針を網羅しました。ここまでの方針に従えば、Project B(MAUI App) がハブとなり、Project A(非 UI ロジック)・Project C(共通 UI) が適切に分離された拡張性の高いアーキテクチャを確立できます。

コメント