2023/03/03(今日ですね) にある meetup app osaka@7 で話すやつです。
meetupapp.connpass.com
dynamic で JavaScriptを書けるのですが、それではインテリセンスも使えないし再利用するためにはラップするなりで型をつけてやった方がいいですね。それで interface を定義して定義するだけで使えるようにしました。内部的には DispatchProxy を使っています。こんな感じで書けます。
<script>class Rectangle { constructor(height, width){this.height = height;this.width = width;} getArea(){returnthis.height * this.width;}}function sum(...theArgs){let total = 0;for(const arg of theArgs){ total += arg;}return total;}</script>
[JSCamelCase] publicinterface IWindow { int Sum(paramsint[] val); } [JSCamelCase] publicinterface IRectangle { int Height { get; set; } int Width { get; set; } int GetArea(); } privateasync Task Test() { usingvar js =await JS.CreateDymaicRuntimeAsync(); //メソッドvar window = js.GetWindow<IWindow>(); varvalue= window.Sum(1, 2, 3, 4, 5); //クラス生成var rect = js.New<IRectangle>("Rectangle", 5, 7); var area = rect.GetArea(); }
ルール
ここでインターフェイスを定義するとき問題がいくつかあって
- C#とJavaScriptでは命名規則が違う場合がある
- 非同期どうするか
- new とかそもそも interface で表せないもの
いくつか対応するためのルールを追加しています。
名前に対するルールです。
//camel case になる、メソッド、プロパティ単位の指定も可能 [JSCamelCase] publicinterface ITest { //インデックスアクセスできるintthis[int index]; // valueになるint Value { get; set; } // sum(,,,args) になる int Sum(paramsint[] values); // 文字列指定 JSNameで指定した名前が使われます [JSName("getData2")] int GetData(int x); }
プロパティ、new をメソッドに変換できます。
publicinterface ITest { //new Rectangle になります [JSConstructor("Rectangle")] IRectangle CreateRectangle(); //Valueになります [JSProperty("Value")] int GetValue(); [JSProperty("Value")] void SetValue(intvalue); //this[index] [JSIndexProperty] int GetAt(int index); [JSIndexProperty] void SetAt(int index, intvalue); }
非同期は Task を返すように定義することで非同期になります。名前の末尾にAsyncをつけると自動でそれは削除されます。つけたい場合は JSName を併用するとそっちが優先されます。プロパティも↑の属性でメソッドに置き換えることにより非同期で使えるようになります。
publicinterface ITest { //GetValueを非同期で呼び出します Task<int> GetValueAsync(); }
Handsontable
それで前回のHandsontableも class と interface で書いてみました。
publicclassCol { publicobject? Data { get; set; } publicobject? ReadOnly { get; set; } publicobject? Width { get; set; } publicobject? ClassName { get; set; } publicobject? Type { get; set; } publicobject? NumericFormat { get; set; } } publicclassProductMaster { publicstring? Edit { get; set; } publicbool Select { get; set; } publicstring? ProductCode { get; set; } publicstring? ProductName { get; set; } publicint UnitPrice { get; set; } publicstring? Comment { get; set; } } publicclassEnterMoves { publicint Row { get; set; } publicint Col { get; set; } } publicclassInitialData { publicobject[]? Data { get; set; } publicstring[]? ColHeaders { get; set; } public Col[]? Columns { get; set; } public EnterMoves? EnterMoves { get; set; } publicbool OutsideClickDeselects { get; set; } publicbool ManualColumnResize { get; set; } publicbool FillHandle { get; set; } } [JSCamelCase] publicinterface IHandsontable { void LoadData(List<ProductMaster> data); void SetDataAtCell(int row, int col, string data); } publicinterface IAfterChangesInfo { dynamicthis[int index] { get;set; } } [JSCamelCase] publicinterface IAfterChangesInfoArray { IAfterChangesInfo this[int index] { get; set; } int Length { get; set; } } publicstaticclassHandsontableExtentions { publicstatic IHandsontable CreateHandsontable(this DynamicJSRuntime js, ElementReference grid, InitialData data, Action<IAfterChangesInfoArray, dynamic> afterChange) { var arg = js.ToJS(data); arg.afterChange = afterChange; return js.New<IHandsontable>("Handsontable", grid, arg); } }
@page "/" @using Blazor.DynamicJS @inject IJSRuntime JS <div @ref="_grid"></div> @code { private ElementReference _grid; protectedoverrideasync Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; var js =await JS.CreateDymaicRuntimeAsync(); dynamic window = js!.GetWindow(); conststring COL_EDIT ="edit"; conststring COL_SELECT ="select"; conststring COL_PRODUCTCODE ="productCode"; conststring COL_PRODUCTNAME ="productName"; conststring COL_UNITPRICE ="unitPrice"; conststring COL_COMMENT ="comment"; conststring EDIT_MARK ="*"; IHandsontable? hot =null; hot = js.CreateHandsontable(_grid, new InitialData { Data =new object[0], ColHeaders =new[] { "編集", "選択", "商品CD", "商品名", "単価", "備考" }, Columns =new Col[] { new Col{ Data = COL_EDIT, ReadOnly =true, Type ="text" }, new Col{ Data = COL_SELECT, Type ="checkbox" }, new Col{ Data = COL_PRODUCTCODE, Type ="text", Width =80 }, new Col{ Data = COL_PRODUCTNAME, Type ="text", Width =200, ClassName ="htLeft htMiddle" }, new Col{ Data = COL_UNITPRICE, Type ="numeric", NumericFormat =new { pattern ="0,00", culture ="ja-JP" } }, new Col{ Data = COL_COMMENT, Type ="text", Width =300, ClassName ="htLeft htMiddle" } }, EnterMoves =new EnterMoves { Row =0, Col =1 }, OutsideClickDeselects =true, ManualColumnResize =true, FillHandle =false, }, (changes, source) =>{ if (source =="loadData") return; for (var i =0; i < (int)changes.Length; i++) { var change = changes[i]; // 編集と選択は対象外if (change[1] == COL_EDIT || change[1] == COL_SELECT) continue; // 変更前と変更後が同じは対象外if (change[2] == change[3]) continue; // 編集に"*"を付ける hot?.SetDataAtCell((int)changes[0][0], 0, EDIT_MARK); } } ); hot.LoadData(new List<ProductMaster>() { { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0001", ProductName ="りんご", UnitPrice =100, Comment ="青森産" } }, { new ProductMaster() { Edit ="", Select =false, ProductCode ="S0002", ProductName ="みかん", UnitPrice =80, Comment ="静岡産" } }, { new ProductMaster() { Edit ="", Select =true, ProductCode ="S0003", ProductName ="メロン", UnitPrice =1000, Comment ="袋井クラウンメロン" } } }); } }
ちょっと残念なのは = まだはうまく表現できないので source と changesの 末端は dynamic のままにしています。