20191202のC#に関する記事は11件です。

C# - Windows.Data.Jsonの謎

Microsoftのドキュメントに書いてあるJson関連クラスのメンバが、一部そのままの名前だとコンパイルが通らない。
参照設定しているファイルが間違っているのか?

適当にそれっぽい名前に変えてみると動いた。謎・・・

クラス ドキュメント上のメンバ 実際(?)
JsonObject Insert Add
JsonArray Append Add
JsonArray Size Count
JsonValue CreateNullValue ない?

環境

Windows10で下記を/r:オプションで指定してコンパイル

csc /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\system.runtime.windowsruntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.InteropServices.WindowsRuntime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.InteropServices.WindowsRuntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.dll ^
"/r:C:\Program Files (x86)\Windows Kits\8.1\References\CommonConfiguration\Neutral\Annotated\Windows.winmd" %*
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# - Windows.Data.Jsonの謎 / winmdの中身をILSpyで確認する

Microsoftのドキュメントに書いてあるJson関連クラスのメンバが、一部そのままの名前だとコンパイルが通らない。
参照設定しているファイルが間違っているのか?

適当にそれっぽい名前に変えてみると動いた。謎・・・

クラス ドキュメント上のメンバ 実際(?)
JsonObject Insert Add
JsonArray Append Add
JsonArray Size Count
JsonValue CreateNullValue ない?

環境

Windows10で下記を/r:オプションで指定してコンパイル

csc /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.WindowsRuntime\v4.0_4.0.0.0__b77a5c561934e089\system.runtime.windowsruntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime.InteropServices.WindowsRuntime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.InteropServices.WindowsRuntime.dll ^
/r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Runtime\v4.0_4.0.0.0__b03f5f7f11d50a3a\System.Runtime.dll ^
"/r:C:\Program Files (x86)\Windows Kits\8.1\References\CommonConfiguration\Neutral\Annotated\Windows.winmd" %*

ILSpyで確認する方法

C:\Windows\System32\WinMetadata に下記のようにメタデータ(winmdファイル)が居る。

image.png

上記の特定のファイル1個をILSpyにDrag&Dropすると見れる。

image.png

ただ、JsonValueCreateNullValueがなぜか居る・・。

ちなみに、クラスの直下にメンバーがいなくても、継承元(Base Types)に居たりする場合があるので注意。

脱線編 - 気になるWinRTの機能たち

  • Windows.Data.winmd をILSpyで見てみると、Windows.Data.Pdfなるものが居て、PDFの読み書きに使えそう。→ 試してみた
  • Windows.AI.winmd をILSpyで見てみると、Windows.AI.MachineLearningなるものが居る。VisualStudioでnugetしなくてもcsc.exeで機械学習やれちゃう?(Visual Studio自体は入れないとダメかもですが・・)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】シーンの重複読み込みをLINQで防ぐ

これは、 C# その2 Advent Calendar 2019 の5日目の記事です。
昨日は @advanceboy さんによる C# の REPL, スクリプティング環境の比較 でした。

Unityのシーンの重複読み込み対応をLINQでやる

Unityで、2つのシーンをまたいで共通する処理がある場合、追加読み込み専用の共通シーンを作ったりすることがあります。

ただし、追加読み込みは気を付けないと、重複して読み込んだりして、バグのもとになります。

今回は、LINQを使って、そのあたりを効率よくコーディングする手法を紹介します。

結論だけ知りたい方へ

以下のような静的クラスを作っておいて、シーン読み込みする際に、すでに読み込まれているかどうかを検査すれば、重複読み込みを避けることができます!

SceneController.cs
using UnityEngine.SceneManagement;
using System.Linq;

public static class SceneController
{
    /** 既にシーンが読み込まれているかどうか */
    public static bool AlreadyLoadScene(string name)
    {
        return SceneManager.GetAllScenes()
            .Any(scene => scene.name == name);
    }
}

この結論だけ読んでもピンと来ない方もいらっしゃると思いますので、順番に説明しますね!

サンプルプロジェクト

サンプルプロジェクトを以下に置きましたので、必要に応じてご確認ください。

GitHub / segurvita / UnityScenePractice

TitleシーンとMainシーンを行き来したい

ゲームを作る際に、タイトル画面からメイン画面に遷移するといった機能を実装することがよくあります。

たとえば、

  • タイトル画面は Title.unity
  • メイン画面は Main.unity

のように、別々のシーンファイルにするのが一般的です。

image.png

上記の図のように Mキーを押したらMainシーンに遷移 したい場合は、以下のようなスクリプトを書くかと思います。

TitleScene.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class TitleScene : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKey(KeyCode.M))
        {
            SceneManager.LoadScene("Main");
        }
    }
}

Mainシーンの方にも同じようなスクリプトを設置すると思います。

これで、とりあえずシーン遷移をすることはできるようになりました!

Commonシーンを共通で読み込むことになった

以下の図のように Common シーンを読み込むことになったとします。

image.png

Titleシーンが起動した際に、Commonシーンを読み込むようにすれば、できそうです。

以下のように Awake() を追加してみました。

TitleScene.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class TitleScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.LoadScene("Common", LoadSceneMode.Additive);
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.M))
        {
            SceneManager.LoadScene("Main");
        }
    }
}

LoadSceneMode.Additive を指定することで、シーン遷移をせずにCommonシーンを追加で読み込むことができます。

これと同じような変更をMainシーン側にもしてみます。

これによって、Titleシーン、Mainシーン、どちらに遷移してもCommonシーンを読み込むことができるようになりました!

Commonシーンを破棄しちゃダメ!って言われた場合

ここまでのコードだと、シーン遷移する度に、Commonシーンも破棄されています。

もし、Commonシーンで音楽の再生等をしていた場合は、シーン遷移するときに音楽も止まってしまいますね!

これだと困るので、以下のように、シーン遷移も LoadSceneMode.Additive でやってしまいましょう!

TitleScene.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class TitleScene : MonoBehaviour
{
    void Awake()
    {
        SceneManager.LoadScene("Common", LoadSceneMode.Additive);
    }

    void Update()
    {
        if (Input.GetKey(KeyCode.M))
        {
            SceneManager.LoadScene("Main", LoadSceneMode.Additive);
        }
    }
}

これと同じような変更をMainシーン側にもしてみます。

するとどうなるでしょうか?

実はこれ、非常に危険なコードです!

このままだと、シーン遷移する度に、Commonシーン・Mainシーン・Titleシーンの3つが重複して読み込まれてしまうんです!

しかも、性質の悪いことに指数関数的に累積していくので、あっという間にメモリがあふれます。

シーンがすでに読み込まれているか確認する

重複してシーンが読み込まれるのを防ぐためには、同じ名前のシーンがすでに読み込まれているか確認する必要があります。

以下のような静的クラスを作りましょう。

SceneController.cs
using UnityEngine.SceneManagement;

public static class SceneController
{
    /** 既にシーンが読み込まれているかどうか */
    public static bool AlreadyLoadScene(string name)
    {
        for (int i = 0; i < SceneManager.sceneCount; i++)
        {
            if (SceneManager.GetSceneAt(i).name == name)
            {
                return true;
            }
        }
        return false;
    }
}

SceneManager.sceneCount で、現在アクティブなシーンの数を数え、 SceneManager.GetSceneAt(i) でそのシーンのデータを取得しています。

すべてのシーンの name を確認していき、一致したものがあれば、そのシーンはすでに読み込まれているということになります。

この静的クラスを適当なフォルダーに設置した上で、さきほどの TitleScene.cs を以下のように改修しましょう。

TitleScene.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class TitleScene : MonoBehaviour
{
    void Awake()
    {
        if (!SceneController.AlreadyLoadScene("Common"))
        {
            SceneManager.LoadScene("Common", LoadSceneMode.Additive);
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.M))
        {
            if (SceneController.AlreadyLoadScene("Title"))
            {
                SceneManager.UnloadSceneAsync("Title");
            }
            SceneManager.LoadScene("Main", LoadSceneMode.Additive);
        }
    }
}

さきほど作った SceneController.AlreadyLoadScene() で、シーンがすでに読み込まれているかを確認して、それによって、 LoadSceneUnloadSceneAsync を呼ぶようにしました。

これで、メモリがあふれる心配はなくなりました!

LINQで改良してみる

せっかく作った SceneController.AlreadyLoadScene() ですが、 for 文を使ってる部分が、少し冗長な気がします。

LINQで改良したものがこちらです。

SceneController.cs
using UnityEngine.SceneManagement;
using System.Linq;

public static class SceneController
{
    /** 既にシーンが読み込まれているかどうか */
    public static bool AlreadyLoadScene(string name)
    {
        return SceneManager.GetAllScenes()
            .Any(scene => scene.name == name);
    }
}

解説をすると、まず、 SceneManager.GetAllScenes() で、アクティブなシーンの一覧を取得しています。

その次に、 .Any(scene => scene.name == name) で名前の一致するシーンが存在するかどうか確認しています。

コード量が減ってかなりスッキリしましたね!

さいごに

今回ご紹介した方法のほかにも、 DontDestroyOnLoad を使った方法等があると思います。

そのあたりは、ご自身の趣味趣向に合わせて、使いたいものを使うのがよいかと思います。

本記事作成にあたり、以下のページを参考にさせていただきました。ありがとうございました。

これは、 C# その2 Advent Calendar 2019 の5日目の記事でした。
明日は @s51517765 さんによる C#で複数のデータをreturnする です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity + C# でゲームを作るのは意外と簡単だというお話

本記事は、何か布教したいエンジニア AdventCalendar 3日目の記事です!!!
周りの人に声をかけまくってみたものの、既に2度目の私の出番が到来してしまいました。
僕は皆の頭の中を覗きたくてこのカレンダーを作ったんだ……!
お願いです!誰か記事を書いて!!!!

概要

ゲーム好きな人であれば、1度は「ゲームを作ってみたい」と思ったことがあるのではないでしょうか。
かく言う私もその1人です。
大昔は、プログラムで1から、描画やBGMを鳴らすといった機能まで作らなければならず、
ソフトウェア面、ハードウェア面、数学、物理の知識など、幅広く、深い知識が求められていたとかいないとか。
ですが、現在はUnityやUnreal Engineといったソフトウェア達に基本的な機能や処理は全て実装されていて、
自分はゲームの仕組み作りに熱中できるのです!
本記事では、自身がゲーム作りに挑戦した時の事を振り返りながら、
意外とゲームを作るのは簡単だぜっていう事を広められたらと思います。

1. 参考書を読む

最初は、いきなり作りたいものに取り掛かるのではなく基礎を固めようと参考書に手を出しました。
ネットにもチュートリアル記事は沢山ありますが、体系的にまとまっているものは多くありません。
今の時代、本のほうが逆にかかる労力が少ない…!と思って、以下の本を読みました(1勝2敗)

Unityの教科書 Unity 2018完全対応版

初心者向けに画像を多く、出来るだけ平易な言葉でゲーム開発について教えてくれます。
ゲーム開発に必要な素材もダウンロード出来ますし、手堅い一冊でしょう。
この本の特徴ではありませんが、この本の著者は、Unityの開発者を手助けするようなブログを書かれています。その内容が結構使えました。
まず、Unityを扱う上で必要な考え方の基礎は、しっかりとこの1冊でついたと思います。

Unityの教科書 Unity 2018完全対応版 2D&3Dスマートフォンゲーム入門講座

Unity&宴「ノベルゲーム」開発入門 (I・O BOOKS)

ノベルゲーム及び、ゲームの会話部分を自分で開発するのではなく、パッケージに任せてしまおうと思って、宴を購入しました。
紹介している参考書は、その宴の入門本です。
中身は、公式サイトで紹介されていることがそのまま載っているイメージです。
今まで開発をやったことが無い人間、例えばシナリオライターが宴を利用して文字起こしを担当するような事があれば有用かもしれません。

Unity&宴「ノベルゲーム」開発入門 (I・O BOOKS)

ゲーム作りのはじめかた Unityで覚える企画からレベルデザインまで

あまりにも内容が薄くて倒れるかと思った一冊。
本書のサブタイトルは、「Unityで覚える企画からレベルデザインまで」である。
つまり、ゲームの企画部分からゲームを開発して、その難易度調整までの一連の流れ全てを解説してくれる本という訳だ。
ゲーム開発を行っていく上で、どのように企画を練って実装に落としていくのか知りたかった私にはうってつけの本だと思って当時は購入した。
しかし、だ。本書は解説する部分が広すぎて、何もかも浅かった。
全てにおいて、「何故、どうしてそうするのか?」といった部分がおいてけぼりになっていて、言われるがままに「こうした方が面白そうですよね!」的な言葉で企画を作った気になり、「おまじないとして覚えてくださいね!」で全て流されているから、気付いたら言われたままのゲームが出来た。でも何故どうしてそうなったのかは全く分からない。といった状態に陥る。
小学生や中学生くらいの時点で、プログラムを利用してゲームを作りたい子が居れば有用かもしれない。
ただ、もし最初のステップとして本書を選ぼうとしているのであれば、RPGツクールなりなんなりを利用して、まずは形のあるゲームを完成させる所から始めたほうが良いと思う。

ゲーム作りのはじめかた

2. Unityの公式チュートリアルを行う

遠回りもしたものの、Unityの基礎は固まった。
ただ、今までの参考書では本当に簡単なゲームしか開発していなかったため、
実際に自分がゲームを開発していくビジョンが見えなかった。
その為、Unity公式が出しているチュートリアルに手を出しました。
今後開発していくにあたって、3Dゲームの場合は素材の入手が大変だろうということで2Dのチュートリアルです。
また、ゲーム開発においてシューティングゲームは入門に丁度いいと聞いたことがあったので、シューティングゲームを選びました。
残念な事に、Unity公式のアップデートによって私が触れたチュートリアルは無くなってしまったようです。
もし貴方がチュートリアルを行うのであれば、自分が漠然と開発してみたいと思っているゲームジャンルに近い物を選ぶと、オリジナルゲームを開発する時の大きな助けになるでしょう。

1つ開発に関するアドバイスをしておくと、作成したゲームをビルドすると文字が表示されない現象が発生します。
文字を出力するオブジェクトには、フリーでダウンロードしてきたフォントを選択することをオススメします。

3. 実際にゲームを作ってみる

実際に作成したゲームがこれです。
ブラザーを救え!

私はバーチャルYoutuberの富士葵さんが大好きで、またゲームを作ろうとしていた当時は彼女の活動1周年と丁度近い時期だったこともあり、彼女にまつわるゲームを作ろうと思い立ちました。

【富士葵】なんでもないや/RADWIMPS 『君の名は。』 【2018 Cover】

歌劇団と呼ばれる、同じく富士葵さんが好きな知り合いも多くいましたが、成功するかも分からない初めて作成するゲームに彼、彼女達を巻き込めないと思い、素材を始め全て自身で作成しました。
絵だけではなく、ゲームバランスも皆無の内容に今見ると笑ってしまいますが、当時は結構真剣でした。
敵ごとに移動パターンやライフ、射撃パターンを用意したり、ボスっぽい演出方法を考えたりなど、結構工夫を凝らすのは楽しかったですね。
チュートリアルで作成したのも、実際に作成したのもシューティングゲームだったこともあり、作品自体は1,2週間程で完成しました。

4. 制作してみて思ったこと

オブジェクトに動作を割り当てていく感じなので、感覚としてはクラスにメソッドを作成していっているような感じでした。
ただ動かすだけなら簡単ですが、ゲームにおいて何が面白い要素なのか考えることが面白かったと同時に非常に難しかったです。
また、目立たない、自分が当たり前だと思っているオブジェクトの動作が意外と難しいというか、実現できなかったりで、もどかしい。
ゲーム規模が大きくなってきたら、60FPSを保つために創意工夫したりしないといけないし、様々な能力が要求される世界なんだなぁと思いました。

5. まとめ

端的にまとめると、
1. 読みやすいUnityの入門書を1冊読みながら手を動かす
2. Unityのチュートリアルを1本やる。

上記の2STEPさえ踏めば、簡単なゲームは作れるようになってるハズ。

6. あとがき

シンプルなゲームを作成するだけであれば、少し本を読めば結構簡単に作成出来るんだ。というのが、一連の制作を通してみての感想でした。
ただ、世の中に出ているようなゲームを作成するためには、やっぱり非常に高度な技術が要求されるし、イラストやBGMなど自分一人で賄うのは非常に大変でしょう。
しかし、大変だと分かっていても、周りをなんとか巻き込んでもっとゲームを作ってみたいと私は思いました。
まだシンプルなゲームの制作を続けていて、あまり人も巻き込めていませんが、少しずつ前に進んでいきたいです。
本記事の情報を通して、僅かな人数でも手を動かしてみようと思って、その中のたった1人でもゲーム開発をしてみようと踏み出すことが出来たのであれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Autodesk Inventor API Hacking (EntitlementAPI)

0. はじめに

AppStoreに有料AddInを公開する場合に使える、購入したユーザーかどうかをAddInが判断する仕組みをAutodeskが提供しています。それをEntitlement APIと呼びます。
実際にcodeを書くにあたっての注意点をまとめます。

1. 大まかな流れ

権限確認は、次のフローで行います。

  1. 前提として、AppStoreに登録してApplication IDを取得しておく。
  2. CWebServicesManagerを使って、UserIdを取得する。
  3. Application IDとUserIdをAutodeskのServerに送る(REST)。
  4. 権限情報(json)が返ってくる。

2. Application IDの取得

このIDは、AppStoreに登録すると発行されます。多くの場合は、このIDをAddInにハードコードすることでしょう。
「登録するAddInに『登録すると発行されるID』をハードコードするなら、デッドロック(鶏と卵)じゃないか!」と思われた方は、正しいです。
厳密には、登録完了しなくても、下書き状態でIDが発行されるので、それを組み込んで提出してください。
AppStore.PNG
上図の赤枠で囲った部分が、Application IDです。ステータスが不完全でも発行されています。

3. CWebServicesManagerを使う

3.1 これは何をするもの?

Autodesk Accountのユーザー名は後から変更される可能性があるので、Autodesk内では一意のUserIdがAccountに対して割り当てられているようです。
AutodeskにServerに権限確認するには、このUserIdが必要なので、CWebServicesManagerを使って取得します。

3.2 CWebServicesManagerはどこにあるの?

<Inventorのinstall場所>\Bin\AddinNETFramework.AdWebServicesWrapper.dll

が実体です。

3.3 アセンブリが署名されていない!!

非常に残念なことに、AddinNETFramework.AdWebServicesWrapper.dllは署名されていません。ですので、AddIn自体に署名するならば、参照することが出来ません。
(署名されたアセンブリから、署名がないアセンブリを参照できない)

こういった場合は、次の2つの解決方法があります。
1. 自分で勝手に署名する。
2. 実行時にDLLを指定して動的Loadする。
(3. 開発元に署名してもらう)

もちろん3.がお勧めなのですが、現実的ではないので「2つの解決方法」としました。
ここでは、2.の方法を取ります。

3.4 アセンブリの動的Load

実行時にLoadすると、署名されてなくても警告もでないので、この方法で行きます。Load済みのDLLを再Load(重複Load)すると異常な挙動を取らないか心配でしたが、これは問題がありませんでした。
実際のcodeは以下の通りです。

/// <summary>
/// Autodesk User Idを取得します。
/// </summary>
/// <returns>取得に成功すれば、true。</returns>
private bool TryGetUserInformation()
{
    try
    {
        var cwsmAsm = System.Reflection.Assembly.LoadFrom(InventorApplication.InstallPath + @"\Bin\AddinNETFramework.AdWebServicesWrapper.dll");
        var typeInfo = cwsmAsm.GetType("Autodesk.WebServices.CWebServicesManager");
        using dynamic mgr = Activator.CreateInstance(typeInfo);
        bool isInitialized = mgr.Initialize();

        if (isInitialized)
        {
            InventorApplication.Login();
            string userId = "";
            mgr.GetUserId(ref userId);
            UserId = userId;
            string userName = "";
            mgr.GetLoginUserName(ref userName);
            UserName = userName;
        }
    }
    catch
    {
        return false;
    }

    if (!string.IsNullOrWhiteSpace(UserId))
    {
        return true;
    }
    else
    {
        return false;
    }
}

注意点として、Autodesk AccountにLoginしていない状態では、GetUserId()が空文字列を返すことです。ですので、本当に取得できたかどうかはUserIdの中身を確認する必要があります。

4. Autodeskの権限Serverとの通信

この内容は、RESTfulやjsonで検索すると優秀な情報があるでしょうから、最終codeを示しておきます。

GetEntitlement()
/// <summary>
/// Autodeskのサーバーに権限を問い合わせます。
/// </summary>
/// <returns></returns>
private async Task GetEntitlement()
{
    try
    {
        // 通信設定、データの下準備
        var parameters = new Dictionary<string, string>()
        {
            { "userid", UserId },
            { "appid", AppId },
        };
        var handler = new HttpClientHandler()
        {
            Proxy = WebRequest.GetSystemWebProxy(), // システムのProxyを使う
        };
        using var client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(timeOut), // TimeOut秒数
        };
        client.DefaultRequestHeaders.ConnectionClose = true;    // KeepAliveしない

        // 通信する
        var response = await client.GetAsync($"https://apps.autodesk.com/webservices/checkentitlement?{await new FormUrlEncodedContent(parameters).ReadAsStringAsync()}");
        var st = await response.Content.ReadAsStreamAsync();

        if (response.StatusCode != HttpStatusCode.OK)
        {
            SetResult(Result.HttpStatusCodeIsNotOk, $"{response.ReasonPhrase} ({(int)response.StatusCode})");
            return;
        }

        // 得られたjsonを解析する
        var serializer = new DataContractJsonSerializer(typeof(ServerResponse));
        var serverResponse = (ServerResponse)serializer.ReadObject(st);

        // 結果の判定
        if (serverResponse.IsValid != true)
        {
            SetResult(Result.EntitlementIsNotValid, serverResponse.IsValid.ToString());
        }
        else if (serverResponse.Message?.ToUpper() != "OK")
        {
            SetResult(Result.MessageIsNotOk, serverResponse.Message ?? "(null)");
        }
        else
        {
            SetResult(Result.Success);
        }
    }
    catch (HttpRequestException ex)
    {
        SetResult(Result.HttpRequestException, ex.Message);
    }
    catch (Exception ex)
    {
        SetResult(Result.ExceptionThrown, ex.ToString());
    }
    finally
    {
        IsGetEntitlementRunning = false;
    }
}
class ServerResponse
public class ServerResponse
{
    public string? UserId;
    public string? AppId;
    public bool IsValid;
    public string? Message;
}

SetResult()は結果を保存するための自作関数です。排他制御が必要だったため、直接メンバ変数を変更せずに、関数経由で結果を保存しています。
その他にも名前しか出てこない変数がありますが、適宜調整してください。

5. 参考資料

99. 親の記事に戻る

Autodesk Inventor API Hacking (概略)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Autodesk Inventor API Hacking (Entitlement API)

0. はじめに

AppStoreに有料AddInを公開する場合に使える、購入済みユーザーかどうかを確認する仕組みをAutodeskが提供しています。それをEntitlement APIと呼びます。
実際にcodeを書くにあたっての注意点をまとめます。

1. 大まかな流れ

権限確認は、次のフローで行います。

  1. 前提として、AppStoreに登録してApplication IDを取得しておく。
  2. CWebServicesManagerを使って、UserIdを取得する。
  3. Application IDとUserIdをAutodeskのServerに送る(REST)。
  4. 権限情報(json)が返ってくる。

2. Application IDの取得

このIDは、AppStoreに登録すると発行されます。多くの場合は、このIDをAddInにハードコードすることでしょう。
「登録するAddInに『登録すると発行されるID』をハードコードするなら、デッドロック(鶏と卵)じゃないか!」と思われた方は、正しいです。
厳密には、登録完了しなくても、下書き状態でIDが発行されるので、それを組み込んで提出してください。
AppStore.PNG
上図の赤枠で囲った部分が、Application IDです。ステータスが不完全でも発行されています。

3. CWebServicesManagerを使う

3.1 これは何をするもの?

Autodesk Accountのユーザー名は後から変更される可能性があるので、Autodesk内では一意のUserIdがAccountに対して割り当てられているようです。
AutodeskにServerに権限確認するには、このUserIdが必要なので、CWebServicesManagerを使って取得します。

3.2 CWebServicesManagerはどこにあるの?

<Inventorのinstall場所>\Bin\AddinNETFramework.AdWebServicesWrapper.dll

が実体です。

3.3 アセンブリが署名されていない!!

非常に残念なことに、AddinNETFramework.AdWebServicesWrapper.dllは署名されていません。ですので、AddIn自体に署名するならば、参照することが出来ません。
(署名されたアセンブリから、署名がないアセンブリを参照できない)

こういった場合は、次の2つの解決方法があります。
1. 自分で勝手に署名する。
2. 実行時にDLLを指定して動的Loadする。
(3. 開発元に署名してもらう)

もちろん3.がお勧めなのですが、現実的ではないので「2つの解決方法」としました。
ここでは、2.の方法を取ります。

3.4 アセンブリの動的Load

実行時にLoadすると、署名されてなくても警告もでないので、この方法で行きます。Load済みのDLLを再Load(重複Load)すると異常な挙動を取らないか心配でしたが、これは問題がありませんでした。
実際のcodeは以下の通りです。

/// <summary>
/// Autodesk User Idを取得します。
/// </summary>
/// <returns>取得に成功すれば、true。</returns>
private bool TryGetUserInformation()
{
    try
    {
        var cwsmAsm = System.Reflection.Assembly.LoadFrom(InventorApplication.InstallPath + @"\Bin\AddinNETFramework.AdWebServicesWrapper.dll");
        var typeInfo = cwsmAsm.GetType("Autodesk.WebServices.CWebServicesManager");
        using dynamic mgr = Activator.CreateInstance(typeInfo);
        bool isInitialized = mgr.Initialize();

        if (isInitialized)
        {
            InventorApplication.Login();
            string userId = "";
            mgr.GetUserId(ref userId);
            UserId = userId;
            string userName = "";
            mgr.GetLoginUserName(ref userName);
            UserName = userName;
        }
    }
    catch
    {
        return false;
    }

    if (!string.IsNullOrWhiteSpace(UserId))
    {
        return true;
    }
    else
    {
        return false;
    }
}

注意点として、Autodesk AccountにLoginしていない状態では、GetUserId()が空文字列を返すことです。ですので、本当に取得できたかどうかはUserIdの中身を確認する必要があります。

4. Autodeskの権限Serverとの通信

この内容は、RESTfulやjsonで検索すると優秀な情報があるでしょうから、最終codeを示しておきます。

GetEntitlement()
/// <summary>
/// Autodeskのサーバーに権限を問い合わせます。
/// </summary>
/// <returns></returns>
private async Task GetEntitlement()
{
    try
    {
        // 通信設定、データの下準備
        var parameters = new Dictionary<string, string>()
        {
            { "userid", UserId },
            { "appid", AppId },
        };
        var handler = new HttpClientHandler()
        {
            Proxy = WebRequest.GetSystemWebProxy(), // システムのProxyを使う
        };
        using var client = new HttpClient(handler)
        {
            Timeout = TimeSpan.FromSeconds(timeOut), // TimeOut秒数
        };
        client.DefaultRequestHeaders.ConnectionClose = true;    // KeepAliveしない

        // 通信する
        var response = await client.GetAsync($"https://apps.autodesk.com/webservices/checkentitlement?{await new FormUrlEncodedContent(parameters).ReadAsStringAsync()}");
        var st = await response.Content.ReadAsStreamAsync();

        if (response.StatusCode != HttpStatusCode.OK)
        {
            SetResult(Result.HttpStatusCodeIsNotOk, $"{response.ReasonPhrase} ({(int)response.StatusCode})");
            return;
        }

        // 得られたjsonを解析する
        var serializer = new DataContractJsonSerializer(typeof(ServerResponse));
        var serverResponse = (ServerResponse)serializer.ReadObject(st);

        // 結果の判定
        if (serverResponse.IsValid != true)
        {
            SetResult(Result.EntitlementIsNotValid, serverResponse.IsValid.ToString());
        }
        else if (serverResponse.Message?.ToUpper() != "OK")
        {
            SetResult(Result.MessageIsNotOk, serverResponse.Message ?? "(null)");
        }
        else
        {
            SetResult(Result.Success);
        }
    }
    catch (HttpRequestException ex)
    {
        SetResult(Result.HttpRequestException, ex.Message);
    }
    catch (Exception ex)
    {
        SetResult(Result.ExceptionThrown, ex.ToString());
    }
    finally
    {
        IsGetEntitlementRunning = false;
    }
}
class ServerResponse
public class ServerResponse
{
    public string? UserId;
    public string? AppId;
    public bool IsValid;
    public string? Message;
}

SetResult()は結果を保存するための自作関数です。排他制御が必要だったため、直接メンバ変数を変更せずに、関数経由で結果を保存しています。
その他にも名前しか出てこない変数がありますが、適宜調整してください。

5. 参考資料

99. 親の記事に戻る

Autodesk Inventor API Hacking (概略)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Exam Ref 70-483『C#でのプログラミング』の試験対策書籍の私的まとめ その2

はじめに

この記事は、Exam Ref 70-483『C#でのプログラミング』の試験対策書籍の私的まとめ その1の続編となります。

Ch2. Create and use types

学べるスキル

  1. 型の作成について(型宣言)
  2. 型の利用方法
  3. カプセル化について
  4. クラス階層の作成と実装方法
  5. プログラム実行による型探索と型の自動生成
  6. オブジェクトのライフサイクル管理
  7. 文字列の扱い

要約

  • ソリューション内のオブジェクトは、参照値によって管理できる。構造体structは値の型である。つまり、代入中に構造体の内容(特定の値)が1つの変数から別の変数にコピーされる。クラスclassは参照型である。つまり、割り当て中に、割り当てられた対象が割り当て先のソースと同じオブジェクトを参照する。
  • 不変オブジェクト(immutable objects)とは、名の通り変更できないオブジェクトである。DateTimeなどの不変オブジェクトは新たなオブジェクト(オリジナルの変異コピー、つまりオリジナルと異なる)を提供するために利用できるメソッドを提供する。たとえば、DateTime構造体は、指定された未来の日数を表す新しいDateTime構造体を返すために使用できるメソッドを提供する。
  • ジェネリック型は、型設計で「プレースホルダー」として使用できるため、型が機能するオブジェクトの型を動的に確立できる。ジェネリック型が使用される状況の良い例は、リスト型と辞書型の作成である。
  • 型は、その型の新しいインスタンスが作成されたときに呼び出されるコンストラクターメソッドを指定できる。コンストラクターには、オブジェクトの初期化に使用できるパラメーターを指定できる。コンストラクターが初期化データが不正であるという欠陥がある場合、コンストラクターが完了するとオブジェクトが作成されるため例外をスローして構築プロセスを中断する必要がある。コンストラクタは、指定された型をインスタンス化することができる様々な方法を提供するためにオーバーロードすることができる。this構文を使用すると、型の1つのコンストラクターが別のコンストラクターを呼び出して、マスターコンストラクターの作成が許可され、オブジェクトでのコードの重複を回避できる。クラスには、続くクラスの最初のインスタンスが作成されたときに一度呼び出される静的コンストラクターを含めることができる。
  • 型には、オブジェクトが動作を実行できるようにするメソッドを含めることができる。メソッドはいくつかのパラメーターを受け入れ、特定のタイプを持つ。void型のメソッドは値を返さないが、任意の型のメソッドはその型の結果を返す必要がある。
  • 拡張メソッドは、追加先のクラスの機能に追加できる。拡張メソッドは、追加されたクラスのプライベートデータメンバーにはアクセスできない。
  • メソッドのパラメーターは、呼び出されたときに名前で識別できる。 これにより、プログラムの可読性が向上し、パラメーターが間違った順序で入力されたためにエラーが発生する可能性も低くなる。
  • メソッドのパラメーターは、パラメーターのデフォルト値を指定することによりオプションにすることができる。 パラメータがメソッド呼び出しから除外されている場合は、代わりにデフォルト値が使用される。
  • オブジェクトは、返される項目を識別するためのインデックス値(通常は整数)を提供するために使用できるインデックス付きプロパティを提供できる。
  • 単一の型のメソッドはオーバーロードできる。 これは、型に同じメソッドの複数のバージョンが含まれ、それぞれが異なる署名を持つことを意味する。メソッドのオーバーロードは、異なるデータ入力から特定のタスクを実行する方法が複数ある状況で使用される。
  • クラス階層内のメソッドはオーバーライドできる。 オーバーライドするには、メソッドをvirtualとしてマークする必要がある(仮想メソッド)。階層内のクラスのインスタンスで仮想メソッドが呼び出されると、システムはクラスオブジェクトを指定して、クラスオブジェクトで述べられているメソッドの実装についてクラス階層を検索し、見つかった最初の実装が呼び出される。 メソッドのオーバーライドは、クラス階層内のオブジェクトが特定の方法で特定のアクションを実行できる必要がある場合に使用される。 baseキーワードを使用すると、メソッドはオーバーライドされたメソッドを呼び出すことができます。
  • アプリケーション内のすべてのアイテムは特定の型Typeである。C#の型Typeシステムは、参照型と値型の間を自動的に変換し、自動的に値を変換(拡大)する。このことでデータが失われない限り、この自動変換は賢明なことである。
  • 型変換によりデータが失われる(縮小する)場合、プログラマはキャストを使用して値の変換を明示的に要求する必要がある。
  • C#アプリケーションが静的に型付けされていない言語やサービスとやり取りできるように、動的dynamic型が提供されている。これにより、型付けの情報がエラーを示している場合でも、オブジェクトの型を無視してプログラムをビルドするようコンパイラーに指示できます。
  • 動的型が使用される場合、作成される変数の型は、使用されるもののコンテキストから推測される。これは、コンパイルエラーとして検出されたであろうエラーが、実行時(run-time)エラーに変更されることを意味する。
  • 動的型は、コンポーネントオブジェクトモデル(COM)ベースのサービスと対応させるときに特に便利である。動的型は、使用されるものの状況から正しい型に自動的にマッピングできるためである。
  • C#アクセス修飾子を使用すると、クラスのメンバーをクラス外のコードから隠すことができる。特に指定しない限り、クラスメンバーはクラスに対してプライベートprivateであり、クラスの外部からはアクセスできない。 データメンバーをパブリックpublicにすると、クラス内のそのデータメンバーへの制御されないアクセスが許可されるが、これは良い考えではない。
  • C#プロパティを使用すると、外部コードがクラスのメンバーとやり取りしたいときに、getおよびset動作により制御できる。プロパティは、クラスによって管理されているデータのバッキングフィールド値(a backing value)を保持するクラスのプライベートメンバーへのアクセスを制御できる。プロパティは読み取り専用プロパティのget動作、および書き込み専用のプロパティのset動作を提供することができる。 getおよびset動作には異なるアクセス修飾子を設定できるため、プロパティのget動作をパブリックに、set動作をプライベートにできる。
  • 一般的に、クラスが他のクラスで使用するために公開するメソッドはpublicする必要があり、クラス内に含まれるデータはprivateにする必要がある。
  • protectedアクセス修飾子により、クラス階層のクラス内でクラスメンバーを参照でき、internalアクセス修飾子により、クラスメンバーと同じアセンブリ内のコードからクラスメンバーを参照できる。
  • インターフェイスの動作を実装するクラスのメソッドは、このインターフェイスを明示的にexplicitly実装するものとして識別できる。これにより、カプセル化が改善される。これは、これらのメソッドを使用できるコンテキストが、オブジェクトへの参照ではなく、インターフェースへの参照を介することだけであることを意味する。Explicit実装は、クラスが複数のインターフェイスを実装し、一部のインターフェイスに同じメソッドが表示される場合の混乱の可能性も排除する。
  • C#クラスは、インターフェイスに記述されている各メソッドに一致するメソッドセットを含むインターフェイスを実装できる。インターフェイスを実装するクラスは、インターフェイス型の参照によって参照できる。
  • C#クラスは、それを拡張する子クラスの基底クラスとして機能することができる。子クラスは、基本クラスのすべてのメンバーを継承し、C#のoverrideメカニズムを使用して、子クラスにより固有の動作を持つ基本クラスのメソッドのオーバーライドバージョンを提供する。基底クラスのメソッドをオーバーライドするには、仮想virtualとして宣言する必要があります。 オーバーライドメソッドはbaseキーワードを使用して、基底クラスのオーバーライドされたメソッドを呼び出すことができます。
  • プログラマーは、基底クラスのメソッドの代替メソッドを作成できる。 これは推奨されない。
  • クラスを封印済みsealedとして宣言して、子クラスの基礎として使用されないようにすることができる。 基本クラスのメソッドを拡張するクラスのメソッドは、それらがオーバーライドされないように、sealedされていると宣言することもできる。
  • 子クラスを作成するには、プログラムで基底クラスのインスタンスを作成する必要がある。 基底クラスにコンストラクターがある場合、子クラスはこれを呼び出して必要なパラメーターを指定する必要がある。 baseキーワードは、この目的のためにコンストラクターで使用される。
  • クラスは抽象abstractクラスとして宣言できる。その場合、子クラスに実装されるメソッドの署名が含まれる。抽象クラスはクラスのテンプレートとしてする。
  • 基底クラスへの参照は、基底クラスを拡張するクラスから作成されたオブジェクトを参照できる。 ただし、その逆は当てはまらない。 子クラスへの参照を作成して、親のインスタンスを参照することはできない。
  • クラスはIComparableインターフェイスを実装できる。これは、その型のオブジェクトの順序を決定するために使用できるCompareToというメソッドが含まれていることを意味する。IComparable動作は、List型で提供されるSortメソッドなどのメソッドで使用できる。このメソッドはオブジェクトを順番に配置できる。
  • クラスは列挙のコンシューマー(foreach構造など)がクラスを処理するために使用できる列挙子を提供するメソッドGetEnumeratorを提供するIEnumerableインターフェイスを実装できる。
  • yieldキーワードは、プログラマーが列挙子を作成する簡単な方法を提供する。列挙のコンシューマーによって列挙から次のアイテムが要求される限り、列挙子メソッドの状態を返し続ける。
  • クラスはIDisposableインターフェイスを実装できる。 このクラスには、使用している重要なリソースを解放するようにクラスのインスタンスに指示するために呼び出すことができるDisposeメソッドが含まれます。Disposeメソッドは、ガベージコレクションプロセスによって自動的に呼び出されることはないが、using構造内でオブジェクトのインスタンスが作成および使用される場合、自動的に呼び出される。
  • IUnknownインターフェイスは、コンポーネントオブジェクトモデル(COM)オブジェクトと直接対応する必要がある.NETコードを作成するときに使用できる。
  • メタデータとは、本体であるデータに関する付帯情報が記載されたデータである。C#プログラムのコンテキストでは、コードのメタデータは、コード要素に関連付けられた1つ以上の属性インスタンスの形式で表現される。 このメタデータは、プログラムのビルド時に作成され、アセンブリのロード時に属性クラスインスタンスにロードされるアセンブリファイルに格納される。
  • 属性Attributeクラスは、属性クラスを拡張し、"Attribute"というテキストで終わる名前を持つ。属性クラスは空にすることができる。その場合、特定の属性がアイテムに適用されていることを示すだけであるか、属性にプログラムでアクセスできるデータ要素を含めることができる。属性は、プログラム内のさまざまな要素に適用できる。例えば、 AttributeUsage属性を属性宣言に追加して、属性を適用できるオブジェクトとクラスを指定することができる。
  • リフレクションReflectionを使用すると、プログラムは型の内容を調査し、プログラムで型の情報を反映させることができる。
  • リフレクションはアセンブリ内の型の特性を決定できるようにアセンブリ上で使用することができる。これを使用して、コンポーネント(構成要素)ベースのシステムが必要なコンポーネントを自動的に選択してロードできるようにすることが可能。
  • プログラムコードは、名前空間、クラス、プロパティ、およびクラスメンバを指定できるCodeDOMドキュメントモデルを使用して、プログラムで生成することができる。 CodeDOMオブジェクトは、バイナリアセンブリファイルまたはC#またはVisual Basicのソースファイルとして出力できます。
  • プログラムコードによる動作をプログラムにより生成する別の方法は、実行するアクションを提供するラムダ式と、他のプログラムアクションを指定する一連の式ノードタイプを含むラムダ式ツリーを作成することである。式ツリーは、LINQクエリや動的言語の実装などのプログラムの動作を表現するために使用される。
  • オブジェクトのガベージコレクションにより、プログラムの作成がはるかに簡単になる。 C#のガベージコレクションシステムは自動的に実行され、未使用のオブジェクト(参照によって参照されていないオブジェクト)を削除する。
  • ガベージコレクションの実行中、アプリケーションスレッドは一時停止する。
  • ガベージコレクターは永続オブジェクト(ガベージコレクションの後に存在するオブジェクト)を認識し、それらをガベージコレクションプロセスから除外する。
  • ガベージコレクションプロセスは手動で開始して使用できるが、これは推奨されない。
  • オブジェクトの削除時に解放する必要があるリソースへの参照を含む未使用のオブジェクトには、ガベージコレクションプロセス中に実行されるファイナライザメソッドを含めることができます。 ファイナライザメソッドは、オブジェクトに割り当てられたリソースを解放できる。
  • ファイナライザーメソッドよりも、オブジェクトはIDisposableインターフェイスを実装し、アプリケーションがリソースを解放するようにオブジェクトに指示するために使用できるDisposeメソッドを含めることができる。usingステートメントを使用して、プログラムの一部のみで使用されるオブジェクトでDisposeメソッドが呼び出されるようにすることができる。
  • usingによる破棄パターンを用いることで、オブジェクトはfinalizeとDisposeの使用を組み合わせて、オブジェクトが保持しているリソースが正しく解放されるようにすることができる。
  • 文字列stringは、任意の長さの文字charのコレクションである。
  • 文字列変数は不変である(変更できない)が、参照によって管理される。 この組み合わせは、値型として扱うことができることを意味します。
  • コンパイル中に、特定の文字列の文字列リテラルの複数のコピーがメモリ内の単一のインスタンスにマッピングされる。 つまり、プログラムが文字列リテラル「cheese」をいくつかの異なる文字列変数に割り当てると、すべての変数が参照するテキスト「cheese」を含む単一の文字列値が作成されるということである。
  • 多くの文字列を一緒に追加して大きな文字列を作成すると、多数の部分文字列が作成されることになる。StringBuilder型は、変更可能な文字列実装を提供する。プログラムは、StringBuilderインスタンスの内容を変更できる。 StringBuilderは、大きな文字列を組み立てるより効率的な手段を提供する。
  • StringBuilderクラスとStringWriterクラスを使用すると、TextReaderおよびTextWriterStreamで動作するプログラムで文字列を扱える。
  • 文字列型は、文字列を操作するためのさまざまなメソッドを提供する。 例えば、文字列内の部分文字列の場所を確立し、それらを抽出して、文字列の開始と終了を一致させることができる。
  • 文字列の内容は変更できない(文字列は不変である)が、文字列の置換メソッドを使用して、更新された内容で新しい文字列インスタンスを作成できる。
  • 文字列の一致は、文字コードを使用して、または同じ単語の複数のスペルが一致する特定の文化に従って実行できる。
  • 文字列は、たとえばforeach構造を使用して列挙できる。
  • 文字列はIFormattableインターフェイスを実装できる。つまり、文字列には、書式設定とカルチャ情報を受け入れる追加のToStringメソッドが含まれる。
  • 文字列補間において、プログラムは、文字列リテラル(先頭に$文字が付く)を利用できる。この文字列リテラルは、書式設定情報と、書式設定して文字列に組み込む変数名を組み合わせる。補間された文字列はコンパイル時に処理され、複合形式の文字列が生成される。

関連記事

参考書籍

『Exam Ref 70-483 Programming in C#, 2nd Edition』 By Rob Miles

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor向けのUIフレームワークのMatBlazorを使ってみる

概要

以前、Blazor向けのUIフレームワークとしてRadzen.Blazorを紹介しました。
(参照)

他にも主だった機能が揃ったUIフレームワークとしてMatBlazorがありましたので紹介したいと思います。

本記事のデモ
ソースコード

前提

.NET Core SDK 3.1.100-preview3-014645
Microsoft.AspNetCore.Blazor 3.1.0-preview2.19528.8
Visual Studio 2019
MatBlazor 1.10.1

導入手順

BlazorのWebAssembly App版をのプロジェクトを作成し、
NugetからMatBlazorをインストールします。
mat1.PNG

_Imports.razorに参照を追加します。
(@using MatBlazor)

Visual StudioのGUIを使わない場合、下記のいずれかのコマンドでも追加できるようです。
Install-Package MatBlazor
dotnet add package MatBlazor

コンポーネント紹介

何点かコンポーネントを紹介します。

Layout Grid

Bootstrapなどと同じようにカラム数を指定することでレスポンシブデザインを実現します。
1行のカラム数はデスクトップが12,タブレットが8,スマホが4です。
mat-layout-grid → mat-layout-grid-inner → mat-layout-grid-cell
といった形で入れ子で記載していきます。
Cellにはspanでカラムの占有数を指定します。
Cell内にinnerを追加することで入れ子でさらにカラムを分割することができます。

Grid.razor
<div class="mat-layout-grid">
    <div class="mat-layout-grid-inner">
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12 ">
            <h3 align="center">Grid</h3>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <h3 class="grid-sample">Span3</h3>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <h3 class="grid-sample">Span3</h3>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <h3 class="grid-sample">Span3</h3>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <h3 class="grid-sample">Span3</h3>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-8 grid-sample">
            <div class="mat-layout-grid-inner">
                <div class="mat-layout-grid-cell mat-layout-grid-cell-span-6 ">
                    <h3 class="grid-sample">Span6</h3>
                </div>
                <div class="mat-layout-grid-cell mat-layout-grid-cell-span-6 ">
                    <h3 class="grid-sample">Span6</h3>
                </div>
            </div>
        </div>
    </div>
</div>

上記では
- 幅3のカラムを4つ
- 幅8のカラム内に幅6のカラムを2つ
を配置しています。

表示イメージは下記のようになります。

grid.PNG

AppbarとMatMenu

Webサイトでよく見かけるアプリケーションメニューを実現できます。

appbar1.PNG

appbar20.png

Appbar.razor
<MatAppBarContainer>
    <MatAppBar Fixed="true">
        <MatAppBarRow>
            <MatAppBarSection>
                <MatIconButton Icon="menu" OnClick="@OnClickMenu" @ref="Button"></MatIconButton>
                <MatMenu @ref="Menu">
                    <MatList>
                        <MatListItem OnClick="@(e => OnSelectMenu("Item1"))">Item 1</MatListItem>
                        <MatListItem OnClick="@(e => OnSelectMenu("Item2"))">Item 2</MatListItem>
                        <MatListItem OnClick="@(e => OnSelectMenu("Item3"))">Item 3</MatListItem>
                        <MatListItem OnClick="@(e => OnSelectMenu("Item4"))">Item 4</MatListItem>
                        <MatListItem OnClick="@(e => OnSelectMenu("Item5"))">Item 5</MatListItem>
                    </MatList>
                </MatMenu>
                <MatAppBarTitle>AppBarTest</MatAppBarTitle>
            </MatAppBarSection>
            <MatAppBarSection Align="@MatAppBarSectionAlign.End">
                <MatIconButton Icon="favorite"></MatIconButton>
            </MatAppBarSection>
        </MatAppBarRow>
    </MatAppBar>

    <MatAppBarContent>
        @content
    </MatAppBarContent>
</MatAppBarContainer>

@code {
    MatIconButton Button;
    BaseMatMenu Menu;
    private string content = "content";

    public void OnClickMenu(MouseEventArgs e)
    {
        this.Menu.OpenAsync(Button.Ref);
    }

    public void OnSelectMenu(string content)
    {
        this.content = content;
    }
}

MatAppBarContainer → MatAppBar → MatAppBarRow(行) → MatAppBarSection(領域) → MatAppBarTitle(タイトル)
といった形で領域を作成します。
MatAppBarSectionにはAlignプロパティがあるので、複数のセクションを作り、左右それぞれにコンテンツを配置するといったことが可能です。
(ただ、私の環境だけか不明ですが、MatAppBarContainerを置くとレイアウトがうまく機能しない場合があったので外しています。外すしても今のところ不都合は生じていません。)

次にメニューの表示ですが、
- MatMenuに対してrefでインスタンスの参照を保持
- OpenAsyncメソッドを呼び出す
ことでドロップダウンのメニューが表示されます。

これをMatIconButtonでボタンクリックをき起点として呼び出すことで、メニューの表示を行います。

MatDialog

よく使うダイアログです。

dialog.PNG

Dialog.razor
<div class="mat-layout-grid">
    <div class="mat-layout-grid-inner">
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <MatButton OnClick="@(e => { okCancelDialogIsOpen = true; })">OKCancel Dialog</MatButton>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12 ">
            @result
        </div>
    </div>
</div>
<!-- ok cancel dialog -->
<MatDialog @bind-IsOpen="@okCancelDialogIsOpen" CanBeClosed="false">
    <MatDialogTitle>Title</MatDialogTitle>
    <MatDialogContent>
        <p>Message</p>
    </MatDialogContent>
    <MatDialogActions>
        <MatButton OnClick="@(e => { okCancelDialogIsOpen = false; result = "OK"; })">OK</MatButton>
        <MatButton OnClick="@(e => { okCancelDialogIsOpen = false; result = "Cancel"; })">Cancel</MatButton>
    </MatDialogActions>
</MatDialog>

@code {
    string result = string.Empty;
    bool okCancelDialogIsOpen = false;
}

ページの要素内にMatDialogのタグを作成して、 bind-IsOpenを切り替えることで表示と非表示を切り替えます。
それぞれ、タイトル(MatDialogTitle)、コンテンツ(MatDialogContent)、下部のボタン(MatDialogActions)のレイアウトを定義して使用します。
CanBeClosedpuプロパティで、画面外のクリック等で閉じられるかどうかを設定できます。

また、OK、Cancelのような定型的なダイアログなどは下記のようにコンポーネント化して使用することも可能です。

OKCandelDialog.razor
<MatDialog @bind-IsOpen="@IsDialogOpen" CanBeClosed="false">
    <MatDialogTitle>@Title</MatDialogTitle>
    <MatDialogContent>
        <p>@Message</p>
    </MatDialogContent>
    <MatDialogActions>
        <MatButton OnClick="@(e => { OnClick(true); })">OK</MatButton>
        <MatButton OnClick="@(e => { OnClick(false); })">Cancel</MatButton>
    </MatDialogActions>
</MatDialog>

@code {

    [Parameter]
    public bool IsDialogOpen { get; set; }
    [Parameter]
    public string Title { get; set; }
    [Parameter]
    public string Message { get; set; }
    [Parameter]
    public EventCallback<bool> ButtonAction { get; set; }

    void OnClick(bool result)
    {
        ButtonAction.InvokeAsync(result);
    }
}

コンポーネントを使用する側は下記のようになります。

OKCandelDialog.razor
<div class="mat-layout-grid">
    <div class="mat-layout-grid-inner">
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-3 ">
            <MatButton OnClick="@(e => { customDialogIsOpen = true; })">Custom Dialog</MatButton>
        </div>
        <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12 ">
            @result
        </div>
    </div>
</div>
<!-- component dialog -->
<OKCancelDialog IsDialogOpen="@customDialogIsOpen" Title="Test" Message="メッセージ"
                ButtonAction="@(e => { customDialogIsOpen = false;  result = e.ToString(); })" />
@code {
    string result = string.Empty;
    bool customDialogIsOpen = false;
}

MatTable

表用のコンポーネントです。

table.PNG

Table.razor
<MatTable Items="@PersonList" AllowSelection="true" Striped="true">
    <MatTableHeader>
        <th>Name</th>
        <th>Age</th>
        <th>Job</th>
    </MatTableHeader>
    <MatTableRow>
        <td>@context.Name</td>
        <td>@context.Age</td>
        <td>@context.Job</td>
    </MatTableRow>
</MatTable>

@code {
    class Person
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public string Job { get; set; }
    }

    List<Person> PersonList;

    protected override void OnInitialized()
    {
        PersonList = new List<Person>();
        PersonList.Add(new Person() { Name = "A", Age = 40, Job = "社長" });
        PersonList.Add(new Person() { Name = "B", Age = 23, Job = "NEET" });
        PersonList.Add(new Person() { Name = "C", Age = 34, Job = "エンジニア" });
        PersonList.Add(new Person() { Name = "D", Age = 50, Job = "隠居" });
    }
}

コレクションをItemsにバインドして、MatTableHeaderにヘッダ名、MatTableRowに表示したいプロパティを指定します。(@context.プロパティ)
.NET技術者だと、ついDataGrid的な使い方を期待してしまいますが、
AllowSelectionプロパティで行選択は可能ですが、残念ながら選択した要素の取得方法が見つかりませんでした。

MatFAB

最近のWEBサイトでよく見かける、フローティングのアクションボタンを追加できます。

fab.PNG

Fab.razor
<style>
    .app-fab--absolute {
        position: fixed;
        bottom: 1rem;
        right: 1rem;
    }
</style>
<div class="mat-layout-grid">
    // 略
    <MatFAB Class="app-fab--absolute" Icon="@Icon" Label="@Label"></MatFAB>
</div>

@code {
    string Icon = MatIconNames.Favorite;
  //String.Emptyだと余計なスペースが入る
    string Label = null;
}

画面の右下に表示する方法はプロパティではなく、スタイルシートで指定します。
(razor内にも記載可能。今回はapp-fab--absolute)

Labelプロパティで文字も表示可能ですが、String.Emptyだと余計なスペースが入るので、動的に表示を切り替えたい場合はnullを設定しましょう。

IconプロパティにはSPAのUIフレームワークではおなじみのMaterial Iconが指定可能です。
MatIconNamesで定義されているアイコンのイメージは下記で確認できます。
https://samprof.github.io/MatBlazor/Icon

よく使うケースだと、画面がスクロールされた場合にボタンを表示といった使い方をすると思いますが、Javascriptと相互連携しないと難しそうなので、近いうちにまた別途調べて紹介できればと思います。
(JSがそのまま使えないのが、VueやReactなどのSPAフレームワークと違って歯がゆいところです。)

その他のコンポーネント

TextBox、CheckBox、DatePicker、RadioButton、Selectなどよく使う入力コンポーネントは一通り揃っています。
詳細は公式サイトを参照ください。
https://samprof.github.io/MatBlazor/

まとめ

Blazor向けのUIフレームワークとしてMatBlazorの紹介をしました。
Radzen.Blazorと比較するとコンポーネントの数は少ないですが、個々の機能に関してドキュメントが揃っているので使いやすいと感じました。
現在も絶賛開発中ですので今後にも期待したいですね。

参考

MatBlazor公式サイト

本記事のデモ
ソースコード

Blazorのその他の投稿記事

何点かBlazorに関して記事を書いていますので、良ければ見てみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

uGUIから読み解くdelegate

uGUIから読み解くdelegate

UnityでButtonのクリック時のコールバックメソッドをスクリプトから定義する時

button.onClick.AddListener(callbackMethod);

のような感じで書くと思います。調べると今まで曖昧だったデリゲートと関連することが分かったので、調べたことを記事にしたいと思います。

記事を書こうと思ったら、すでに模範解答のような記事を見つけました。
良記事なので、そちらを参照してみても良いかもしれません。

学び後の理想の状態

  • 「Delegateって何ですか?」と聞かれた時に、しっかりと答えることができる
  • UnityActionとは何かについて理解している

現状

  • そもそもDelegateとは何かよく分かっていない
  • uGUIのコールバックメソッドのスクリプトからの登録の仕方は分かっているけど、本当のところどういう仕組みで動いているのか分かっていない。

AddListener()とは

AddListenerはUnityEventクラスのパブリックメソッドで

public void AddListener(Event.UnityAction call)

のように定義されています。ランタイムコールバックの追加をこの関数はしています。

注目すべきは引数にUnityAction型のコールバックメソッドを指定しているところです。
一体UnityActionとは何なのでしょうか。

UnityActionとは

ではUnityActionの定義を見てみましょう。

public delegate void UnityAction();

となっています。つまり戻り値、引数なしのdelegateにすぎないと分かります。
UnityActionを理解するにはデリゲートについて理解していないとだめですね。

デリゲートとは

デリゲートの定義

まずは言葉の定義から。Microsoft公式ドキュメントより

デリゲートは、特定のパラメーター リストおよび戻り値の型を使用して、メソッドへの参照を表す型です。

デリゲート自体はあくまでメソッドへの参照を表す型。
1つ簡単な例をあげておきます。

簡単な例
using System;

namespace delegateTest
{
    class Program
    {
        // 1. まずはデリゲートを定義します。(デリゲートはあくまでメソッドへの参照を表す型です)
        public delegate void SimpleCalc(int a, int b);

        // SimpleCalcデリゲートと同じ戻り値の型とパラメーターリストであること関数を用意
        static void Add(int a, int b) 
        {
            Console.WriteLine($"{a} + {b} = {a + b}");
        }
        static void Subtract(int a, int b) 
        {
            Console.WriteLine($"{a} - {b} = {a - b}");
        }

        static void Main(string[] args)
        {
            // 2. デリゲートをインスタンス化。同じ戻り値の型、パラメーターリストを持つ関数を代入
            SimpleCalc del_calc = Add; 
            // 3. デリゲートインスタンスを通じて関数(この場合Add)を呼び出す
            del_calc(20, 10); 

            // 2, 3繰り返す
            del_calc = Subtract;
            del_calc(20, 10);
        }
    }
}
結果
20 + 10 = 30
20 - 10 = 10

定義したデリゲートをインスタンス化する時、同じ戻り値の型、引数リストを持つ関数を参照することができ、参照された関数はそのデリゲートインスタンスを通して呼び出されるということですね。他にもデリゲートはクラスメソッド、インスタンスメソッドのどちらも参照することができます。
また便利な機能として、マルチキャストデリゲートと言って複数のメソッドを代入することができます。

デリゲートの使い所

どんな状況でデリゲートの力が発揮されるのか分かれば、デリゲートへの理解も深まると思います。(実際、僕自身デリゲートをわざわざ使う理由が中々分かりませんでした。)

「++C++; // 未確認飛行 C」のデリゲートの利用例を参考にすると、

  • 述語(条件式を外から挿す)
  • コールバック(非同期処理の終了通知)
  • イベント処理

などが主な利用例だそうです。今回理解したいUnityActionがイベント処理に用いられているのもここからわかりますね。後のサンプルを見ると、よりデリゲートのイメージが湧くかもしれません。

結局デリゲートとは

デリゲートを引数や戻り値として用いることで、デリゲートを通して関数をもっと自由に扱えるようになる(関数を呼ぶタイミングを調整できたり、実行する関数を状況に応じて変更できたり)ことがデリゲートを使う利点なのではないでしょうか。

サンプル

サンプルとして、ボタンを押したらwebから画像を引っ張ってきて、それをシーン上のimageUIに貼り付けるということをしてみます。

シーンにimageとbuttonを用意します。
スクリーンショット 2019-12-02 0.03.24.png

canvasに以下のUIController.csというスクリプトを貼り付けます。

UIController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Networking;
public class UIController : MonoBehaviour
{
    [SerializeField] string imagePath;
    public Button button;
    public Image image;

    private void Start() 
    {
        button.onClick.AddListener(OnButtonClicked);
    }

    private void OnButtonClicked() 
    {
        StartCoroutine(LoadImage(imagePath, DisplayImage));
    }

    private void DisplayImage(Texture2D tex2D) 
    {
         Rect rect = new Rect(0f, 0f, tex2D.width, tex2D.height);
         image.sprite = Sprite.Create(tex2D, rect, Vector2.zero);
    }

    public delegate void ImageProcessing(Texture2D texture);
    IEnumerator LoadImage(string _imagePath, ImageProcessing _callback) 
    {
        UnityWebRequest request = UnityWebRequest.Get(_imagePath);
        yield return request.SendWebRequest();

        if (request.isNetworkError) 
        {
            Debug.LogError(request.error);
        }
        else 
        {   
            Texture2D tex2D = new Texture2D(400, 200);
            tex2D.LoadImage(request.downloadHandler.data);

            _callback(tex2D);
        }
    }
}

まずは、

button.onClick.AddListener(OnButtonClicked);

UnityAction型のデリゲートにコールバック関数としてOnButtonClickedを渡す。
そうすることでボタンがクリックされた時に、OnButtonClickedが呼ばれることになります。

結果
スクリーンショット 2019-12-02 0.57.06.png

特に解説するようなことは何も書いていないのですが、LoadImage関数の引数を見ると、ここでも自分で定義したデリゲートを使用しています。

// public delegate void ImageProcessing(Texture2D texture)
IEnumerator LoadImage(string _imagePath, ImageProcessing _callback) 

第二引数のコールバック関数には、戻り値の型と引数リストさえ合っていれば良いので、

private void Save(Texture2D texture) 
{
    File.WriteAllBytes(Application.dataPath + "/savedImage.png", texture.EncodeToPNG());
}

のような画像を保存するような関数を渡したりもできますね。

少し発展

AddListenerで登録したOnButtonClicked()関数なのですが、

private void OnButtonClicked() 
{
    StartCoroutine(LoadImage(imagePath, DisplayImage));
}

わざわざコルーチンを実行するためだけに関数を定義したくないというのが本音です。

そこで匿名関数(匿名メソッドとラムダ式があるが、今回はラムダ式)を用いると、インラインで処理内容を記述できるようになります。

button.onClick.AddListener( () => StartCoroutine(LoadImage(imagePath, DisplayImage)) );

匿名関数、ラムダ式に関しては【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】を参照してくだされば分かるかと。

また、LoadImage関数でも

// public delegate void ImageProcessing(Texture2D texture);
IEnumerator LoadImage(string _imagePath, ImageProcessing _callback) 

と引数にImageProcessingというデリゲートを指定していますが、このためだけにデリゲートを作るのもあまり格好が良いとは言えませんし、そもそもデリゲートって型なので名前をつけるのちょっと難しいんですよね。今回それを解決してくれるのが、 Action<T>型のデリゲートです。(Func, Action系のデリゲートに関しても先ほどのリンクをみていただけるとわかります。)

Action<T>型を用いて変更すると、

using System;

// デリゲートをわざわざ定義しなくて良い
IEnumerator LoadImage(string _imagePath, Action<Texture2D> _callback) 
{
}

これまたスッキリ。

まとめ

今回はbuttonを扱いましたが、sliderとかでも基本一緒だと思います。
デリゲートはまだまだ奥が深く勉強しがいがありそうです。以下のことを今後調べたいと思います。

  • Func, Action系デリゲート
  • 匿名関数
  • LINQ

Qiita書いたのほぼ初めてだったのですが、学んだことを言語化して記事にするのって中々大変なのだなと感じました。
いつも読むばかりだったので、これからは継続的に学んだことのアウトプットとして記事を書いていけたらなと思います。

最後の方は、体力切れてしまったので、少しあやふやな部分が多いです。もし誤り等あればご指摘お願いします。また、以下の観点からコメント頂けると嬉しく思います!

  1. 「デリゲートの使い方、こんなのあるよ!」
  2. 「デリゲート周りだと、〜とか調べてみたら面白いよ!」

参考資料

以下今回調べるに当たってお世話になった参考資料です。
ありがとうございました。インターネット万歳。

(MSDN)
Delegate Class
Delegate(C# Programming Guide)
MulticastDelegate Class
(++C++; // 未確認飛行 C)
デリゲート
デリゲートの利用例
デリゲートの内部
(Qiita)
【LINQの前に】ラムダ式?デリゲート?Func?な人へのまとめ【知ってほしい】
(Unity Documentation)
Unity Action
Button.onClick
UnityEvent.AddListener

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CleanArchitectureでひとつ『上』 のコードを目指す:テスト編

前回の記事(実装編)からの続きになります。
Unity(C#)で私なりにCleanArchitectureの実装例を説明しました。

CleanArchitectureのルールに従って、アプリケーションロジックからフレームワークを抽象化して切り離すことで、特定のフレームワークに依存しないアプリケーションロジックを実装することが出来ます。
つまり、フレームワークが無くても(≒決まってなくても)アプリケーションロジックのみを実装することが出来、フレームワークの都合を抜きにしてテストすることが出来る訳です。

この記事ではUnityのシーンに実装したアプリケーションロジックが正しい仕様で動いているかをTest RunnerのPlay Modeテストする方法を解説します。

  • Play Modeでのテスト実行がシーンに反映されている様子です。 bmi.gif

GitHub:naninunenoy/UnityViewPatterns/BMIApp

おさらい

UIの実装を View に落とし仕込み、アプリケーションロジック( UseCase )からは View を直接参照させるのではなく、Presenter という中間層を定義し、それを介してUIの操作(ボタンのイベント受信やテキストの変更など)を行っていました。

image.png

同様にデータの入出力(保存/読み来み)やログイン処理などでも UseCase からの利用を中間層を介してやることで、フレームワーク(詳細)にとらわれないアプリケーションロジックの実行が可能になります。これにより、クライアント側で一時的なデータ保存の実装を用意してやれば

「データを保存するバックエンドが用意できていないからクライアント側の実装が進められない」

という状況にも対応できますし、テスト用のログイン実装を用意すれば

「ログイン画面のテストが通信状況の良し悪しで結果が変わってしまう」

いった問題に対応できます。

DI

※DI(dependency injection): 依存性の注入

肝になる考え方は、CleanArchitectureによってアプリケーションロジックである UseCase が詳細(UIやデータ保存や認証の方法)とは無関係でいられるので、製品コードとテストコードとの実行でそれら(詳細の実装)を切り替えてもアプリケーションロジックは問題ないということです。
本来のクラスの実装では内部変数やイベントが隠蔽されているので操作できない(良いことです!!)ところを、テスト用のクラスではそれらを外側(テスト実行のコード)から自由に操作できるようにし、操作した結果が画面やデータに反映されているかをテストすればOKという訳です。

または、実装が特定のフレームワークに依存してしまっているので、依存せずテストに都合のいいクラスをテスト用に用意するなどの選択肢があります(こっちが本来の恩恵かも)。

このために、実際とテストとの実行で UseCase にinterfaceで渡される中間層のクラスをDIで切り替える必要があります。

Zenject

Zenject(または Extenject)はDIのフレームワークですがテストによる実行もカバーしており、自動テストのための解説も載ってます。 自分でGoogle翻訳したやつ

その中に SceneTestFixtureというものがあります。本来はシーンのロードがエラー(例外)なく行えるかをテストするもののようですが、こいつでPlayModeテスト用のDIを行ってシーンを実行できないかを試してみました。

(結果的に実現できましたが、前提として準備しておくことが多く、既存のプロジェクトに後の載せで行うにはかなり厳しいと思います汗)

Main と Installer

実装の前提ですが、UseCase に渡す Presenter などは Installer で準備します。そして、準備された Presenter などの中間層の要素を Main で受けとって UseCase を作成/実行します。

BMISceneMain.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Zenject;
using BMIApp.CleanArchitecture;

namespace BMIApp.BMI {
    public class BMISceneMain : MonoBehaviour, ISceneMain {
        IUseCase bmiUseCase;
        IUseCase historyUseCase;
        IUseCase logoutUseCase;

        // Injectメソッドで準備された中間要素を受けっとってUseCaseを生成
        [Inject]
        void ConstructUseCases(IHistoryListPresenter historyListPresenter,
                               IBMIHistoryRepository historyRepository,
                               IBMIPresenter bmiPresenter,
                               IUserAccountRepository userAccountRepository,
                               IAccountPresenter accountPresenter) {
            historyUseCase = new HistoryUseCase(
                historyListPresenter, 
                historyRepository, 
                this);
            bmiUseCase = new BMIUseCase<BMIDataTransferObject>(
                bmiPresenter,
                historyUseCase as IPushHistoryDelegate,
                this);
            logoutUseCase = new LogoutUseCase(
                userAccountRepository,
                accountPresenter,
                this);
        }

        void Awake() {
            // run UseCase
            bmiUseCase.Begin();
            historyUseCase.Begin();
            logoutUseCase.Begin();
        }
    }
}

Installer はZenjectの MonoInstaller を継承したものであり、実際にアプリケーション実行のためのDIを行うものになります。こいつは中間層も詳細も両方知っておいてよい存在になります。[SerializeField] などでUnityの要素を受け取るのもこいつに集約させると良いでしょう。

BMISceneInstaller.cs
using UnityEngine;
using BMIApp.CleanArchitecture;

namespace BMIApp.BMI {
    // MainInstallerBaseは後で説明します
    public class BMISceneInstaller : MainInstallerBase {
        // inspectorからアタッチする
        [SerializeField] SharedScriptableObject sharedData = default;
        [SerializeField] BMIView bmiView = default;
        [SerializeField] HistoryView historyView = default;
        [SerializeField] HistoryElmView historyElmView = default;
        [SerializeField] AccountView accountView = default;

        // シーンの最初に呼ばれる。DIを行う。
        public override void InstallBindings() {
            base.InstallBindings();
            var dataStore = new PlayerPrefsHistoryDataStore(sharedData.CurrentUserId) 
                as IHistoryDataStore;
            Container
                .Bind<IHistoryListPresenter>()
                .FromInstance(new HistoryListPresenter(historyView, historyElmView))
                .AsCached()
                .IfNotBound();
            Container
                .Bind<IBMIHistoryRepository>()
                .FromInstance(new BMIHistoryRepository(dataStore))
                .AsCached()
                .IfNotBound();
            Container
                .Bind<IBMIPresenter>()
                .FromInstance(new BMIPresenter(bmiView))
                .AsCached()
                .IfNotBound();
            Container
                .Bind<IUserAccountRepository>()
                .FromInstance(new UserAccountRepository(sharedData))
                .AsCached()
                .IfNotBound();
            Container
                .Bind<IAccountPresenter>()
                .FromInstance(new AccountPresenter(accountView))
                .AsCached()
                .IfNotBound();
        }
    }
}

Installer でDIされた実装が Main に渡り UseCase の材料になって実行されるわけです。

テストでの実装

Installer でDIされた実装が Main に渡り UseCase の材料になって実行されるわけです。

つまり、テストではテスト用のDIを事前に行った状態でPlayModeテストでシーンを読み込めばテストのためのシーン実行が出来る訳です。

BMISceneTest.cs
using System.Collections;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
using UnityEditor;
using Zenject;
using BMIApp.CleanArchitecture;
using BMIApp.BMI;

namespace BMIApp.Tests.PlayMode {
    public class BMISceneTest : SceneTestFixture {
        const string sceneName = "BMI";

        BMITestPresenter bmiPresenter = new BMITestPresenter();
        HistoryListTestPresenter historyPresenter = new HistoryListTestPresenter();
        AccountTestPresenter accountPresenter = new AccountTestPresenter();
        UserAccountTestRepository accountRepository = new UserAccountTestRepository();
        BMIHistoryTestRepository historyRepository = new BMIHistoryTestRepository();

        SharedScriptableObject sharedData = default;
        TemporaryHistoryDataStore historyData = default;

        BMIView bmiView = default;
        HistoryView historyView = default;
        HistoryElmView historyElmView = default;
        AccountView accountView = default;

        void CommonInstallBindings() {
            StaticContext.Container
                .Bind<ITest>().To<Test>()
                .AsTransient();
            StaticContext.Container
                .Bind<IBMIPresenter>().FromInstance(bmiPresenter)
                .AsTransient();
            StaticContext.Container
                .Bind<IHistoryListPresenter>().FromInstance(historyPresenter)
                .AsTransient();
            StaticContext.Container
                .Bind<IAccountPresenter>().FromInstance(accountPresenter)
                .AsTransient();
            StaticContext.Container
                .Bind<IUserAccountRepository>().FromInstance(accountRepository)
                .AsTransient();
            StaticContext.Container
                .Bind<IBMIHistoryRepository>().FromInstance(historyRepository)
                .AsTransient();
        }

        void FindGameObjects() {
            // find
            var canvas = GameObject.Find("Canvas").transform;
            bmiView = canvas.Find("BMIView").GetComponent<BMIView>();
            historyView = canvas.Find("HistoryView").GetComponent<HistoryView>();
            accountView = canvas.Find("AccountView").GetComponent<AccountView>();
            // prefab
            var prefab = AssetDatabase.
                LoadAssetAtPath<GameObject>("Assets/BMIApp/Prefabs/HistoryElm.prefab");
            var historyElm = prefab.GetComponent<HistoryElmView>();
            // data
            sharedData = ScriptableObject.CreateInstance<SharedScriptableObject>();
            historyData = new TemporaryHistoryDataStore();
            // set
            bmiPresenter.InnerPresenter = new BMIPresenter(bmiView);
            historyPresenter.InnerPresenter = 
                new HistoryListPresenter(historyView, historyElm);
            accountPresenter.InnerPresenter = new AccountPresenter(accountView);
            accountRepository.InnerRepository = new UserAccountRepository(sharedData);
            historyRepository.InnerRepository = new BMIHistoryRepository(historyData);
        }

        void BeginMain() {
            GameObject.Find("SceneContext")
                .GetComponent<IMainInstaller>().SceneMainObject.SetActive(true);
        }

        [UnityTest]
        public IEnumerator BMI計算_保存_削除までの一連の操作() {

            CommonInstallBindings();
            yield return LoadScene(sceneName);
            FindGameObjects();
            BeginMain();

            // 最初は未入力
            Assert.IsEmpty(bmiView.NameInput.text);
            Assert.IsEmpty(bmiView.HeightInput.text);
            Assert.IsEmpty(bmiView.WeightInput.text);
            Assert.IsEmpty(bmiView.AgeInput.text);
            Assert.IsFalse(bmiView.GenderMaleToggle.isOn);
            Assert.IsFalse(bmiView.GenderFemaleToggle.isOn);
            Assert.That(bmiView.BMIText.text, Is.EqualTo("99(やせすぎ)"));
            Assert.IsFalse(bmiView.SaveButton.interactable);
            Assert.That(historyView.Content.childCount, Is.Zero);

            // 名前/身長/体重を入力すると[保存]が押せるようになる
            bmiView.NameInput.onEndEdit.Invoke("test_name");
            Assert.IsFalse(bmiView.SaveButton.interactable);
            bmiView.HeightInput.onEndEdit.Invoke("123");
            Assert.IsFalse(bmiView.SaveButton.interactable);
            bmiView.WeightInput.onEndEdit.Invoke("56");
            Assert.IsTrue(bmiView.SaveButton.interactable);

            // 計算されたBMIと評価が表示される
            Assert.That(bmiView.BMIText.text, Is.EqualTo("37.0(肥満)"));

            // [保存]を押すとリストに追加される
            bmiView.SaveButton.onClick.Invoke();
            yield return null;
            Assert.That(historyView.Content.childCount, Is.EqualTo(1));

            // 内容が 日時-名前-BMI
            var elm = historyView.Content.GetChild(0)?.GetComponent<HistoryElmView>();
            Assert.IsFalse(elm == null);
            Assert.That(elm.DateText.text, 
                Is.EqualTo(System.DateTime.Now.ToString("M/d")));
            Assert.That(elm.NameText.text, Is.EqualTo("test_name"));
            Assert.That(elm.BMIText.text, Is.EqualTo("37.0"));

            // 後から追加された方が上にくる
            bmiView.HeightInput.onEndEdit.Invoke("100");
            bmiView.WeightInput.onEndEdit.Invoke("1");
            bmiView.SaveButton.onClick.Invoke();
            yield return null;
            elm = historyView.Content.GetChild(0)?.GetComponent<HistoryElmView>();
            Assert.IsFalse(elm == null);
            Assert.That(elm.BMIText.text, Is.EqualTo("1.0"));

            // リポジトリにも追加されている
            Assert.That(historyData.Datas.Count, Is.EqualTo(2));

            // [クリア]でデータが消える
            historyView.ClearButton.onClick.Invoke();
            yield return null;
            Assert.That(historyView.Content.childCount, Is.Zero);
            Assert.That(historyData.Datas.Count, Is.Zero);

            yield return null;
        }
    }
}

さらっと(?)書いていますが、テスト実行のために解決すべきポイントがいくつかあったので解説します。

問題1

シーンがロードとされる前にDIするので SceneContext がまだ存在しないためBindできなかった

対応

StaticContext に設定できます。
しかし、StaticContext のBindよりも InstallerSceneContext に改めてBindされるものの方が優先されてしまうので、Installer でのBind全てに .IfNotBound() を設定します。

問題2

シーンがロードとされる前にDIしたい訳ですが、Presenter のコンストラクタには IView が必要であり View は シーン上のGameObject なのでシーンがロードするまで取得できない(テスト用に事前にDIするPresenter が生成できなかった)

対応

IPresenter を実装した TestPresenter を定義しました。

BMITestPresenter.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UniRx;
using BMIApp.BMI;

namespace BMIApp.Tests.PlayMode {
    public class BMITestPresenter : IBMIPresenter {
        public BMIPresenter InnerPresenter { set; get; }
        public IReadOnlyReactiveProperty<string> NameInput => InnerPresenter.NameInput;
        public IReadOnlyReactiveProperty<string> HeightInput => InnerPresenter.HeightInput;
        public IReadOnlyReactiveProperty<string> WeightInput => InnerPresenter.WeightInput;
        public IReadOnlyReactiveProperty<string> AgeInput => InnerPresenter.AgeInput;
        public IReadOnlyReactiveProperty<bool> GenderMaleSelect => InnerPresenter.GenderMaleSelect;
        public IReadOnlyReactiveProperty<bool> GenderFemaleSelect => InnerPresenter.GenderFemaleSelect;
        public IObservable<Unit> SaveButtonClickObservable => InnerPresenter.SaveButtonClickObservable;
        public void Begin() => InnerPresenter.Begin();
        public void SetBMIResult(string result) => InnerPresenter.SetBMIResult(result);
        public void SetSaveButtonEnable(bool enable) => InnerPresenter.SetSaveButtonEnable(enable);
    }
}

こいつはコンストラクタに IView を持たず、シーンがロードされてから BMIViewGameObject.Find() などで見つけてきて改めて BMIPresenter を生成し、.InnerPresetner に後で設定することが出来ます。動作は本来の BMIPresenter と同じ振る舞いをします。

問題3

IUseCase.Begin()Main.Awake() に書かれているので、シーンロード直後に問答無用で実行されるため上のように .InnerPresenter を設定する暇がなかった(設定しても実行された後なので無意味だった)

対応

Main.Awake() のを任意のタイミングで実行するためにトリックを仕込みます。
まず、Installer全てを MainInstallerBase が親になるように継承させ、その中でテスト実行かを判別し MainGameObject を非活性にするようにします。

MainInstallerBase.cs
using UnityEngine;
using Zenject;

namespace BMIApp.CleanArchitecture {
    public abstract class MainInstallerBase : MonoInstaller, IMainInstaller {
        [SerializeField] GameObject main = default;
        public GameObject SceneMainObject => main;

        public override void InstallBindings() {
            // Bindの内容でテストによる実行かを判断し、
            // テストの場合はmainをここで非活性化し、
            // テストから任意のタイミングでAwakeを呼べるようにする
            if (Container.HasBinding<ITest>()) {
                main.SetActive(false);
            }
        }
    }
}

非活性( gameObject.activeSelf==false )な GameObjectAwake() が実行されず、活性化したタイミングで Awake() が実行されるという Unity の仕様があります。Zenjectも内部的にこの仕組みを利用しているらしいです。1

そして、テストコードで Main を活性化すれば任意のタイミングで IUseCase.Begin() を呼ぶことが出来ます。BeginMain() がそれです。

あとは View であるuGUIをコードから任意に操作したり、シーンをロードする前に DataStore に任意の設定しておくなどして想定通りの挙動になっているかを Assert でチェックしていって下さい。

まとめ

テストコードでの流れは
1. テスト用のDIを StaticContext.Container に行う
2. 目的のシーンを読み込む
3. シーンからテストに必要な要素( View など)を取り出して準備をする
4. 前述のトリックを解除し、UseCase を実行する
5. 挙動が想定通りかどうかをテスト

になります。

テストの実現方法の説明にスペースを割きましたが、ここまで準備すれば外部に依存しているクラスを別のテスト用のクラスに入れ替えて、にUnity内の実装だけをテストすることが出来ます。

「折角 UseCase が inerface に依存するようになったんやし、シーンテストするときに詳細の実装を入れ替えられたらええんちゃうんちゃうん?」
みたいなノリでやり始めたのですが、結構つまづきポイントが多く、結局はテスト都合の実装を製品コードに埋め込む必要があるという結果になってしまいました。

それでもテストがあればデグレへの不安が大幅に減りますし、バグが見つかってもUnity側(クライアント側)で完結したテストが通っていれば原因切り分けの助けになるでしょう。

補足1

TestPresenter は通常の Presenter はコンストラクタで IView を受け取る仕様になっていて、しかも readonly で書き換え不可能なのために仕方なく生まれたもので、

外部に依存しているクラスを別のテスト用のクラスに入れ替えて、にUnity内の実装だけをテスト

という文脈とは本来的には無関係です。

補足2

Play Modeで[Run All]するとZenjectのテストでコケる場合があります。

TestScene (0.056s)
---
Zenject.ZenjectException : Assert hit! Cannot load scene 'TestSceneContextEvents' for test 'TestSceneContextEvents'.  The scenes used by SceneTestFixture derived classes must be added to the build settings for the test to work
---

エラーメッセージに書いてあるように、Scenes In Buildに TestSceneContextEvents シーンを追加してやれば通ります。
(Zenject/OptionalExtras/IntegrationTests/SceneTests/TestSceneContextEvents/ にあります)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

インスタンスの管理はちゃんとせよという教訓を得た話

ちゃんとしたシステムを作りたい場合インスタンスというもののライフサイクルをきちんと把握しておかなければいけません。
プログラムは目に見えないものなので、インスタンスがどこで作成されて、どこで解放されるのか、それが把握できなくなるということは極端な話ですが制御不能だといってもいいでしょう。

例えばデザインパターンではインスタンス生成に関するパターンという区分けがありまして、代表的なもので言えばSingletonとかFactory(AbstractFactoryやFactoryMethod)がありますね。

今回私が作っていたシステムではスレッドの実行とインスタンス生成を組み合わせた処理がありました。
プログラムの全貌を話すことはできませんが、搔い摘むと下記のようなイメージです。

TaskManager.cs
class TaskManager
{
    private CancellationTokenSource source;
    public void Create(ISomethingProcess proc){
        source = new CancellationTokenSource();
        var token = source.Token;

        Task.Run(async() =>{
            try{
                while(!token.IsCancellationRequested){
                    //何かをする処理 ...
                    proc.SomeMethod();


                    await Task.Delay(100, token);//ちょっと待ってみたり
                }
            catch(TaskCanceledException e){
                logger.Info("キャンセルされました",e);
            }
            catch(Exception e){
                logger.Error("致命的なエラーが発生しました。",e);
                throw;
            }
        },token);
    }
    public void Cancel(){
        source.Cancel();
    }
}

TaskList.cs
class TaskList
{
    public TaskList(IFactory factory){
        list = new List<TaskManager>();
        this.factory = factory;

    }
    private List<TaskManager> list;
    private IFactory factory;

    public void Start(){
        Stop();
        list.AddRange(factory.CreateTaskList());
        foreach(var each in list){
            each.Create(factory.GetSomethings());
        }
    }

    public void Stop(){
        foreach(var each in list){
            each.Cancel();
        }
        list.Clear();
    }
}

TaskListはコマンドによってタスクを起動したり、停止したりします。
TaskManagerはTaskを生成したり、キャンセルしたりします。

今回TaskListはいろんな処理からStartしたりStopしたりをしてまして、横着して以下のようなコードを書いてしまってました。

Something.cs
class Something
{
    public void Hoge(){
        tasklist = new TaskList(factory);
        tasklist.Start(); // まとめて実行
    }
    public void Fuga(){
        tasklist?.Stop();
    }
    TaskList tasklist;
}

TaskList自体には何も問題ないかもしれませんがSomethingクラスではtasklistのインスタンスをHogeが実行されるたびに再生成し、スタートをしています。
私はTaskListを作る時、「StopしてからStartしてればタスクがキャンセルされるので制御できるよね!」という考えのもと作ってたんですが
Somethingを作ってた時は、「TaskList再生成すればGCされるだろうし、再生成すればいいよね~」と考えてた感じです。
この処理で、2回3回連続してHogeを実行してしまうと、見事に以前インスタンス化したTaskListが宙ぶらりんになってしまい、キャンセルできないタスクが実行しっぱなしという現象に見舞われたのでした。

今回は全部自分で作成しているシステムでしたのでもう惑わされませんが、これがTaskListとSomething、分業して開発している場合は普通に起こりえる問題ですよね。怖い怖い。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む