Open3

[.NET MAUI] アプリにバンドルしたファイルの読み込み

GomitaGomita

入力用リストから引き続き、リスト生成に使用する車種のデータをハードコーディングせず、アプリケーションにバンドルしたファイルから読み込むようにする。

ファイルはプロジェクトフォルダ配下の「/Resources/Raw/cartype.json」として作成しておく。.NETのJSONパーサーではトレイリングコンマ(配列の最後の要素の「,」)はダメ。

/Resources/Raw/cartype.json
[
    { "Name": "コンパクト" },
    { "Name": "ミニバン" },
    { "Name": "セダン" },
    { "Name": "ワゴン" },
    { "Name": "SUV" },
    { "Name": "スポーツ" },
    { "Name": "商用" }
]

ここから先は対応するOSによって異なってくる部分だが、MacCatalystであればアプリ実行時のカレントディレクトリがxxx.app/となる。ビルドした際に出力されるxxx.appはファイルシステム上、フォルダと同様の扱いが可能であり、Finderでbin/Debug/net8.0-maccatalyst/maccatalyst-x64/xxx.appを右クリックして「パッケージの内容を表示」してみるとxxx.app/Contents/Resource/Raw/cartype.jsonと辿ることができる。よってstring json = File.ReadAllText("./Contents/Resources/Raw/cartype.json");とすればバンドルしたファイルを読み込むことが可能。

今回はiOSにも対応させたいが、iOSではビルドしたxxx.app/配下のフォルダ構造が異なり、xxx.app/Raw/cartype.jsonとなる。MacCatalystとiOSのフォルダ構造の差異を吸収するため、NSBundleクラスを使おう。

NSBundle.MainBundle.ResourcePathResourcesフォルダに対応するxxx.app/内のフォルダパスを取得できるので、ここから辿ったRaw/cartype.jsonファイルを読み込み、CarTypeクラスのリストにパースして、あとはListViewのItemSourceに突っ込むだけ!

MainPage.xaml.cs
using Foundation;
using System.Text.Json;
...
    private void MainPage_Loaded(object sender, EventArgs e)
    {
        string filePath = NSBundle.MainBundle.ResourcePath;
        filePath = Path.Combine(filePath, "Raw");
        filePath = Path.Combine(filePath, "cartype.json");
        string json = File.ReadAllText(filePath);
        var lst = JsonSerializer.Deserialize<List<CarType>>(json);
        CarTypeList.ItemsSource = lst;
        // 車種のリストを非表示にする
        CarType_Unfocused(sender, e);
    }
GomitaGomita

今回はMacCatalystとiOSのみなのでNSBundleで差異を吸収できたが、WindowsやAndroidにも対応させる場合はFileSystem.OpenAppPackageFileAsyncを使う必要がある。
https://learn.microsoft.com/ja-jp/dotnet/maui/platform-integration/storage/file-system-helpers?view=net-maui-8.0&tabs=windows#using-file-system-helpers

上記ページに書いてある「ReadTextFile」メソッドをそのままパクる。

MainPage.xaml.cs
    public async Task<string> ReadTextFile(string filePath)
    {
        using Stream fileStream = await FileSystem.Current.OpenAppPackageFileAsync(filePath);
        using StreamReader reader = new StreamReader(fileStream);

        return await reader.ReadToEndAsync();
    }

そして、NSBundleおよびFile.ReadAllText使っていたファイル読み込みの処理を下記の通り変更する。

MainPage.xaml.cs
+    private void MainPage_Loaded(object sender, EventArgs e)
-    private async void MainPage_Loaded(object sender, EventArgs e)
    {
+        string json = await ReadTextFile("Raw/cartype.json");
-        string filePath = NSBundle.MainBundle.ResourcePath;
-        filePath = Path.Combine(filePath, "Raw");
-        filePath = Path.Combine(filePath, "cartype.json");
-        string json = File.ReadAllText(filePath);
GomitaGomita

条件付きコンパイル

自前で作ったReadTextFile関数を使えば全OSに対応できるはずだけど、あえて#記号で始まる条件付きコンパイルを使用して、MAC+iOSとWindows+Androidに両対応する。

MainPage.xaml.cs
+#if IOS || MACCATALYST
using Foundation;
+#endif
...
        // 車種リストの生成(本来は初めてリスト表示をした際に生成すべき)
+#if IOS || MACCATALYST
		string filePath = NSBundle.MainBundle.ResourcePath;
		filePath = Path.Combine(filePath, "Raw");
		filePath = Path.Combine(filePath, "cartype_ver1.json");
		string json = File.ReadAllText(filePath);
+#else
+		string json = await ReadTextFile("cartype_ver1.json");
+#endif
		var lst = JsonSerializer.Deserialize<List<CarType>>(json);