- 投稿日:2019-08-19T13:04:59+09:00
【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を使いたい」です!はじめに
UniEnumExtensionはBoothとGitHub上で公開されているエディタ拡張のアセットです。
「UniEnumExtension」を導入することで列挙型のToStringを元々のソースコードを一切書き換えずに概ね500倍から100倍以上高速化出来ます。
バージョン情報など
- UniEnumExtension version 0.1.1
- Unity2018.4, 2019.2で動作確認
導入方法
Gitを既にインストールしている場合
まず初めに新規プロジェクトを作成します。
Packagesフォルダには初期時点でAnalystic LibraryやPackage Manager UIなどが含まれていますね。
次にコマンドプロンプトやターミナルなどを起動してください。
対象プロジェクト以下のPackagesフォルダに移動してください。git clone https://github.com/pCYSl5EDgo/UniEnumExtension
とコマンドを入力してください。以下のような表示になるはずです。
Unityエディタに戻って見てみましょう。新たにUniEnumExtensionとMono.Cecilが増えたはずです。
以上でインストールは終了です。 簡単ですね!
Boothから購入した場合
ダウンロードしたzipファイルをその場に解凍します。
UniEnumExtension.unitypackageファイルが出てきますので、それをプロジェクトにドラッグ&ドロップなどでインポートしてください。
LICENSE-jpはつまり開発者は全員エディタ拡張として1PCにつき1つ購入して使用することと譲渡や再頒布の禁止などを定めているものです。Assets/Plugins/UniEnumExtensionInstallerはGitHubからUniEnumExtensionをインストールするためだけのインストーラーです。
Packages/UniEnumExtensionを確認できたならば削除して構いません。使い方
特に何もせずとも列挙型の性能が良くなります。
……というのでは解説が寂しいのでもうちょっと具体的に。 メニューのWindow/UniEnumExtensionをクリックすると以下のようなウィンドウが開きます。
"ProcessRewriteToString All Assemblies"にチェックが入っていますが、このチェックがあることで全Playerビルドに含まれるasmdefが処理対象となります。
チェックを外してみましょう。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を立ててください。
- 投稿日:2019-08-19T07:20:13+09:00
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なメソッド、プロパティ、フィールドに対してテストコードを書く際に、拡張メソッドを用いることで、シンプルに書ける方法を紹介しました。
本稿の内容を活用して こちら のツールを作ったりしています。
- 投稿日:2019-08-19T05:07:52+09:00
UnityでInputの分岐器を作った話
概要
Unityあるある、「複数の操作可能なオブジェクトが同時に操作できてしまう」を抹殺するため、常に1か所にのみインプットを流す仕組みを作ったお話。
作成したもの
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.cspublic interface IInputSupplier{ bool anyKey{ get; } //中略 }Entity
WrapUnityStandardInputEntity.cspublic class WrapUnityStandardInputEntity:IInputSupplier{ public bool anyKey => Input.anyKey; //中略 }Empty
WrapUnityStandardInputEmpty.cspublic class WrapUnityStandardInputEmpty:IInputSupplier{ public bool anyKey => false; //中略 }Tips
- あくまで本ライブラリはインプットを中継し分岐させるだけなので、サプライヤーはカスタマイズ前提です。
- EntityにMonobehaviour継承クラスを使用したい場合は、下記のようにInputTtクラスの初期化をコメントアウト等で無効にした上でセットを行ってください。
InputTt.csprivate 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個くらいインプット制御のアプローチ作ってきたのですが、これが一番使い勝手よくてカスタマイズも楽だったので公開してみました。
- 投稿日:2019-08-19T00:34:45+09:00
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で検索してインストールします。
プロジェクトの編集
インストールが終わったら、プロジェクト内の_Imports.razorにRadzen.blazorの参照を追加します。
_Imports.razor// 略 // 追加 @using Radzen.Blazorフォント及びCSSのインストールと設定
テーマファイルを公式サイトからDLしてインストール
https://github.com/akorchev/blazor.radzen.com/raw/master/Radzen.Blazor.Themes.zipdistフォルダ以下に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
定番のボタン。
Clickイベントを渡す、おなじみの使い方です。
IconプロパティでMaterial iconsのアイコン名を指定すると使えます。button_sample.blazor<RadzenButton Click="@ButtonClicked" Icon="account_circle" Text="Test"></RadzenButton> @code { void ButtonClicked() { // } }DatePicker
これも、NET開発者ならおなじみのやつです。
日付のみのものから時刻まで選択可能なものまで揃っています。
(日付、時刻両方入力可能な形式もあり)
- 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。
XAMLとかで記述するのと同じように設定できます。
- データソースのコレクションを渡す(Dataプロパティ)、
- 内部にカラムを定義して、コレクションの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コンポーネントとして作成します。ダイアログ表示は一手間かかるので順に手順を説明します。
DialogServiceの登録
ダイアログを表示するためのサービスをDIする必要があるため、まずはStartup.csにDialogServiceを追加します。(ネームスペースはRadzen)
Startup.cspublic 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; } }ダイアログを呼び出すページの作成
次にダイアログを表示するページを作成します。
ボタンを押下したらダイアログを表示させるとします。
- RadzenDialogのタグを記述
- 先ほどと同様にdialogServiceをinject
- ボタンの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/その他、過去投稿したBlazor関連記事
Blazorで作成したウェブサイトをGitHub Pagesで公開する
Blazorで作成したウェブサイトをFirebaseで公開する