ささいなことですが。

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

実はクラサバも作れます。Codeer.LowCode.Blazor

Codeer.LowCode.Blazorは、その名の通りBlazorアプリにローコード機能を組み込むためのライブラリです。BlazorはBlazorWebViewを使えばWPFやWinFormsで動かすことができます。つまり、Codeer.LowCode.Blazorを利用することで、WPFやWinFormsにローコード機能を組み込むことが可能です。さらに、WindowsアプリなのでUIだけでなく、DB操作ロジックも組み込めるため、クラサバアプリも作成できます。しかも無理やり組み込む感じではなくテンプレも用意したんで本当に簡単に作れます!

Web版の概要

Codeer.LowCode.Blazorは、ブラウザ側ではUIのロジックが、サーバー側ではDB操作のロジックが動作します。これらの間はWebApiで接続されています。WebApiの部分はライブラリ外ですが、テンプレートを使用して新規作成する際に自動で設定されます。

クラサバ版の概要

実際のところ、ライブラリ部分に大きな違いはありません。WebApiで接続するか、インプロセスのロジックで接続するかの違いだけです。これもテンプレートを使用して簡単に作成できます。

クラサバアプリにローコードを組み込んでみよう

クラサバアプリでよく聞くのは、基本部分は作っているけど、細かいカスタマイズはお客様ごとにハードコードで対応しているという話です。Codeer.LowCode.Blazorを使えば、そのハードコードの部分を大幅に工数削減できます。テンプレで生成したコードを元にあなたのアプリに組み込んでください。もしくは別プロセスにしてプロセス連携も良いと思います。

クラサバアプリからのリプレイスのつなぎに最適

Codeer.LowCode.Blazorは、WinFormsなどのクラサバアプリからWebアプリへのリプレースを支援することもメインの目的の一つです。ドラッグ&ドロップで画面を作成したり、大部分をデザイナーの設定やスクリプトで作成したりすることで、Blazorの機能を活用しつつスキルのアンマッチによる負担を軽減します。しかし、一気にリプレースするのは大変です。まずはWinFormsにCodeer.LowCode.Blazorを組み込み、少しずつ置き換えていきます。これにより、将来的にWebアプリに移行する際もそのまま活用できるのです。

お問合せはこちらから!

https://www.codeer.co.jp/LowCode
よろしくお願いします!

ちなみにトライアルは無料で使えます。テンプレからサクッと作れますので是非試してみてください。

Codeer.LowCode.Blazorをリリース!(してました

5/13にCodeer.LowCode.Blazorというライブラリをリリースしました!有料なんですけど評価版は無料で申請なく使えるんで使ってみてね。テンプレートも作っててサクッと試すことができます。
marketplace.visualstudio.com

ライセンスのお見積り、ご相談はこちらからお願いします!

実行エンジン型のローコード機能をBlazorアプリに組み込むライブラリです。

ローコードアプリって色々あるけどライブラリってたぶん世界初なんちゃうかな。以前に「CSパフォーマンス勉強会」でLTしたのが動画を撮ってくれてたんで参考までに(ありがとうございます!)4:48:40からですね、リンクそこにしてるけどいけるんかな。
www.youtube.com

  • うちのアプリにもローコード機能あったらなー
  • 大体は一般的なローコードアプリでいいけど、特別に組み込みたい機能あるんよね
  • お客さんから「既製品使ってよ、でもこれとあれとそれはできないと困る」って無理言われること多くなったよな
  • Webアプリ開発は苦手なんでとりあず大部分はローコードで作れたらいいのに、そしたらクラサバからWebアプリにリプレイスしやすいよね

とかありますよね、わかります。僕もそう思ったんで作りました。

テンプレで新規作成してビルドするとWebアプリとWPFのDesignerアプリができてそのDesingerで画面/機能を作っていくことができるのです!もちろんそれぞれ拡張可能となっています。

ノー/ローコードとプロコードのいいとこどり

一旦言葉の整理。Codeer.LowCode.Blazorでは以下の定義にしてます。(あくまでこの製品ではってことで

ノーコード デザイナで設定
ローコード デザイナでスクリプト実装
プロコード 普通に.NETやBlazorのコードで実装

大部分はデザイナの設定だけでサクッと作ってこだわりの部分はBlazorと.NETでお手軽にいつも通りに実装する。自分のアプリなんでスコープは小さいしめっちゃ融通効く。まさにこれが僕が実現したかったことなんです。

ノー/ローコードだけでも多くのプロジェクトは作れます。

かなり頑張りました!

ポトペタで画面作成

グリッドレイアウト、フローレイアウト、キャンバスレイアウトを組み合わせて画面が作れます。フィールドも標準的なものはそろってます。標準的でないやつは後で紹介するプロコードで増やしていけます。そういうのもサンプルコード的に順次公開していきます。

DBとの連携

一般的なCRUDはもちろんJOINや1Nの関係の表現も可能です。TableだけでなくViewにも関連付けることができるのでBI機能も簡単に実現。変更履歴も簡単に残せるようにしています。 その他検索/論理削除/楽観ロック/作成更新情報など一般的にDBの操作で必要になるものは取り揃えていて多くの既存システムのDBと連携できます。検索機能も充実しています。

身に覚えのあるスクリプトの書き味


イベントハンドラや同期的なダイアログの表示、UIの有効/無効、表示/非表示の制御など.NET開発者が一度は書いたことがあるであろう書き味で書けます。WebAPIも呼べるしエクセルの制御だってできちゃいます。もちろんこれもプロコードでAPIを増やしていくことができます。インテリセンスももちろん使えます(頑張った)。サンプルのようにC#っぽく書けるので、「プロコードでの実装と何が違うのですか?」って聞かれました。当然の疑問ですよね。以下のような感じで使ってもらうことを想定しています。

  • スクリプトなのでアプリのビルドなくデザイナだけで変更可能
  • デザイナで作ったModuleやFieldは基本スクリプトで制御
  • 簡単なことはこっちの方が書きやすい
  • デバッガは使えないので複雑なことは非推奨、難しいことはプロコードで公開したAPIに任せる

プロコードでの拡張

様々な方法で拡張できるようにしています。ライブラリでの組み込み型なのでそもそものアプリの部分はかなり自由に改造できます。

  • コードビハインド
  • 画面全体をrazorで実装
  • 画面の一部をrazorで実装
  • Fieldを実装してポトペタで配置できるようにする
  • スクリプトAPIを公開
  • そもそものアプリ部分を調整

詳細な機能もブログで紹介していきます。

いや、GitHubにもドキュメント作っていってるんですけどね。公式ドキュメントって堅苦しくなるし、考えすぎてなぜかわかりにくくなるんですよね・・・。(とは言え、そっちも頑張ります
場合によってはサンプル交えつつ雑記的に書いた方がわかりやすくなる場合もあるので、こっちも色々書いていきます。

だいたいなんでもExcelをPDFに変換する魔法

すみません、フリーレン風に言いたかっただけです。Nugetのライブラリです。
というわけで作りました。MITライセンスでサーバーでも、なんならWebAssemblyでブラウザ上でも使えます。
github.com

変換できるのは

  1. 文字(サイズ、太さ、色)
  2. セル(種類、色、マージ)
  3. 画像

これだけなんですけど、一般的に使う分にはこれだけいけたら十分じゃない?リクエストあればサクッと作れるものなら足していこうと思います。

GitHubのReadMeにも載せてますがこんな感じの変換ができます。

おまけ機能で簡単な文字置換ができます。これを使えば元ネタをExcelで作っておいて、それをデータに合わせて書き換えてPDFに変換ってことができます。

使い方なんですけど、Fontがちょっと面倒。これは内部的に使ってるPdfSharpの仕様です。Fontって勝手に再配布できないし、そもそもサイズが大きいのでライブラリには取り込めない。なので使う分だけ用意してもらう仕様です。とはいえ業務アプリで使う場合はそんなフォントこだわりないんじゃないかな?この例ではNotoSansで統一して太字だけ対応してます。用途に合わせて調整してみてください。

public class CustomFontResolver : IFontResolver
{
    public byte[] GetFont(string faceName)
        //Implement so that you can get as many fonts as you need.
        => faceName.EndsWith("#b") ? Resources.NotoSansJP_ExtraBold : Resources.NotoSansJP_Regular;

    public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
    {
        var faceName = familyName; 
        if (isBold) faceName += "#b";
        return new FontResolverInfo(faceName);
    }
}
GlobalFontSettings.FontResolver = new CustomFontResolver();

それが終わると後は変換するだけです。ファイルパスを渡すのと、Webアプリで使いやすいようにStreamを渡す版も作ってます。

using var outStream = ExcelConverter.ConvertToPdf(workbookPath, 1);
File.WriteAllBytes(pdfPath, outStream.ToArray());

作り方

ClosedXMLとOpenXmlで解析してPdfSharpで書いているだけです。だから意外と小規模なコードです。幅と高さの単位を合わせるのがめっちゃ大変だった・・・。そのへん@tmytに教えてもらいました。ありがとう!

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

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();
}

ルール

ここでインターフェイスを定義するとき問題がいくつかあって

  1. C#JavaScriptでは命名規則が違う場合がある
  2. 非同期どうするか
  3. 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 のままにしています。

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

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

年末のBlazorのアドベントカレンダーでHandsontableをBlazorで使う話が書かれてたのでやってみました。若干非効率ながら全部C#で書けました!コードはそれを参考にさせてもらいました。(てかそのJSをC#に書きなおさせてもらいました)
qiita.com

@page "/"
@using Blazor.DynamicJS
@inject IJSRuntime jsRuntime
<div @ref="_grid"></div>

@code {
    private ElementReference _grid;
    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; }
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (!firstRender) return;
        var js = await jsRuntime.CreateDymaicRuntimeAsync();

        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 = "*";

        //コンストラクタ引数をJavaScriptの世界につくる
        var arg = js.ToJS(new
        {
            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 { row = 0, col = 1 },
            outsideClickDeselects = true,
            manualColumnResize = true,
            fillHandle = false,
        });

        //コールバックで使うので先に宣言
        dynamic? hot = null;

        //オブジェクトの中にシリアライズできないメンバーは入れれないので別途ここで入れる
        arg.afterChange = (Action<dynamic, dynamic>)((changes, source) =>
        {
            if (source == "loadData") return;

            //これほんとは重いからやらない方がいい
            //changesの型を調べてシリアライズして持ってきた方がいい
            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(changes[0][0], 0, EDIT_MARK);
            }
        });

        hot = js.New("Handsontable", _grid, arg);

        var products = 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 = "袋井クラウンメロン" } }
        };

        hot.loadData(products);
    }
}

コールバックの中の処理がちょっと非効率。ループ回るたびにJSの世界とやり取りしてるんですよね・・・。これはchangesがシリアライズできたらいんですけどね、これ複数の型が入ってる配列だから今うまくシリアライズできないんですよねー。なんかいい方法考えよう。

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 で型を決めて呼び出す方式も実装しちゃったんですよね。なんであと何回か書きます。内部のつくりとかも書けたら書きます。

Selenium.Friendly.Blazor - その2

前回の続き
ishikawa-tatsuya.hatenablog.com

Selenium.Friendly.Blazor を作ったんですが、Windowsアプリ版と違って対象アプリで

  1. アセンブリを参照しないといけない
  2. App.razorに少し書き足さないといけない

という点がありました。仕方ないかなーと思ってたんですがtwitterで記事を見てくれた @jsakamoto が2の解決策を教えてくれました!

BINDING.assembly_load

BINDING.assembly_loadっていうのでJavaScriptからアセンブリを読み込めるようです。早速やってみました。BlazorAppFriendのコンストラクタに仕込んでみます。

public BlazorAppFriend(object webDriver)
{
    // ※アセンブリをロード!
    ((dynamic)webDriver).ExecuteScript("BINDING.assembly_load('Selenium.Friendly.Blazor');");

    ResourcesLocal.Initialize();
    _connector = new FriendlyConnectorCore(webDriver);
    ResourcesLocal.Install(this);
}

コンポーネントの検索

protected override void OnInitialized()
  => Selenium.Friendly.Blazor.BlazorController.Initialize(this);

App.razorでこれを書き足していたのは二つ理由があって

  1. アセンブリのロード
  2. App.razorの登録

2はコンポーネントを検索するためのルートのコンポーネントが必要だったので付けてました。何となくどこかにstaticであるような気はしたのですが、どうせ1が解決できてないしなーってことで調査を保留にしてたのですが解決したので探してみました。(そして見つけた。やった!
github.com
Microsoft.AspNetCore.Components.WebAssembly.Rendering.RendererRegistry

private static readonly Dictionary? _renderers
というフィールドを持っていてそこにApp以下Componentが入っていました(今は)

で、これをコンポーネント検索のところで使うとApp.razorの登録なしで検索することができました。
github.com

あとは Assembly を参照せずに読み込ますことができれば完璧!

なんだけど、そんなのできるんかな?
誰かいいアイデアあったら教えてください。