2021年6月19日土曜日

FAC製作記 その3 JsonSerializer.SerializeAsync()で何も出力されない

本題の前に少しだけ、これまでの反省といいますか、ちょっとした注意書きを。
前回が前提となる事柄に分量や気力などを吸い取られまくったので、これからはそういう事柄は適当にカットします。

ここから本題。
これまでの2回はダイアログでしたが、今回はJSONの処理についてです。
FACでは設定した内容をsettings.jsonというファイルにJSON形式で保存しています。
起動したときにファイルから設定を読み込んで、画面の「設定を保存」ボタンをクリックするとファイルに設定を書き込むようになっています。 (画面についてはFACについての最初の投稿にスクショを貼っています)

【FACでのJSONの扱い方】
.NET 5対応の前後でJSONのシリアライズ/デシリアライズに使っているものが異なっています。
.NET 5対応前:Jil
.NET 5対応後:JsonSerializer
Jilは個人が作っているライブラリ、JsonSerializerは.NET Core 3.0/C# 8.0から追加された標準機能です。

今回の課題と解決策は以下の通りです。

【課題】
JsonSerializerの非同期シリアライズ&書き込みを使うと何も出力されない。

【解決策】
JsonSerializerはJSONのシリアライズ/デシリアライズにだけ使う。
ファイルの読み書きはStreamReader/StreamWriterに任せる。

今回は.NET 5対応のときに作ったテスト用アプリのソースコードを見ていただいたほうが早いと思います。

まずは読み込みから。


読み込みのメソッド全部でこれ。
コメントアウトしているDeserializeAsync()がストリームを使った非同期デシリアライズ&読み込みですが、こちらはうまくいきました。

一方の書き込みが以下になります。


try-catchのtry部分しか収まりませんでした。
コメントアウトしたコードにあるSerializeAsync()が非同期のシリアライズ&書き込みメソッドです。
例外は発生しなかったのですが、「課題」に書いたとおり、書き込み完了後にファイルを開いても何も出力されていない、という現象が発生しました。

【試行錯誤】
まずはFileStreamを作るときに使ったメソッドが良くないのではないか?と疑いました。
スクショの77~79行目で、Open()を使ったり、オプション指定するコンストラクターを使ったりと試しています。
これらはいずれも効果なく、ファイルには何も出力されない状態が続きました。

次にそもそもシリアライズは成功しているのか?ということで、Serialize()でシリアライズした結果をデバッグ出力させてみることにしました。
スクショの82と83行目です。
こちらは成功して、JSON文字列が出力されました。

以上の2つから、JsonSerializerはシリアライズだけ担当してもらい、ファイルへの書き込みは別にすればうまくいくのでは?という発想が出てきます。
それを試したのが90~92行目です。
コメントにある通りで、ファイルにはJSON文字列が出力されていました。成功です。

さて、これで読み込みはすべてJsonSerializerで、書き込みはJsonSerializer+StreamWriterで処理している状態になりました。
これでもちろん動くのですが、書き込みがJsonSerializer+StreamWriterなら、読み込みはJsonSerializer+StreamReaderにしたほうが、対照というか見栄えというか、きれいに見えませんか?

ということで今度は読み込み側に手を加えてみます。
ファイルからの読み込みをStreamReaderに受け持ってもらい、JsonSerializerはデシリアライズだけ担当してもらいます。
読み込みの52と53行目ですね。
実行した結果は変更前と変わらず、こちらは特に引っかかりなく書き換えに成功しました。

なので、今回はJsonSerializerを使う範囲をJSONとオブジェクトの変換に限定し、ファイルの読み書きはStreamReader/StreamWriterを使うことで解決としました。

【考察】
最初はファイルへの書き込みが終わる前にファイルを開いてしまったのが原因かもしれないと思ったのですが、それなら書き込めずに例外が発生するはずですので、この可能性はないでしょう。
次にGitHubでJsonSerializerのソースコードを見てみたのですが、わかりませんでした。
処理をたどっていくと、どうやらストリームに直接書き込まずにバッファリングしているようですが、バッファリングしようがしまいが最終的に出力先に書き込むのは変わらないはずなので、これのせいだとも思えません。
なんだったんでしょうね・・・?

【参考資料】

今日の1曲:


2021年6月6日日曜日

FAC製作記 その2 ダイアログの表示をどう実装するか

前回に続きダイアログがテーマです。
前回はそもそもダイアログを表示できないところからスタートし、それを解決して表示できるようになりました。
今回はダイアログを表示できるところからスタートしますが、どこでどのように表示するかがポイントになります。
「いや、必要なタイミングで表示させるだけでしょ?」となりそうですが、その表示させる「だけ」にも考えなければならないことはあるわけです。

今回の課題と解法は以下のとおりです。

【課題】
MVVMの役割分担を壊さずにダイアログを表示したい。
.NET Framework時代はMVVMフレームワークの機能を使って実現していたが、.NET移行でライブラリを乗り換えたので、別の方法を探さなければいけない。

【解法】
ヘルパークラスを使う。
ヘルパークラスはM/V/VMいずれでもない名前空間に作る。

本題の前にMVVMについて。
MVVMは「Model」「View」「ViewModel」の頭文字を取ったもので、この3つでアプリケーションを構成します。
それぞれの役割は以下の通り。
  • Model:アプリケーションの本体、UIに関わらない内部の処理
  • View:ユーザーに見える部分、画面とか
  • ViewModel:ModelとViewの橋渡し
以下はWikipediaの図ですが、MとVM、VとVMがやり取りしてアプリの機能を実現します。
(作者:Ugaya40, CC BY-SA 3.0)

例えばFACの「ファイルを分類する」という機能の場合は以下になります。
1. ユーザーがVの実行ボタンをクリックする
2. VからVMに実行ボタンが押されたことが伝わる
3. VMはMのファイル分類を実行する
4. MからVMにファイル分類の結果が返される
5. VMからVにファイルの分類が完了したことが伝わる
6. Vがユーザーにファイルの分類が完了したことを見せる

MVVMは必ず従わなければいけない規則というわけではありません。
しかし、1箇所に全部まとめてしまうとメンテナンスも大変なので、FACでもMVVMパターンを採用しています。

ではMVVM的にダイアログはどう扱うべきでしょうか?
ダイアログはユーザーに見える部分ですから、Vが扱うべきである、と考えることができます。
しかしVは表示とユーザーからの入力を受け付けることが役割ですから、「いつ」「何を」表示するのか決めるのは越権行為です。
「いつ」「何を」表示するか決めるのはM、またはMとVMの役目と言えます。
ではMとVMで扱うのが正しいのかというと、今度はMやVMが直接画面を触るという越権行為が発生します。

もう1点、MVVMの解説図に「Data Binding」というのがあります。
日本語でもそのまま「データバインディング」と言われます。
Vでイベント(例:ボタンのクリック)が発生したときにVMのどの処理を実行するか、Vにどんなものを表示するのか、など、VとVMを紐付ける仕組みです。
MVVMパターンを使う上で重要なポイントではあるのですが、.NETや.NET Framework標準ではこの機能が用意されていません。
ですので、MVVMパターンを使うときは、データバインディングを含めてフレームワークのライブラリを使うことが一般的です。

ではFACでは.NET Framework時代にどうしていたのか、.NETではどうするのか、1つずつ書いていきます。

【.NET Framework時代】
MVVMフレームワーク:MVVM Light Toolkit
MVVM Light Toolkitはその名の通り薄い軽いフレームワークでした。
シンプルですが、データバインディングを始め必要なものは用意されています。

この時代はダイアログの表示をMVVM + Messengerで実現していました。
Messengerはその名の通り、VMからのメッセージをVに渡す役割をします。
VはMessengerに「xxというメッセージが来たらこの処理を呼んでくれ」と登録します。
VMは必要なときにMessengerにxxというメッセージを送ります。
これでVM→(メッセージ)→Messenger→(登録された処理の呼び出し)→Vという流れができます。
なお、本来はMessengerも自分で作らなければならないのですが、MVVM Light ToolkitにはMessengerも用意されています。

今回の場合、VがMessengerに登録する処理でダイアログを表示しています。
こうすることでVMから直接Vを触ることなく、つまりMVVMの役割分担を壊さずにダイアログを表示することができます。

【.NET時代】
MVVMフレームワーク:Prism
Prismはある意味MVVM Light Toolkitとは逆で、盛り沢山なフレームワークです。
機能的にはMVVM Light Toolkitで十分なのですが、最新版のリリースが2018年9月と古く、.NETに対応する様子もないため、.NETへの移行を期にPrismに乗り換えることにしました。

PrismにもMessengerに使えるEventAggregatorが用意されていますが、調べてみるとMVVM Light ToolkitのMessengerと全く同じというわけにはいかないようです。
人によっては「EventAggregatorはVM間の通信を実現する」と言い切っているくらいで、VとVM間の通信に使うのはちょっと怖くなってきました。

ではどうするのか。ここで先に書いた解法です。
FACのフォルダ/名前空間構成は以下のようになっています。
「Models」「ViewModels」「Views」にMVVMのそれぞれにあたるクラスが入ります。
そして、選択して色が変わっている「Helpers」というフォルダがあります。
ここにダイアログを表示するDialogHelperというクラスを入れてあります。(IDialogHelperはDialogHelperで実装する内容を定めたインターフェースです)

ダイアログを表示するときはVMがDialogHelperを呼び出します。
VM→(処理の呼び出し)→Helperという流れですね。
この方式でも、VMから直接Vを触ることなくダイアログを表示することができます。

【残った課題】
Helperクラスを使うことで、「VMが直接ダイアログを表示したりVを触ったりしない」という条件はクリアできました。
しかし「表示はVが受け持つ」という条件はクリアできていません。
.NETに移行するときにはライブラリの乗り換えなど大きな問題が積み重なっていたため、「VMに表示処理が入っていなければヨシ!」と現場猫化しました。
ということで、.NET Framework時代と同じく、ダイアログの表示はVに戻せないか、後日再チャレンジして見るかもしれません。

【参考資料】