🍐

Xamarinからできるだけ簡単に.NET MAUIへ移行したい

2023/08/22に公開

下準備

1. 開発環境を Visual Studio 2022へ更新(筆者の場合は2017より)

2. Xamarin.Forms 5.0へ更新(同じく4.7より)

  • Android のターゲットを10にするよう要求されました(9より)。

3. ビルドエラー「証明書が提供された署名の拇印と一致しません。」

  • 5.0では仕組みが変わったらしく*.UWP_TemporaryKey.pfxは邪魔なので削除します。

4. UWPで描画されない

  • Windows のターゲットを 10.0.19041へ上げます(10.0.16299より)。

ようやく下記の前提条件に到達します。
https://learn.microsoft.com/ja-jp/dotnet/core/porting/upgrade-assistant-install

5. 拡張機能で「.NET Upgrade Assistant」をインストール

6. ソリューションエクスプローラで、4プロジェクト(コア, iOS, Android, UWP)とも、右クリックから「Update」を実行

これにて Xamarin / .NETCore2.0 から MAUI / .NET6.0 へのマイグレーションがとりあえず実行されました。

手動マイグレーション

各種解説を拝見し、筆者なりに試行錯誤してみましたが、.NET MAUI の新規プロジェクトを起こし、ここに自分で作ったcs/xamlファイル等をコピーする方法がもっとも楽であろうと判断しました。

1. .NET MAUI アプリのプロジェクトを .NET 6.0 で新規作成

  • C# のバージョンはcsprojで指定しないかぎり 10.0となります。

2. コア部分のコピー

  • cs/xamlのセットが入ったViews, ViewModels, Modelsフォルダ等は、丸ごとプロジェクトのルートに追加します。
  • cs/rexのセットはResourceフォルダに追加します。
  • (筆者の場合)生成されたAppShellのセットは邪魔なので削除します。
     これに対応して、生成されたApp.xaml.csnew AppShell()は取り去ります(下記)。
App.xaml.cs
public partial class App : Application
{
	public App()
	{
		InitializeComponent();
		//MainPage = new AppShell();
		MainPage = new MainPage();
	}
	protected override void OnStart()
	{
		// Handle when your app starts
		(独自の実装があれば記載)
	}
}

3. 各プラットフォーム毎のコピー

  • csprojファイルは要らないです。AssemblyInfo.csも要りません。
  • AndroidフォルダにMainActivity.cs, MainApplication.csがありますので、必要に応じて独自の実装を書き込みます。
  • iOSフォルダにProgram.cs, AppDelegete.csがあります。Program.csは元のMain.csが改名されたものとみなして良いです。必要に応じて独自の実装を書き込みます。
  • WindowsフォルダにはApp.xaml.csしかないので、元のMainPage.cs内に独自の実装があればここに移します(下記)。
Platforms/Windows/App.xaml.cs
public App()
{
	this.InitializeComponent();
}
protected override MauiApp CreateMauiApp()
{
	(独自の実装があれば記載)
	return MauiProgram.CreateMauiApp();
}

4. バンドルファイルのコピーについて考察

新規プロジェクトのResources/RAWに入れ、下記の方法でアクセスすることが推奨されています。バンドルファイルに対して何かしらの変更をかけたい場合は、アプリデータフォルダーへコピーしそこで作業する旨が説明されています。

https://learn.microsoft.com/ja-jp/dotnet/maui/platform-integration/storage/file-system-helpers

しかしここで大問題。.NET MAUI のファイル操作 Microsoft.Maui.Storageはもれなくawaitが付いており非同期で実行されてしまいます。例えばViewModels内から sqlite に書き込みたいなら、アプリデータフォルダーへコピーが完了したことを確認してから、ロードしてアクセスする手順を踏まないといけません。正解は、
https://learn.microsoft.com/ja-jp/dotnet/maui/data-cloud/database-sqlite
に書いてあるようですが、ロジックを変えず Xamarin から気軽に移行したいと考えている身にとっては荷が重すぎます。

また、HTTPS 通信の証明書ファイルのような readonly 用途なら、アプリデータフォルダーへのコピーが必要ないから大丈夫かと思いきや、バンドルファイルの読み込みに用意されているのは、非同期動作のFileSystem.Current.OpenAppPackageFileAsync()なので、証明書のパスを API に直接渡す用途には使えません。下記のように、p12 ファイルがアプリデータフォルダーに確実にコピーされたことを前提とした実装とするしかないのでは?と思います。

var dir = FileSystem.Current.AppDataDirectory;
X509Certificate2 _x509 = new X509Certificate2(Path.Combine(dir, "p12ファイル"));

5. バンドルファイルのコピーについて結論

以上を踏まえて、コアに渡った後はいつでも sqlite への書き込みや https 通信の証明書を読み込めるように、各プラットフォーム側の実装でMauiProgram.CreateMauiApp()を呼ぶ前に、FileSystem.Current.AppDataDirectoryフォルダに、ファイルが確実にコピーされるようにしたいと思います。

• iOSの場合

バンドルファイルをResources/RAWに配置したうえで、下記のように実装しました。

Platforms/iOS/AppDelegate.cs
protected override MauiApp CreateMauiApp()
{
	string[] files = { "A", "B", "C" };
	foreach (var file in files)
	{
		string src = Path.Combine(NSBundle.MainBundle.BundlePath, file);
		string dst = Path.Combine(FileSystem.Current.AppDataDirectory, file);
		File.Copy(src, dst);
	}
	return MauiProgram.CreateMauiApp();
}

• Androidの場合

バンドルファイルをResources/RAWに配置したうえで、下記のように実装しました。

Platforms/Android/MainApplication.cs
protected override MauiApp CreateMauiApp()
{
	string[] files = { "A", "B", "C" };
	foreach (var file in files)
	{
		using (BinaryReader br = new BinaryReader(Android.App.Application.Context.Assets.Open(file)))
		{
			string dst = Path.Combine(FileSystem.Current.AppDataDirectory, file);
			using (BinaryWriter bw = new BinaryWriter(new FileStream(dst, FileMode.Create)))
			{
				byte[] buffer = new byte[2048];
				int len = 0;
				while ((len = br.Read(buffer, 0, buffer.Length)) > 0)
				{
					bw.Write(buffer, 0, len);
				}
			}
		}
	}
	return MauiProgram.CreateMauiApp();
}

• Windowsの場合

ファイル操作にはWindows.Storageが用意されていますが、すべてモダンな非同期であり、これでは目的であるMauiProgram.CreateMauiApp()を呼ぶ前のコピー完了を保証できません。かといってTask.Resultで無理やり同期処理とすると、デットロックを起こしてしまいます[1]。残念ながら、いにしえの Win32 API のお出ましであります。
バンドルファイルをResources/RAWに配置したうえで、下記のように実装しました。

Platforms/Windows/App.xaml.cs
protected override MauiApp CreateMauiApp()
{
	string[] files = { "A", "B", "C" };
	foreach (var file in files)
	{
		string src = Path.Combine(Package.Current.InstalledLocation.Path, file);
		string dst = Path.Combine(FileSystem.Current.AppDataDirectory, file);
		NativeMethods.CopyFile(src, dst, false);
	}
	return MauiProgram.CreateMauiApp();
}
internal static class NativeMethods
{
	[DllImport("Kernel32.dll", CharSet = CharSet.Unicode)]
	[return: MarshalAs(UnmanagedType.Bool)]
	internal static extern bool CopyFile(string lpExistingFileName, string lpNewFileName, bool bFailIfExists);
}

コードの手直し

1. 下準備の 6.で自動変換しきれなかったものを手直しします

  • Xamarin.Forms.Color
     →Maui.Graphics.Colors
  • Xamarin.Forms.Application.Current.Properties
     →Maui.Storage.Preferences.Get
  • Xamarin.Forms.Application.Current.Properties.ContainsKey
     →Maui.Storage.Preferences.ContainsKey

2. 拡張機能と Nuget から Xamarin を取り除きます

3. csファイル内のusingや、xamlファイル内のxmls=に、Xamarinが残存しているなら取り除きます

4. 筆者の場合ではありますが、IntelliSence によっていくつか指摘されたので、ここに列挙します

  • Device.OpenUri
     →Launcher.OpenAsync
  • Device.BeginInvokeOnMainThread
     →MainThread.BeginInvokeOnMainThread
  • NavigationPage.Icon
     →NavigationPage.IconImageSource

(了)

※参考にさせていただいた記事

https://zenn.dev/proudust/articles/2021-12-12-xamarin-upgrade-to-dotnet6
https://dev.classmethod.jp/articles/xamarin-forms-to-net-maui-manual-migrate/
https://hatsune.hatenablog.jp/entry/2023/03/21/121449

デットロックを起こす悪い例
protected override MauiApp CreateMauiApp()
{
	_ = copy().Result;
	return MauiProgram.CreateMauiApp();
}
async Task<int> copy()
{
	string[] files = { "A", "B", "C" };
	foreach (var file in files)
	{
		StorageFile src = await StorageFile.GetFileFromApplicationUriAsync(new Uri($"ms-appx:///{file}"));
		await src.CopyAsync(ApplicationData.Current.LocalFolder, file, NameCollisionOption.ReplaceExisting);
	}
	return 0;
}
脚注
  1. デットロックを起こす悪い例
    Task.Resultを使って同期処理にしてしまうとcopy()完了後のMauiProgram.CreateMauiApp()呼び出しという順序は保証されますが、ここは UI を描画するメインスレッドであるためcopy()内のawaitにとりかかった時点で、デットロックを起こしてしまいます。 ↩︎

Discussion