ラベル FAC の投稿を表示しています。 すべての投稿を表示
ラベル FAC の投稿を表示しています。 すべての投稿を表示

2022/04/26

FAC製作記 その4 MVVMフレームワークをPrismからCommunityToolkit.Mvvmに移行する

2021年11月に.NET 6がリリースされました。
FACも.NET 6でビルドし直して、ついでにあれこれ手を加えて、3月19日に.NET 6版をリリースしました。
今回は「あれこれ手を加えて」の1つ、MVVMフレームワークの移行についてです。

なお、今回からコード類はQiitaに載せることにしました。

MVVMフレームワークの移行についてはその2でも少し書きました。
その2のときは.NET 5対応に伴って、MVVM Light ToolkitからPrismに移行しました。
今回はタイトルの通り、PrismからCommunityToolkit.Mvvmに移行しました。

【移行した理由】
前回はMVVM Light Toolkitが.NET 5に対応しないから、という理由でした。
今回は少し理由が違っていて、Prismと一緒に使っていたライブラリが開発を終了したためです。

FACではPrismを使うにあたり、Prism.Unityを使っていました。
で、これを構成するDIコンテナのUnity Containerが、2021年11月9日付けで開発とメンテナンスを終了しました。
今でもNuGetからインストールすることはできますが、非推奨となっています。

Unity ContainerはDIコンテナなので、別のDIコンテナに切り替えることも考えました。
例えば、Unityと並んでPrismと統合されているDryIocとかがあります。
ですが、仕事で調べ物をしている最中に、偶然「Microsoft.Toolkit.Mvvm」というものに遭遇しました。
せっかくなのでこれを調べてみて、「MVVM Light Toolkitみたいじゃねえか!」ということで移行を決意しました。

【Microsoft.Toolkit.MvvmとCommunityToolkit.Mvvm】
調べて出てきたのは「Microsoft.Toolkit.Mvvm」、俺がFACで使っているのは「CommunityToolkit.Mvvm」です。
NuGetで「mvvm toolkit」とかで検索すると、同じアイコンで2つ出てきます。
安定版の最新バージョンはどちらも同じで、CommunityToolkit.Mvvmは新しいメジャーバージョンのプレビュー版があったりします。
どうやら将来的にはCommunityToolkit.*に統一される予定らしいということで、CommunityToolkit.Mvvmを使うことにしました。

【移行する】
結構力技でやりました。
一応順を追って行きます。

1. 参考にするためにMVVM Light Toolkitを使うアプリとCommunityToolkit.Mvvmを使うアプリを作る
いきなり脱線します。
というのも、FACではPrism.Unityを使うためにApp.xaml/App.xaml.csを書き換えていました。
書き換える前のコードなんか覚えていないので、丸写しするためにテスト用のアプリを作ったわけです。

2. NuGetで必要なライブラリをインストール
CommunityToolkit.MvvmとMicrosoft.Extensions.DependencyInjectionをインストールします。
後者はDIコンテナです。
Unity Containerから脱却するためのMVVMフレームワーク移行なので、DIコンテナは使い続けます。

3. App.xaml/App.xaml.csを書き換える
1.で作ったCommunityToolkit.Mvvm版のコードを丸写しします。
名前空間が違うのでそこだけ書き換えます。

4. ロケーターを用意する
Prism.UnityではApp.xaml.csでDIの設定をしていましたが、ばっさり削除してしまったのでどこかで設定する必要があります。
そこでViewModelLocatorに相乗りさせることにしました。
通常はViewModelsフォルダーにViewModelLocatorというクラスを用意します。
MVVM Light Toolkitの場合はNuGetでインストールするだけで自動生成してくれるファイルですね。
これにDIも含めた役割を持たせるために、プロジェクト直下にLocatorというクラスを用意しました。
ここにCommunityToolkit.MvvmのViewModelLocatorと、Microsoft.Extensions.DependencyInjectionのDIの設定をまとめてしまいます。
最後にApp.xamlでアプリのリソースにLocatorクラスを登録すればOKです。

5. ViewとViewModelを修正する
MVVMフレームワークを移行するので、ViewModelへの影響は避けられません。
また、FACの場合はViewとViewModelの紐づけにPrismの機能を使っていたので、これを削除する必要があります。
紐づけはMVVM Light Toolkitのころと同じく、ViewのDataContextをXAMLで設定することで実現します。

Gitのログを見てみると、2.~5.で約2時間半という感じでした。

.NET 5対応に伴ってPrismに移行したときには、また移行することがあるとは思ってもいませんでした。
MVVMフレームワークはアプリ作りの根幹にかかわることだけに、そうそう移行をするものではないですが、これもまた経験と捉えましょう。
MVVM Light Toolkitは気に入っていたので、同じ感覚で使えるのは嬉しいポイントですね。

今回のコードはこちらに載せました。


今日の1曲:

2021/06/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/06/06

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に戻せないか、後日再チャレンジして見るかもしれません。

【参考資料】

2021/05/30

FAC製作記 その1 ダイアログを表示するときに例外発生

先日の通り、FACを作るときに遭遇したあれやこれやを早速書いていきます。

本日のお題はダイアログです。
GUIアプリケーションであれば、ダイアログを使わないもののほうが少ないのではないでしょうか。
FACでも結果の通知とフォルダーの選択にダイアログを使っています。

今回の現象と解決策だけ書くと以下のとおりです。

【何があったのか】
.NET Framework 4.7.2から.NET 5に移行するとき、ダイアログの表示で例外が発生した。

【どうしたのか】
ダイアログのライブラリを乗り換えた。

それぞれについて詳しく書く前に、.NET Framework 4.7.2時代のダイアログについて説明します。

【コトが起きる前】
.NET Framework 4.7.2時代、FACはダイアログとしてWindows API Code Packを使っていました。
これはもともとMicrosoftが提供していたもののようですが、俺が知ったときにはすでにMicrosoft直々の公開は終了していて、いろいろなユーザーがかつて公開されていたものをそのままor独自のモディファイを加えて公開している状況でした。
上記のリンクはそれらのうち俺が使っていたものです。

たかだかダイアログでなぜライブラリを使っているのかというと、標準では足りなかったからです。
FACはGUIフレームワークとしてWPFを使っています。
WPFにもダイアログは用意されているのですが、問題があります。
用意されているのは通常の通知用ダイアログのみで、フォルダーを選択するダイアログはないのです。(FACでは使っていませんが、ファイル選択ダイアログもない)

WPFでフォルダー選択ダイアログを使いたい場合、Windows API Code Packは定番と言っていいでしょう。
それについて解説しているブログもすぐ見つかるくらいなので、王道と言ってもいいかもしれません。
Windows API Code Packには通常のダイアログ(※)も用意されているので、ダイアログはすべてWindows API Code Packを使っている、というのが.NET Framework時代のFACでした。
※正確には機能が強化されたTaskDialogなのですが、通知用であれば同じ感覚で使えます。

【起きたこと】
FACを.NET 5に移行するため、事前検証としてテスト用アプリを作って、色々と動作を確認していたとき、事件は起こりました。
Windows API Code PackのGitHubのreadmeにも「.NET 5をサポートしてるよ!」と書いてあるので、同じようにコード書けば動くだろー、と楽観視していました。

まずは通常のダイアログの動作を確認します。


例外が発生しました。
サポートしてるんじゃなかったんかい。

次にフォルダー選択ダイアログの動作を確認します。


こちらは.NET Framework 4.7.2のときと同じように表示されます。
なんでやねん。

【例外のメッセージから調べる】
気を取り直して、例外のメッセージを確認してみます。

System.NotSupportedException: 'TaskDialog feature needs to load version 6 of comctl32.dll but a different version is current loaded in memory.'
EntryPointNotFoundException: Unable to find an entry point named 'TaskDialogIndirect' in DLL 'Comctl32.dll'.

「TaskDialogはcomctl32.dllのバージョン6以上が必要だが、そうでないバージョンがロードされている。」「comctl32.dllに'TaskDialogIndirect'というエントリーポイントがない。」とあります。
調べてみると、dllファイルのバージョンをマニフェストファイルで指定すれば良い、とのこと。

しかし、ここで俺は以下のように思いました。
「.NET Frameworkではマニフェストファイルなんか不要だった。.NETになって必要になるのは納得いかない」
今思い返すと逆ギレですが、「じゃあ別のダイアログを探そう」という発想に行き着きました。

【乗り換え先のダイアログを探す】
「.NET 5対応」「NuGetでインストール可能」を条件に探してみると、Ookii.Dialogsというものが見つかりました。
余談ですが、こちらの作者の方は東大に留学経験があり、サイトのトップにも「Ookii -発音は/oːkiː/(だいたい"oh-key")- は日本語でbigという意味で、私のラストネームと同じ意味です」とあるので、「大きい」を意識した命名です。
さっそくインストールして、Windows API Code Packのダイアログ類を使っていたところをOokii.Dialogsに差し替えてみます。

まずは通常のダイアログから。


問題なく表示されますね。
続いてファイル選択ダイアログも。


こちらも問題なく表示されました。

ということで、この件はWindows API Code PackからOokii.Dialogsへの乗り換えという形で解決しました。

【原因と解決策の考察】
精査したわけではないので、俺の想像であることをご承知おきください。
すでに書いたように、Windows API Code PackはもともとMicrosoft謹製です。
名前から想像するに、WindowsのAPIをごく薄くラップしたライブラリ類なのでしょう。
したがって、.NET Frameworkと .NET 5の差分(標準で参照するcomctl32.dllのバージョンが違う?)を隠蔽しきれなかったのではないかと思います。

Ookii.DialogsはTaskDialogクラスを独自に実装しています。
そのためにcomctl32.dllを参照する必要がなく、Windows API Code Packで地雷になった箇所の影響を受けなかったのではないでしょうか。

【参考資料】

今日の1曲:


2021/05/22

作ってるもの

ブログの記事としてはお久しぶりです。
前回の記事が2016年の10月ですので、約4年半ぶりです。

この間特に何かあったとかではないのですが、
「1週間空いてしまった、来週は書こう」
「そうこうしてるうちに1ヶ月空いてしまった、来月こそ書こう」
「1年空いたし、もうよくね?」
という感じで、ずるずると4年半以上何も更新がないという事態になっていました。
これからは不定期でも年に1回くらいは更新したいな、と思っています。

今回のタイトルの「作っているもの」ですが、パソコン上で動かす簡単なツールを作っています。
名前を「Fur Affinity Classifier」としました。以下では頭文字取って「FAC」と記載します。
ソースコードや実行ファイルはGitHubにおいてあります。
READMEは英語で書いてありますが、今回は日本語での説明兼宣伝をさせてください。

【何これ?】
Fur Affinity(以下FA)からダウンロードしたファイルを、ユーザーのIDごとにフォルダーに分類するツールです。
FAについてはご存じの方が多いと思うので、詳しい説明は省略しますが、ケモノに特化したpixivと考えていただいてよいと思います。
classify (分類) に -er を付けて「分類機」という意味合いで名付けました。

【なんで作ったの?】
俺はFur Affinityで気に入ったイラストなどがあると自分のパソコンに保存するのですが、ファイルの分類が手間でした。
ファイルを分類するためには
1. ファイル名を見て
2. ユーザーIDを確認して
3. ユーザーIDに応じたフォルダーを開いて
4. ファイルを移動する
という手順が必要でした。
これがファイル2~3個とかならまだなんとかなりますが、5個10個となると面倒、20を超えると分類に割く時間がもったいないくらいになります。
じゃあ自分の勉強も兼ねて、自動でやってくれるツールを作ってしまえ、というのがスタートラインになっています。

【何ができるの?】
「何これ?」の通り、FAからダウンロードしたファイルを、フォルダーに分類することができます。
FAからダウンロードしたファイルは所定の形式になっていますので、後述する対象のフォルダー内のファイルを走査して、合致するファイルを移動します。
また、「このユーザーはIDと違うフォルダーに振り分ける」という設定もできます。
日本のような非ラテン文字圏だとこういう事態は起きやすいのではないかと思います。

【どうすれば使えるの?】
FACの実行には.NET 5が必要になります。
また、FAC内でも設定が必要です。
実際に俺が使っているときのスクリーンショットを以下に貼ります。
フォルダー名は黒で潰してありますがご了承ください。


スクショのうち「フォルダー設定」、つまり「ダウンロードしたファイルが入っているフォルダー」「IDごとのフォルダーが入っているフォルダー」の設定が必須になります。
IDと違うフォルダーに振り分けたい場合は、スクショで「IDと異なるフォルダーに分類する」となっている一覧に入力することで、「ID」列のIDは「フォルダー」列のフォルダーに振り分けるようになります。

【注意することとかある?】
あくまでも「俺が使うためのツール」を公開したものであるので、要望をいただいた場合は検討しますが、対応をお約束することはできません。
ライセンス(MIT)のとおり、保証とかは一切ありません。

今後はFACを作る上で困ったこととその解決法とか、そういうことも書いていきたいなと思っています。
「え、こんな簡単なモンでもそんなんあるの?」と思われるかもしれませんが、あるんだなこれが。

今日の1曲: