始まりました放浪軍師のアプリ開発。大変ご無沙汰しております。
前回大々的に Unity であんなの作りたいこんなの作りたいと語ったのに、実はまったく触れていませんゴメンナサイ。 …というのも、更新を終了しようと思っていた Xamarin 版乱ちゃん Project ですが、終了する前に今まで学んだことを生かして作り直したら、より良いものになるし、まとめとして良いのではないか?と思ったので、乱ちゃんProjectのコードを一新に没頭しておりました。そこで、今回から数回に渡り、その過程で学んだ内容をいくつか備忘録として残していこうと思いますのでよろしくお願いいたします。
Xamarin.Forms アプリで Json による永続化を行いたい
従来の乱ちゃんProjectは SQLite を用いて情報の永続化を行っていました。当時こんな記事も書いています。 www.gunshi.info しかし、新たに Model 層の作り直しを行った結果 Class の階層化が進み、SQLite のようなデータベースでの永続化が難しくなりました。そこで悩んでいると、
という質問を頂きました。なお、この時私はJsonをよくわかっていませんでした。すみません、つまんない質問なのですが、SQLiteってちょいちょい見かけるのですが何故そんなに使われるのでしょうか?
— Ueda (@megyo9) 2020年3月24日
個人的には、クライアントで完結する程度のケースの多くは単純にデータをjsonでシリアライズして保存するだけで十分じゃないかと思っちゃうのですが。
(DB苦手マンなので)
Jsonって何?
Wikipedia先生に尋ねるとこんな感じで教えてくれます。
ja.wikipedia.org
階層構造をまるっと保存したりするときに非常に便利です。テキストなので扱いも楽でGood!!さっそくやってみましょう。
サンプル
かなりオーバースペック気味なサンプルを作ってしまいました。MVVMの見本としても悪くないかも?
UWP | Android |
---|---|
- LOAD
保存したデータを呼出できます - SAVE
現在のデータを保存できます - ADD
新しく項目を追加します - CLEAR
全部消します - SAMPLESET
サンプルデータを生成します - ↑↓
並び順を入れ替えます - DEL
項目を消します - ≡
選択すると該当するPersonが表示されます
環境
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
というエラーを吐きます。
これはDebugビルド時には発生しないエラーなので本当に注意が必要です。ちなみに私はこれに気が付かずUWP版乱ちゃんProjectをリリース。直後ユーザーより指摘されて初めて発覚しました。シャレにならんて…
症状確認。Debagビルドでは発生せずReleaseビルドでのみ発生する。発生箇所はJsonSerializer.Serializeの模様。https://t.co/ENcK0O6zAi https://t.co/U2au47R828
— 放浪軍師🎲夏狂乱(VTuberみたいな抽選アプリ)配布中 (@roamschemer) 2020年4月22日
なお、これの解決策ですが以下のissueに記述がありました。
ここの英語でのやり取りを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 は積極的に使っていこうと思います。