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() { return this.height * this.width; } } function sum(...theArgs) { let total = 0; for (const arg of theArgs) { total += arg; } return total; } </script>
[JSCamelCase] public interface IWindow { int Sum(params int[] val); } [JSCamelCase] public interface IRectangle { int Height { get; set; } int Width { get; set; } int GetArea(); } private async Task Test() { using var js = await JS.CreateDymaicRuntimeAsync(); //メソッド var window = js.GetWindow<IWindow>(); var value = 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] public interface ITest { //インデックスアクセスできる int this[int index]; // valueになる int Value { get; set; } // sum(,,,args) になる int Sum(params int[] values); // 文字列指定 JSNameで指定した名前が使われます [JSName("getData2")] int GetData(int x); }
プロパティ、new をメソッドに変換できます。
public interface ITest { //new Rectangle になります [JSConstructor("Rectangle")] IRectangle CreateRectangle(); //Valueになります [JSProperty("Value")] int GetValue(); [JSProperty("Value")] void SetValue(int value); //this[index] [JSIndexProperty] int GetAt(int index); [JSIndexProperty] void SetAt(int index, int value); }
非同期は Task を返すように定義することで非同期になります。名前の末尾にAsyncをつけると自動でそれは削除されます。つけたい場合は JSName を併用するとそっちが優先されます。プロパティも↑の属性でメソッドに置き換えることにより非同期で使えるようになります。
public interface ITest { //GetValueを非同期で呼び出します Task<int> GetValueAsync(); }
Handsontable
それで前回のHandsontableも class と interface で書いてみました。
public class Col { public object? Data { get; set; } public object? ReadOnly { get; set; } public object? Width { get; set; } public object? ClassName { get; set; } public object? Type { get; set; } public object? NumericFormat { get; set; } } public class ProductMaster { public string? Edit { get; set; } public bool Select { get; set; } public string? ProductCode { get; set; } public string? ProductName { get; set; } public int UnitPrice { get; set; } public string? Comment { get; set; } } public class EnterMoves { public int Row { get; set; } public int Col { get; set; } } public class InitialData { public object[]? Data { get; set; } public string[]? ColHeaders { get; set; } public Col[]? Columns { get; set; } public EnterMoves? EnterMoves { get; set; } public bool OutsideClickDeselects { get; set; } public bool ManualColumnResize { get; set; } public bool FillHandle { get; set; } } [JSCamelCase] public interface IHandsontable { void LoadData(List<ProductMaster> data); void SetDataAtCell(int row, int col, string data); } public interface IAfterChangesInfo { dynamic this[int index] { get;set; } } [JSCamelCase] public interface IAfterChangesInfoArray { IAfterChangesInfo this[int index] { get; set; } int Length { get; set; } } public static class HandsontableExtentions { public static 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; protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; var js = await JS.CreateDymaicRuntimeAsync(); dynamic window = js!.GetWindow(); const string COL_EDIT = "edit"; const string COL_SELECT = "select"; const string COL_PRODUCTCODE = "productCode"; const string COL_PRODUCTNAME = "productName"; const string COL_UNITPRICE = "unitPrice"; const string COL_COMMENT = "comment"; const string 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 のままにしています。