放浪軍師のアプリ開発局

VTuberみたいなアプリケーション夏狂乱など、自由気ままにアプリを開発したりしています。他にもいろいろやってます。尚、このブログはわからないところを頑張って解決するブログであるため、正しい保証がありません。ご注意ください。

Xamarin.FormsアプリでJsonによる永続化を行う

始まりました放浪軍師のアプリ開発。大変ご無沙汰しております。
前回大々的に Unity であんなの作りたいこんなの作りたいと語ったのに、実はまったく触れていませんゴメンナサイ。 …というのも、更新を終了しようと思っていた Xamarin 版乱ちゃん Project ですが、終了する前に今まで学んだことを生かして作り直したら、より良いものになるし、まとめとして良いのではないか?と思ったので、乱ちゃんProjectのコードを一新に没頭しておりました。そこで、今回から数回に渡り、その過程で学んだ内容をいくつか備忘録として残していこうと思いますのでよろしくお願いいたします。

Xamarin.Forms アプリで Json による永続化を行いたい

従来の乱ちゃんProjectは SQLite を用いて情報の永続化を行っていました。当時こんな記事も書いています。 www.gunshi.info しかし、新たに Model 層の作り直しを行った結果 Class の階層化が進み、SQLite のようなデータベースでの永続化が難しくなりました。そこで悩んでいると、

という質問を頂きました。なお、この時私はJsonをよくわかっていませんでした。

Jsonって何?

Wikipedia先生に尋ねるとこんな感じで教えてくれます。 ja.wikipedia.org
階層構造をまるっと保存したりするときに非常に便利です。テキストなので扱いも楽でGood!!さっそくやってみましょう。

サンプル

かなりオーバースペック気味なサンプルを作ってしまいました。MVVMの見本としても悪くないかも?

UWP Android
f:id:roamschemer:20200519000533g:plain:h400 f:id:roamschemer:20200519000628g:plain:h400
  • LOAD
    保存したデータを呼出できます
  • SAVE
    現在のデータを保存できます
  • ADD
    新しく項目を追加します
  • CLEAR
    全部消します
  • SAMPLESET
    サンプルデータを生成します
  • ↑↓
    並び順を入れ替えます
  • DEL
    項目を消します

  • 選択すると該当するPersonが表示されます

github.com

環境

Xamarin.Forms 4.6.0.726
ReactiveProperty 7.0.0
Prism.Unity.Forms 7.2.0.1422
System.Text.Json 4.7.2

System.Text.Json

2020年現在、C#Jsonを扱う場合はこのSystem.Text.Jsonを使うのが主流であるようです。ドキュメントはこちら。
docs.microsoft.com

インストール

NugetからSystem.Text.Jsonを検索してすべてのプロジェクトにインストールすれば使用できます。

シリアル化

Jsonにする事をシリアル化と言うそうです。今回はこんな感じで使用しました。

public void Save(string jsonFileName) {
    //ファイルパスを取得する。各プラットフォーム対応。
    var fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), $"{jsonFileName}.json");
    //オプションを指定する
    var options = new JsonSerializerOptions {
        Encoder = JavaScriptEncoder.Create(UnicodeRanges.All), //日本語もいけるようにする
        WriteIndented = true //良い感じに改行してくれるようにする
    };
    //thisをjson文字列にシリアライズする
    var json = JsonSerializer.Serialize(this, options);
    //文字列を保存する
    File.WriteAllText(fileName, json);
}

これでthisのプロパティがすべてjson化されます。なお、Xamarin.Formsなのでファイルパスの指定は特殊な記述になっています。詳しくはこちらのドキュメントを参照してください。 docs.microsoft.com

逆シリアル化

こちらはJsonをデータ化する事を言います。今回はこんな感じで使用。

public void Load(string jsonFileName) {
    //一旦消しておく
    Categorys.Clear();
    //ファイルパスを取得する。各プラットフォーム対応。
    var fileName = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), $"{jsonFileName}.json");
    //ファイルが無ければサンプルを生成する
    if (!File.Exists(fileName)) {
        CreateSample();
        return;
    }
    //Jsonファイルを文字列として読み込む
    var json = File.ReadAllText(fileName);
    //Json文字列を逆シリアライズ
    var coreModel = JsonSerializer.Deserialize<CoreModel>(json);
    //読み込んだmodelに置き換える
    foreach (var x in coreModel.Categorys) { Categorys.Add(x); }
}

ビックリするぐらい簡単ですね。

罠があるので注意しよう

簡単ですねとか言いながら実は結構ハマりました。この System.Text.Json にはいくつか罠があるんですよ…。私が今回ハマった罠は以下のとおりです。

罠1 プロパティは public な Setter が必要

プロパティの Setter は public でないと逆シリアル化時に読み込まれません。この時エラーは吐かないので気が付きにくくハマります。

罠2 パラメータなしのコンストラクタが必要

逆シリアル化の際、パラメータなしのコンストラクタがないとエラーになります。これは逆シリアル化するプロパティで指定したクラスすべてに必要です。継承元のクラスも同様です。ただし、コンストラクタ自体を記述していない場合はパラメータなしのコンストラクタがある扱いになるのでエラーにはなりません。わかりにくいので例を挙げると以下のようになります。

//コンストラクタの記述がないのでOK
public class TestClass1 {
    public string Name { get; set; }
}

//パラメータなしのコンストラクタがないのでNG
public class TestClass2 {
    public string Name { get; set; }

    public TestClass2(string name) {
        Name = name;
    }
}

//パラメータなしのコンストラクタ(オーバーロード)があるのでOK
public class TestClass3 {
    public string Name { get; set; }

    public TestClass3() {
    }

    public TestClass3(string name) {
        Name = name;
    }
}

なお、カスタムコンバータという機能を使えばこれを回避することが可能だそうです。 docs.microsoft.com

罠3 UWPのReleaseビルドでシリアライズ時にエラー

これはほんとに危険だと思うのですが、UWPのReleaseビルドを行うとシリアライズ時にSystem.Reflection.MissingMetadataExceptionというエラーを吐きます。

f:id:roamschemer:20200518233919p:plain:w450

これはDebugビルド時には発生しないエラーなので本当に注意が必要です。ちなみに私はこれに気が付かずUWP版乱ちゃんProjectをリリース。直後ユーザーより指摘されて初めて発覚しました。シャレにならんて…

なお、これの解決策ですが以下のissueに記述がありました。

github.com

ここの英語でのやり取りをGoogle先生に翻訳してもらいましたが、残念ながら言っていることの意味が理解できませんでした。ただ、解決策としてUWPプロジェクトの Properties/Default.rd.xml を開き、以下のように記述すればエラーは回避できることは確認できました。

<Directives xmlns="http://schemas.microsoft.com/netfx/2013/01/metadata">
  <Application>
    <!--
      An Assembly element with Name="*Application*" applies to all assemblies in
      the application package. The asterisks are not wildcards.
    -->
    <Assembly Name="*Application*" Dynamic="Required All" />
    
    
    <!-- Add your application specific runtime directives here. -->
    <!-- ここから -->
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterString" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterUri" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterBoolean" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterInt32" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterInt16" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterByte" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterDecimal" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterGuid" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterEnum" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterDateTime" Dynamic="Required All" />
    <Type Name="System.Text.Json.Serialization.Converters.JsonConverterDateTimeOffset" Dynamic="Required All" />
    <!-- ここまで -->

  </Application>
</Directives>

まとめ

とまぁ結構シャレにならない罠がありますが、非常に便利なので System.Text.Json は積極的に使っていこうと思います。