20190819のC#に関する記事は4件です。

【Unityゆるふわアドカレ2019】UniEnumExtensionで500倍速の列挙型を扱おう【Mono.Cecil】

今日は私の誕生日なので24歳学生としては初投稿です。

この記事はUnityゆるふわサマーアドベントカレンダー 2019の19日目の記事です。
前々日17日の記事は @nanaki_pg さんの「Oculusの加速度センサを使ったVR空間での移動」でした!
前日18日の記事は @mao_ さんの「」でした!
翌日20日の記事は @kingyo222 さんの「Unity:UI Elements でいくつかサンプル書いたよ!」です!
翌々日21日の記事は @yKimisaki さんの「UIElementsでもUniRxを使いたい」です!

はじめに

UniEnumExtensionBoothGitHub上で公開されているエディタ拡張のアセットです。

「UniEnumExtension」を導入することで列挙型のToStringを元々のソースコードを一切書き換えずに概ね500倍から100倍以上高速化出来ます。

バージョン情報など

  • UniEnumExtension version 0.1.1
  • Unity2018.4, 2019.2で動作確認

導入方法

Gitを既にインストールしている場合

image.png

まず初めに新規プロジェクトを作成します。

image.png

Packagesフォルダには初期時点でAnalystic LibraryやPackage Manager UIなどが含まれていますね。

次にコマンドプロンプトやターミナルなどを起動してください。
対象プロジェクト以下のPackagesフォルダに移動してください。

git clone https://github.com/pCYSl5EDgo/UniEnumExtension
とコマンドを入力してください。

以下のような表示になるはずです。

image.png

Unityエディタに戻って見てみましょう。新たにUniEnumExtensionとMono.Cecilが増えたはずです。

image.png

以上でインストールは終了です。 簡単ですね!

Boothから購入した場合

ダウンロードしたzipファイルをその場に解凍します。
UniEnumExtension.unitypackageファイルが出てきますので、それをプロジェクトにドラッグ&ドロップなどでインポートしてください。
LICENSE-jpはつまり開発者は全員エディタ拡張として1PCにつき1つ購入して使用することと譲渡や再頒布の禁止などを定めているものです。

image.png

image.png

Assets/Plugins/UniEnumExtensionInstallerはGitHubからUniEnumExtensionをインストールするためだけのインストーラーです。
Packages/UniEnumExtensionを確認できたならば削除して構いません。

使い方

特に何もせずとも列挙型の性能が良くなります。

……というのでは解説が寂しいのでもうちょっと具体的に。 メニューのWindow/UniEnumExtensionをクリックすると以下のようなウィンドウが開きます。
image.png

"ProcessRewriteToString All Assemblies"にチェックが入っていますが、このチェックがあることで全Playerビルドに含まれるasmdefが処理対象となります。
チェックを外してみましょう。

image.png

UniEnumExtensionは処理対象の数に比例してコンパイル時間を長くします。列挙型が含まれないアセンブリを対象外に指定すれば処理時間は短くなります。
手元で計測した所1アセンブリあたり0.05~0.2秒ほど処理時間が掛かっていました。

なぜこのアセットが必要なのか?

Enums.NETに詳しいです。

  • ToStringなどでもリフレクションが走り、キャッシュが効いたり効かなかったりメソッドに依ってまちまち
  • System.Enumのメソッドは殆どが非ジェネリックであり、無駄なtypeof()が必要
    • 第2引数がobject型を要求したりするのでボクシング・アンボクシングが常に生じる
    • 戻り値の型がT[]ではなくArrayなのでキャスティング必須

さて、Enums.NETはEnumsNET名前空間とEnums静的クラスを定義することこれら問題を打破しました。
その結果、列挙型のToStringにおいて標準に比較して45倍の高速化を達成しました。
これは素晴らしいことです。

UniEnumExtensionは標準に対して300~500倍の高速化を達成しています。
これはMono.Cecilによる静的IL解析によりポストコンパイル時にボクシングの生じるメソッド呼び出しや仮想メソッド呼びなどを定数に置換したり、高速でアロケーションの少ない非仮想メソッド呼び出しに置換することで実現されています。

また、全然違う機能ですが、Burst Job内でforeachが使えます!

いかなる場合に高速化が行われているのか

  • 処理対象のアセンブリに含まれる列挙型に対するToString呼び出し全て1^
  • Enum.IsDefined(typeof(具体的な型名), 定数 | 文字列型の変数) は定数埋め込みあるいは高速な関数呼び出しに置換されます2^
    • 特に第2引数が定数である時、真偽値の定数埋め込みに置換されます
  • Enum.GetValues(typeof(具体的な型名))は高速な配列新規生成と初期化に置換されます
  • HasFlag(Enum)で生じる2回のボクシングと仮想メソッド呼び出しは0回のボクシングと具象メソッド呼び出しまたは定数埋め込みに置換されます
  • 処理対象アセンブリに含まれる列挙型にIEquatable<列挙型>を実装させることでSystem.Collections.DictionaryのTKeyに指定した時の動作が高速になる場合があります

高度なトピック

Packages/UniEnumExtension/BuildPlayer/EnumExtensionPostBuildPlayerScriptDll.csとPackages/UniEnumExtension/UI/Program.csを御覧ください。

EnumExtensionPostBuildPlayerScriptDll.cs は MonoビルドやIL2CPPビルド時にポストプロセスIL編集を行います。
UnityEditor.Build.IPostBuildPlayerScriptDLLsを実装したクラスはビルド時にいつの間にかインスタンス化され、コールバックが呼ばれます。コールバックのタイミングはビルドに使われる全C#ファイルがコンパイルされてDLLが出揃った時です。
public void OnPostBuildPlayerScriptDLLs(BuildReport report)
{
    step[0] = BeginBuildStep.Invoke(report, uniEnumExtension);
    try
    {
        Implement(report);
    }
    finally
    {
        EndBuildStep.Invoke(report, step);
    }
}

コールバック内では特に必要は無いですが、UnityEditor.Build.Reporting.BuildReportのinternalなAPIであるBegin/EndBuildStepを使っています。
公式リファレンス内に説明がないので具体的な働きは不明ですが、おそらくビルド時間やエラーハンドリングする際の情報量が増えるのでしょう。

private void Implement(BuildReport report)
{
    string[] guidArray = AssetDatabase.FindAssets("t:" + nameof(ProgramStatus));
    ProgramStatus programStatus = AssetDatabase.LoadAssetAtPath<ProgramStatus>(AssetDatabase.GUIDToAssetPath(guidArray[0]));
    programStatus.Initialize();
    IEnumerable<string> targetNames = programStatus.Enables.Zip(programStatus.OutputPaths, (enable, outputPath) => (enable, Path.GetFileName(outputPath))).ToArray();
    IEnumerable<string> assemblyPaths = report.files.Where(buildFile =>
    {
        if (buildFile.role != "ManagedLibrary")
        {
            return false;
        }
        if (string.IsNullOrWhiteSpace(buildFile.path)) return false;
        string buildName = Path.GetFileName(buildFile.path);
        return targetNames.All(pair => pair.Item2 != buildName) || targetNames.First(pair => pair.Item2 == buildName).Item1;
    }).Select(buildFile => buildFile.path);
    string directoryName = Path.GetDirectoryName(report.files[0].path);
    Debug.Log(directoryName);
    using (var extender = new EnumExtender(searchDirectory: new string[1] { directoryName }))
    {
        extender.Extend(assemblyPaths);
    }
}

シングルトンな設定ファイルなScriptableObjectを読み込みます。
その後BuildReportのfilesプロパティでDLLやPDBファイル一覧を得られます。
その内、roleがManagedLibraryなもので設定上処理対象なDLLのみをIEnumerable<string> assemblyPathsに取り出します。
これをEnumExtenderのインスタンスのExtendメソッドに渡せばIL書き換えがそのアセンブリ群に対して行われます。
EnumExtenderのコンストラクタにはアセンブリの参照を解決するためのディレクトリ名を与えます。
アセンブリの参照解決って?という方はMono.Cecil入門を御覧ください。

高レベルAPI

using(EnumExtender extender = new EnumExtender(string[] searchDirectory))
extender.Extend(IEnumerable<string> assemblyPaths);

が高レベルAPIとして露出されています。
assemblyPathsでパスを指定するとよしなに色々処理します。

低レベルAPI

EnumExtenderにはもう1つコンストラクタがあります。

public EnumExtender(IModuleProcessor[] moduleProcessorCollection, ITypeProcessor[] typeProcessorCollection, IMethodProcessor[] methodProcessorCollection, string[] searchDirectories)

UniEnumExtension.IModuleProcessor, UniEnumExtension.ITypeProcessor, UniEnumExtension.IMethodProcessorはモジュール(アセンブリ)、型、メソッドに対して処理を行うインターフェースです。
コンストラクタからIL処理を登録するのですね。

上述3インターフェースは公開されていますので独自にポストコンパイル時にフックしてEnumExtenderを実行すると良いでしょう。
注意点としては、EnumExtenderは必ずDisposeしてください。DisposeすることでILの書き込みが完了します。

EnumExtenderはUniEnumExtension.IExtenderを実装していますのでその段階からすげ替えるのも良いかもしれませんね。

public interface IExtender : IDisposable
{
    void Extend(IEnumerable<string> assemblyPaths);
}
public interface IModuleProcessor : IProcessor
{
    void Process(ModuleDefinition moduleDefinition);
}
public interface ITypeProcessor : IProcessor
{
    void Process(ModuleDefinition systemModuleDefinition, TypeDefinition typeDefinition);
}
public interface IMethodProcessor : IProcessor
{
    bool ShouldProcess(TypeDefinition typeDefinition);
    void Process(ModuleDefinition systemModuleDefinition, MethodDefinition methodDefinition);
}

感想

Mono.Cecilでポストコンパイル時constexprをする可能性を感じられました。
readonlyフィールドが実現してILレベルでもなんらかの形で表現出来たならば本当にconstexprが出来ると思います。

機能追加の要望などがありましたら、私のTwitterにDMまたはGitHubでIssueを立ててください

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

C#でprivateメソッドをテストする時の便利な書き方(実装の都合をテストコードから排除)

はじめに

C#でprivateメソッドに対してMSTestで単体テストを作成する時の便利な書き方の紹介です。
privateメソッドをテストするには、実装の都合で面倒なコードを書く必要がありますが、それを解消します。

前提知識

C#のMSTestにて、privateのメソッド、プロパティ、フィールドを呼び出したい場合は、PrivateObjectクラスを用います。
また、staticでprivateのメンバーは、PrivateTypeクラスを用います。
以下に例を記します。

    // テスト対象のクラス
    public class MyClass
    {
        // privateメソッド
        private int AddValue(int additionValue)
        {
            return _Value + additionValue;
        }

        // privateプロパティ
        private bool IsZero => _Value == 0;

        // privateフィールド
        private int _Value = 0;

        // staticなprivateメソッド
        private static int GetTotalValue(int value1, int value2)
        {
            return value1 + value2;
        }
    }

    // MyClassのprivateなメンバーをテストするためのテストコード
    [TestClass]
    public class MyClassTest
    {
        // privateメソッドのテスト
        [TestMethod]
        public void AddValueTest()
        {
            // PrivateObjectを作成して、AddValueメソッドを呼び出す
            var myClass = new MyClass();
            var privateObject = new PrivateObject(myClass);
            var value = (int) privateObject.Invoke("AddValue", 1);
            Assert.AreEqual(1, value);
        }

        // privateプロパティとprivateフィールドのテスト
        [TestMethod]
        public void IsZeroTest()
        {
            // PrivateObjectを作成して、_ValueフィールドとIsZeroプロパティを呼び出す
            var myClass = new MyClass();
            var privateObject = new PrivateObject(myClass);

            // privateフィールドを取得
            var value = (int) privateObject.GetField("_Value");
            Assert.AreEqual(0, value);

            // privateプロパティを取得
            var isZero = (bool) privateObject.GetProperty("IsZero");
            Assert.IsTrue(isZero);
        }

        // staticなprivateメソッドのテスト
        [TestMethod]
        public void GetTotalValueTest()
        {
            // PrivateTypeを作成して、GetTotalValueメソッドを呼び出す
            var privateType = new PrivateType(typeof(MyClass));
            var value = (int) privateType.InvokeStatic("GetTotalValue", 1, 1);
            Assert.AreEqual(2, value);
        }
    }

詳細は以下を参照ください。
MSTestでprivateメソッドをテストする | 山本隆の開発日誌

便利な書き方

そして便利な書き方の紹介です。
上記の通り、PrivateObjectとPrivateTypeのインスタンスをテストコードで毎回インスタンス化するのは面倒です。また、PrivateObjectなどを使うことは実装の都合であり、そのテストの関心事ではないため、テストコードからは排除したいです。
そこで、テストするメソッドと同じ名前で同じシグネチャの拡張メソッドを作成し、その拡張メソッドの中で、PrivateObjectを作成するようにします。
具体例を以下に記します。

        // MyClassの以下のメソッド用の拡張メソッドを定義
        // private int AddValue(int additionValue)

        public static int AddValue(this MyClass myClass, int additionValue)
        {
            return (int) new PrivateObject(myClass).Invoke("AddValue", additionValue);
        }

上記のように、拡張メソッドを定義しておけば、各テストコードは、シンプルに記載できます。
privateメソッドである AddValue(int additionValue) は、上記で定義した拡張メソッドにより、まるでprivateのメソッドをそのまま呼び出しているかように実行できます(実際にはpublicな拡張メソッドが呼び出されています)。もちろんインテリセンスも効きます。

            var myClass = new MyClass();
            var value = myClass.AddValue(1);

privateなプロパティとフィールドについても同様です。
ただ、C# 8.0時点の構文には「拡張プロパティ」はまだ無いため、IsZeroプロパティは IsZero() という拡張メソッドで呼び出す必要があります。() を付ける必要があるため、全く同じ呼び出し方にはなりませんが、それでも元のテストコードよりシンプルになります。
(拡張プロパティは、当初は C# 8.0 の候補だったので、C# 9.0くらいで実現されるかもしれません)
また、staticなprivateメソッドに関しては、拡張メソッドで代替できないため、別のクラス(ここではMyClassExtensionsクラス)で、同じ名前とシグネチャのstaticメソッドを定義します。

上記の方法をすべて行うことで、実装の都合であるPrivateObjectとPrivateTypeをテストコードから排除できます。
以下に、冒頭に記載したMyClassのテストを改善した例を記載します。

    // 拡張メソッドの定義(テストプロジェクト内で定義)
    public static class MyClassExtensions
    {
        // privateメソッド用の拡張メソッド
        public static int AddValue(this MyClass myClass, int additionValue)
        {
            return (int) new PrivateObject(myClass).Invoke("AddValue", additionValue);
        }

        // privateプロパティ用の拡張メソッド
        public static bool IsZero(this MyClass myClass)
        {
            return (bool) new PrivateObject(myClass).GetProperty("IsZero");
        }

        // privateフィールド用の拡張メソッド
        public static int _Value(this MyClass myClass)
        {
            return (int) new PrivateObject(myClass).GetField("_Value");
        }

        // staticなprivateメソッド用のstaticメソッド
        public static int GetTotalValue(int value1, int value2)
        {
            return (int) new PrivateType(typeof(MyClass)).InvokeStatic("GetTotalValue", value1, value2);
        }
    }

    // テストコードは、PrivateObjectとPrivateTypeを用いずに、シンプルに記載
    [TestClass]
    public class MyClassTest
    {
        // privateメソッドのテスト
        [TestMethod]
        public void AddValueTest_Simple()
        {
            var myClass = new MyClass();
            var value = myClass.AddValue(1);
            Assert.AreEqual(1, value);
        }

        // privateプロパティとprivateフィールドのテスト
        [TestMethod]
        public void IsZeroTest_Simple()
        {
            var myClass = new MyClass();

            // privateフィールドを取得
            var value = myClass._Value();
            Assert.AreEqual(0, value);

            // privateプロパティを取得
            var isZero = myClass.IsZero();
            Assert.IsTrue(isZero);
        }

        // staticなprivateメソッドのテスト
        [TestMethod]
        public void GetTotalValueTest_Simple()
        {
            var value = MyClassExtensions.GetTotalValue(1, 1);
            Assert.AreEqual(2, value);
        }
    }

まとめ

C#でprivateなメソッド、プロパティ、フィールドに対してテストコードを書く際に、拡張メソッドを用いることで、シンプルに書ける方法を紹介しました。
本稿の内容を活用して こちら のツールを作ったりしています。

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

UnityでInputの分岐器を作った話

概要

Unityあるある、「複数の操作可能なオブジェクトが同時に操作できてしまう」を抹殺するため、常に1か所にのみインプットを流す仕組みを作ったお話。
example.gif

作成したもの

https://github.com/domasyake/InputTurnout

経緯

 Unity、カスタマイズ前提で敢えて最低限の機能しか用意してないんだろうなーっていうもの幾つかあると個人的には思います、インプットなんかはそのうちの1つ。ゲームはプレイヤーキャラやUIなど、「入力によって操作できるもの」が多数必要になりますが、各自がバラバラにインプット情報を取得するとなると、操作対象の切り替えもBoolで判定したりとかGameObjectそのもののActiveで止めたりとかするしかなく、バグの温床になること請け合い。
 そこでインプット情報を常に1か所にのみ流すようにする分岐器(線路の切り替えするアレ)みたいなものを経由すれば、制御対象=インプット出力先となり各自で判定せずに済むだろうと考え作成しました。 
 ついでにインプットの依存先をInterfaceに強制できるのでインプット出力元の可変(アセット使ったりモック使ったり)にも対応できるようになります。

仕組み

インプットの出力元をサプライヤー、インプットを利用する側をレシーバーとします。
サプライヤーとして、実際に出力を行うEntityと常に非入力値を返すEmptyの2つを用意します。レシーバーは分岐器を介してサプライヤーにアクセスしますが、1つのレシーバーにのみEntityを、それ以外にEmptyを常に返すことでレシーバーは自身が今操作対象なのかどうかを意識せずに実装することができます。

使い方

GitHubのReleaseにあるUnityPackageをインポートしてください。サンプルも含まれているのでそちらも参考に。

レシーバー

private void Start(){
   InputTt.ChangeReceiver(this);//本来はレシーバーが直接ChangeReceiverするのではなく別途Managerなどの制御クラスで行うべき
}

private void Update(){  
   if (InputTt.Input(this).XXXX()){
   //移動処理とか
   }
}

サプライヤー

Interface

標準ではUnityEngine.Inputの主要メソッドのラッパーとして定義しています

IInputSupplier.cs
public interface IInputSupplier{
   bool anyKey{ get; }
   //中略
}

Entity

WrapUnityStandardInputEntity.cs
public class WrapUnityStandardInputEntity:IInputSupplier{
   public bool anyKey => Input.anyKey;
   //中略
}

Empty

WrapUnityStandardInputEmpty.cs
public class WrapUnityStandardInputEmpty:IInputSupplier{
   public bool anyKey => false; 
   //中略
}

Tips

  • あくまで本ライブラリはインプットを中継し分岐させるだけなので、サプライヤーはカスタマイズ前提です。 
  • EntityにMonobehaviour継承クラスを使用したい場合は、下記のようにInputTtクラスの初期化をコメントアウト等で無効にした上でセットを行ってください。
InputTt.cs
private static void SupplierInitialize(){
   //SetSupplier(new WrapUnityStandardInputEntity(),new WrapUnityStandardInputEmpty());
}

public static void SetSupplier(IInputSupplier entity,IInputSupplier empty){
   LogWrite("------see Input Supplier------");
   inputSupplier = entity;
   emptySupplier = empty;
}
  • あくまでInputTt.Input(object key)に渡されたインスタンスで判定を行っているだけなので複数のレシーバーで共通のインスタンスを持ってそれをキーとしてアクセスすれば複数クラスでインプットを受け取ることも可能です。
  • InputTt.IsActiveプロパティでインプットを停止できます。
  • インスタンスをキーにあらゆる変更を停止するFreeze()と解除用のUnFreeze()メソッドが用意されています。暗転時等、是が非でも操作させたくない時にIsActiveと合わせて使用したりなど。

余談

 冗長なコードが死ぬほど嫌いなので今まで3個くらいインプット制御のアプローチ作ってきたのですが、これが一番使い勝手よくてカスタマイズも楽だったので公開してみました。

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

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

概要

Blazor向けのUIフレームワークであるRadzen Blazor Componentsを導入して試すまでのメモ。

Demo
https://nobu17.github.io/RadzenBlazorTest/
ソース
https://github.com/nobu17/RadzenBlazorTest/

背景

Blazor単体にはSPA用のUIフレームワークは用意されていないので別途インストールが必要です。
Boootstrap等の一般的なHTML,css用のフレームワークを使用してゴリゴリHTMLを書く方法も考えられますが、今回はBlazorに特化されたものを使用してみようと思います。

前提

使用している.NET Core及びBlazorのテンプレートのバージョン等は下記です。

  • .NET Core 3.0 Preview SDK 3.0.100-preview7-012821
  • Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview7.19365.7
  • Visual Studio 2019 Preview

  • Radzen.Blazor 0.0.52

インストール手順

プロジェクトの作成

まずは、公式サイトの手順に沿ってテンプレートから新規Blazorプロジェクトを作成します。
今回はclient-side版を使用します。
公式ドキュメント

Radzen.blazorのインストール及び設定

Radzenのページに紹介されている手順に沿ってインストールしてみます。

NuGetからインストール

NugetからRadzen.blazorをインストールします。
ソリューションエクスプローラの依存関係を右クリック→NuGetパッケージの管理 を選択します。
Radzen.blazorで検索してインストールします。
nuget.png

プロジェクトの編集

インストールが終わったら、プロジェクト内の_Imports.razorにRadzen.blazorの参照を追加します。

_Imports.razor
// 略
// 追加
@using Radzen.Blazor

フォント及びCSSのインストールと設定

テーマファイルを公式サイトからDLしてインストール
https://github.com/akorchev/blazor.radzen.com/raw/master/Radzen.Blazor.Themes.zip

distフォルダ以下にfontsフォルダとcssフォルダがあるので、
Blazorのプロジェクト内のwwwrootルートフォルダ内にコピー&ペーストします。

次に、_Host.cshtml file (server-side Blazor) or wwwroot/index.html (client-side Blazor)
を開いてcssの参照を追加します。今回はClientSideなのでindex.htmlになります。

index.html
<!DOCTYPE html>
<html>
<head>
    // 略
    <link rel="stylesheet" href="css/default.css">
</head>
<body>
    // 略
</body>
</html>

プロジェクトファイルの編集

ClientSideのプロジェクトの場合、csprojファイルを開いて、
RazorLangVersionの後にBlazorLinkOnBuildにfalseに設定した要素を追加する

xxxx.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>7.3</LangVersion>
    <RazorLangVersion>3.0</RazorLangVersion>
    <!-- 追加  -->
    <BlazorLinkOnBuild>false</BlazorLinkOnBuild>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Blazor" Version="3.0.0-preview7.19365.7" />
    <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="3.0.0-preview7.19365.7" PrivateAssets="all" />
    <PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="3.0.0-preview7.19365.7" PrivateAssets="all" />
    <PackageReference Include="Radzen.Blazor" Version="0.0.49" />
  </ItemGroup>

</Project>

セットアップは終了です。
まずはここまででビルドが通るか確認します。

コンポーネント紹介

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

Button

定番のボタン。

btn1.png

Clickイベントを渡す、おなじみの使い方です。
IconプロパティでMaterial iconsのアイコン名を指定すると使えます。

button_sample.blazor
<RadzenButton Click="@ButtonClicked" Icon="account_circle" Text="Test"></RadzenButton>

@code {
  void ButtonClicked()
  {
    //
  }
}

DatePicker

これも、NET開発者ならおなじみのやつです。

日付のみのものから時刻まで選択可能なものまで揃っています。
(日付、時刻両方入力可能な形式もあり)

timtim.png

  • ValueにDatetime型のプロパティとバインディングを行います。
  • ShowTime、ShowSeconds、TimeOnlyプロパティで表示形式を変更できます。
  • DateFormat、HourFormatで表示形式を変更可能。
datepciker_sample.blazor
@page "/datepicker_sample"


<h3>DatePickerSample</h3>
<RadzenCard>
    <div class="row">
        <div class="col-md-12">
            <h3>DatePicker</h3>
            <RadzenDatePicker  Value="CurrentTime" Change="Change" />
            <h3 style="margin-top: 40px">DatePicker with time</h3>
            <RadzenDatePicker Value="CurrentTime" ShowTime="true" Change="Change" />
            <h3 style="margin-top: 40px">DatePicker with 12 hour time format</h3>
            <RadzenDatePicker Value="CurrentTime" ShowTime="true" Change="Change" HourFormat="12" />
            <h3 style="margin-top: 40px">Time-only DatePicker</h3>
            <RadzenDatePicker Value="CurrentTime" ShowTime="true" TimeOnly="true" DateFormat="HH:mm" Change="Change" />
            <h3 style="margin-top: 40px">Calendar</h3>
            <RadzenDatePicker Value="CurrentTime" Inline="true" Change="Change" />
        </div>
    </div>
</RadzenCard>

@code {

    public DateTime CurrentTime { get; set; }

    protected override void OnInit()
    {
        CurrentTime = DateTime.Now;
    }

    void Change(DateTime? value)
    {
        CurrentTime = value.GetValueOrDefault();
        StateHasChanged();
    }
}

DataGrid

.NET開発者ならよく使う、みんな大好きDataGrid。

dg1.png

XAMLとかで記述するのと同じように設定できます。

  1. データソースのコレクションを渡す(Dataプロパティ)、
  2. 内部にカラムを定義して、コレクションの1要素内でバインドするプロパティを設定(TItemで型を指定し、Propertyでプロパティ名を指定)
datagrid_sample.blazor
@if (@persons == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <RadzenGrid AllowFiltering="true" AllowPaging="true" PageSize="4" AllowSorting="true" Data="@(persons)" TItem="Person" RowSelect="RowSelect">
        <Columns>
            <RadzenGridColumn TItem="Person" Property="Name" Title="Name" Type="string" />
            <RadzenGridColumn TItem="Person" Property="Age" Title="Age" Type="integer" />
            <RadzenGridColumn TItem="Person" Property="Birthdate" Title="Birth Date" Format="date-time" FormatString="{0:yyyy/MM/dd HH:mm:ss}">
            </RadzenGridColumn>
        </Columns>
    </RadzenGrid>

    if (@selectedPerson != null)
    {
        <div class="row">
            <div class="col-md-12">
                <div>@selectedPerson.Name が選択されました。</div>
            </div>
        </div>
    }
}

@code {
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public DateTime Birthdate { get; set; }
    }
    Person selectedPerson = null;
    IEnumerable<Person> persons = null;

    protected override void OnInit()
    {
        persons = Enumerable.Range(0, 10).Select(p => new Person() { Name = p + "さん", Age = p + 10, Birthdate = DateTime.Now });
    }

    void RowSelect(Person p)
    {
        selectedPerson = p;
    }
}

ダイアログ

これもよく使うやつです。
ダイアログの中身もrazorコンポーネントとして作成します。

d2.png

ダイアログ表示は一手間かかるので順に手順を説明します。

DialogServiceの登録

ダイアログを表示するためのサービスをDIする必要があるため、まずはStartup.csにDialogServiceを追加します。(ネームスペースはRadzen)

Startup.cs
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<DialogService>();
        }

        public void Configure(IComponentsApplicationBuilder app)
        {
            app.AddComponent<App>("app");
        }
    }

ダイアログのレイアウト用コンポーネントの作成

次にDialog自体のレイアウトのレイアウトを定義したコンポーネントを作成します。
Dialogserviceをindjectしてボタン押下時にダイアログを閉じて結果を返すようにしています。

TestDialog.razor
@inject Radzen.DialogService dialogService

<div class="row">
    <div class="col-md-12" style="height:200px;">
        <div>@Message</div>
    </div>
</div>
<div class="row">
    <div class="col-md-12">
        <RadzenButton Click="@((args) => dialogService.Close(true))" Text="OK" Style="margin-bottom: 10px; width: 150px" />
        <RadzenButton Click="@((args) => dialogService.Close(false))" ButtonStyle="secondary" Text="Cancel" Style="margin-bottom: 10px; width: 150px" />
    </div>
</div>

@code {

    [Parameter]
    public string Message { get; set; }
}

ダイアログを呼び出すページの作成

次にダイアログを表示するページを作成します。
ボタンを押下したらダイアログを表示させるとします。

  1. RadzenDialogのタグを記述
  2. 先ほどと同様にdialogServiceをinject
  3. ボタンのClickイベント内でdialogServiceのOpenメソッドを呼び出す。(ジェネリクスに先ほど作成したダイアログコンポーネントの型及びパラメータを指定します。)
TestDialog.razor
@page "/dialog_sample"

@inject Radzen.DialogService dialogService

<RadzenDialog />
<h3>DialogSample</h3>
<div class="row">
    <div class="col-md-6">
        <h3>OpenDialog</h3>
        <RadzenButton Click="Click" Text="Open" Style="margin-bottom: 20px; width: 150px" />
        <br />
        @resultText
    </div>
</div>

@code {
    string resultText = "";

    protected override void OnInit()
    {
        dialogService.OnClose += OnClose;
    }

    void Click()
    {
        // ダイアログの表示
        var dParam = new Dictionary<string, object>() { { "Message", "メッセージ" } };
        var dOpt = new Radzen.DialogOptions() { Width = "500px", Height = "300px", Left = "30%", Top = "20%" };
        dialogService.Open<TestDialog>("タイトル", dParam, dOpt);
    }

    void OnClose(dynamic result)
    {
        // ダイアログが閉じたとの結果を取得
        resultText = result.ToString();
        // 変更通知
        Invoke(() => { StateHasChanged(); });
    }
}

その他

テキストボックスやチェックボックス、数値入力、タブ等々、よく見る入力コントールはそろっている感じです。
気になる方は公式サイトを見てみてください。

まとめ

ということでBlazorのUIフレームワークのRadzen.Blazorを紹介してみました。
まだまだ発展途上ということで、詳細なドキュメントや機能自体が少なく、一般的なSPAのUIフレームワークと比較すると実装するべき点が多く大変ですが、様々なUIのコンポーネントが用意されているのは良いですね。

現状はドキュメントが無いため、GitHub上のサンプルのソースコードを見ながら実装という形ですが、ドキュメントが充実する事を期待したいですね。

Angular,React,Vue等のSPAフレームワークといえば、豊富なUIコンポーネント群がセットで使えることがメリットなのでBlazorにも今後こういったフレームワークが充実する事を期待したいですね。

Demo
https://nobu17.github.io/RadzenBlazorTest/
ソース
https://github.com/nobu17/RadzenBlazorTest/

Radzen Blazor Components公式サイト

その他、過去投稿したBlazor関連記事

Blazorで作成したウェブサイトをGitHub Pagesで公開する
Blazorで作成したウェブサイトをFirebaseで公開する

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