Xamarinからできるだけ簡単に.NET MAUIへ移行したい
下準備
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より)。
ようやく下記の前提条件に到達します。
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.cs
のnew AppShell()
は取り去ります(下記)。
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
内に独自の実装があればここに移します(下記)。
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp()
{
(独自の実装があれば記載)
return MauiProgram.CreateMauiApp();
}
4. バンドルファイルのコピーについて考察
新規プロジェクトのResources/RAW
に入れ、下記の方法でアクセスすることが推奨されています。バンドルファイルに対して何かしらの変更をかけたい場合は、アプリデータフォルダーへコピーしそこで作業する旨が説明されています。
しかしここで大問題。.NET MAUI のファイル操作 Microsoft.Maui.Storage
はもれなくawait
が付いており非同期で実行されてしまいます。例えばViewModels
内から 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
に配置したうえで、下記のように実装しました。
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
に配置したうえで、下記のように実装しました。
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
に配置したうえで、下記のように実装しました。
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);
}
コードの手直し
下準備の 6.で自動変換しきれなかったものを手直しします
1.-
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 を取り除きます
cs
ファイル内のusing
や、xaml
ファイル内のxmls=
に、Xamarin
が残存しているなら取り除きます
3. 4. 筆者の場合ではありますが、IntelliSence によっていくつか指摘されたので、ここに列挙します
-
Device.OpenUri
→Launcher.OpenAsync
-
Device.BeginInvokeOnMainThread
→MainThread.BeginInvokeOnMainThread
-
NavigationPage.Icon
→NavigationPage.IconImageSource
(了)
※参考にさせていただいた記事
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;
}
-
デットロックを起こす悪い例
Task.Result
を使って同期処理にしてしまうとcopy()
完了後のMauiProgram.CreateMauiApp()
呼び出しという順序は保証されますが、ここは UI を描画するメインスレッドであるためcopy()
内のawait
にとりかかった時点で、デットロックを起こしてしまいます。 ↩︎
Discussion