ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

C#だけでJavaScriptを書く! Blazor.DynamicJSを作りました ①

2023/03/03 にある meetup app osaka@7 で話すやつです。
meetupapp.connpass.com

BlazorでちょいちょいJavaScript書くときありますよね、でもちょっとしたやつやからJavaScript別に書くの面倒なんですよねー、でDynamic使ったらラップできるんじゃないかってことでやってみました。Nugetにも公開しててコードはこちら
github.com

こんな感じで書けます。

//JavaScriptを書くためのオブジェクト作成
using var js = await JS.CreateDymaicRuntimeAsync();

//ここからはdynamicでJavaScript書けます。
dynamic window = js.GetWindow();

dynamic button = window.document.createElement("button");
button.innerText = "new button";

dynamic div = window.document.getElementById("parent-div");
div.append(button);

//コールバックも書けます
button.addEventListener("click", (Action<dynamic>)(e => {
    string detail = e.detail.toString();
    window.console.log($"clicked {detail}");
}));

結構頑張っててコールバックもかけるんですよね。「遅いんじゃないの?」って言われそうですけど、まあそのとおりですねw、一行につき一回くらJavaScriptの呼び出しが入ります。とはいえそんな速くはないけどWebAssemblyだとこれくらいの行数やったら実使用上は問題ないんちゃうかな?

グローバルなやつだけじゃなくてimportもこんな感じで書けます。

using var js = await JS.CreateDymaicRuntimeAsync();

//絶対パスを指定してね
dynamic mod = await _js!.ImportAsync("/module.js");

//module.js に sum ってメソッドがあるとして
int sum = mod.sum(1, 2, 3, 4);

いい感じの書き心地ですよね。ここまではよかったんですけどね・・・、new と 非同期がいまいち。まずは new

<script>
    Rectangle: class {
        constructor(height, width) {
            this.height = height;
            this.width = width;
        }
    }
 </script>
using var js = await JS.CreateDymaicRuntimeAsync();
dynamic window = js.GetWindow();

//new XXX ってやる部分を JSSynctaxで囲む
dynamic rect = new JSSyntax(window.Rectangle).New(10, 20);

//moduleの場合も同様
dynamic mod = await _js!.ImportAsync("/module.js");
//moduleにもRectangleが定義されているとして
dynamic rect2= new JSSyntax(mod.Rectangle).New(10, 20);

//あんまりなんでグローバルのクラスはこう書けるようにもしてます
var rect3 = js.New("Rectangle", 1, 2);

JSSyntaxってのが苦肉の策。ここはシンタックスなんですよー、シンタックスに対する操作なんですよーって感じ。非同期も同様に JSSyntax で囲むようにしました。

using var js = await JS.CreateDymaicRuntimeAsync();
dynamic window = js.GetWindow();

dynamic div = window.document.getElementById("parent-div");

await new JSSyntax(div.append).InvokeAsync(button);

//コールバックも非同期にできます
button.addEventListener("click", (Func<dynamic, Task>)(async e =>
{
    await Task.CompletedTask;
    string detail = e.detail.toString();
    window.console.log($"clicked {detail}");
}));

でも正直 WebAssemblyの方は別にasyncでなくてもええんちゃう?って思って僕は非同期使ってないけどどうなんやろ?あとmodule使うにせよmoduleのクラスをC#からnewすることってないから(グローバルはjs.New()でいけるようにしたし)僕が使う分には許容範囲の書き心地です。

Blazor.DynamicJS管理下のJavaScriptオブジェクト

dynamicで受けたやつは何かというと以下二つです。
①呼び出し情報
JavaScriptオブジェクト(numberとかも含む)

②はそれに対して操作することもできるし、それがJsonにできるものであればC#の世界に持ってこれます。

dynamic window = js.GetWindow();

//これはまだJavaScriptの呼び出しは発生していないくて、"window.document"っていう文字列の呼び出し情報
dynamic document = window.document;

//ここで JavaScript を呼び出して戻り値のボタンは Blazor.DynamicJSのヘルパJavaScriptで管理していてそのIDを返してます。
dynamic button = window.document.createElement("button");

//buttonを操作するときはそのIDとプロパティ名とかで呼び出す感じ
button.innerText = "new button";

//dynamic以外の型に変換するとシリアライズされて持ってこれる
string innerText = button.innerText;

引数

引数はもともとの IJSRuntime に渡せるもの(Jsonにできるものと ElementReference)と前述したJavaScriptオブジェクトを渡せます。

dynamic button = window.document.createElement("button");
dynamic div = window.document.getElementById("parent-div");
//引数にJavaScriptのオブジェクトを渡してます。
div.append(button);

実はFriendlyと同じ設計思想

Friendlyは別プロセスのメソッドをリフレクションで呼び出して、これはJavaScriptに対して同じ思想でやってるだけでした。オブジェクトの管理とかシリアライズとか引数のルールとかも実は同じです。

連載物です。

「ちょっとやってみるかなー」って軽く始めたけど結構ガッツリ実装ました。これは dynamic を使って緩く書く書き方なんですけど、DispatchProxy で型を決めて呼び出す方式も実装しちゃったんですよね。なんであと何回か書きます。内部のつくりとかも書けたら書きます。