Morning Girl

Web API, Windows, C#, .NET, Dynamics 365/CRM etc..

Blazor で API Explorer を作って色々悩んだお話 #GyutanKaigi2019

先週土曜日に仙台で開催した「牛タン会議2019」でBlazorについての発表をしてきました! 今回はその内容をもうちょっとまとめて、Blogとして公開したものになります。

speakerdeck.com

vsuc.connpass.com

ちなみに、この記事は Blazor アドベントカレンダー 12日目です。めちゃくちゃ遅くなってごめんなさい・・・orz

qiita.com

こんなものを作ってみた

実は私、ちょうど2ヶ月ほど前から Blazor を触り始めたんですが、なんて素晴らしい!  JavaScriptフレームワークが苦手な私でもこれならなんか作って公開できそうだ!(思い違い) と感激していました。

そこで、当時開発でよく悩んでいたスマレジのAPIを手軽に実行できるアプリを作ってみたのです。

f:id:sugimomoto:20191217212509p:plain

スマレジ API はすごく柔軟な機能を提供していて、数多くの操作、リソースをコントロールできるのですが

f:id:sugimomoto:20191217212518p:plain

API リクエストの仕様がちょっと独特かつ、項目が多いので気軽に試すのが難しいかなーというAPIでした。

f:id:sugimomoto:20191217212525p:plain

なので、FacebookMicrosoft が公開しているような API Explorer を作って公開しちゃえば、このつらみから開放されるのではないか!? と考えたのです。

そして作ってみたのがこんな感じのものです。

https://cdatajbuilds.s3-ap-northeast-1.amazonaws.com/sugimototest/smaregi.gif

まだ手直し真っ只中ですが、以下のAzure Web Apps で公開しています。(低料金プランとSignalIR未設定のためか、ちょっと重い・・・)

https://smaregiapiexplorer.azurewebsites.net/

さて、なんとか自分がイメージしていたものが動くところまでは至ったのですが、そこで色々とぶつかっていたので、それを共有したいと思います。

私が遭遇した課題 その1

このAPI Explorer を実装する上で、一番の課題だったところは、一番の機能的ポイントでもある、動的なテーブル形式のInputフォームでした。

f:id:sugimomoto:20191217212534p:plain

スマレジ APIは各カラムのフィルターを柔軟に書けるようになっている反面、この部分のリクエストをJSONで書くのがなかなか大変だったので、この部分の実現が一番の肝でした。

ただ、この動的なInputフォーム動的であるが故に、Blazorの強力なバインディング機能でプロパティに値をバインディングできない(できないよね? できたら教えて下さい)!

バインディングできないと、値のとり方がわかんない!

私の試みた解決策 その1

まあ、しょうがない。それじゃあDOM APIにアクセスして、動的に取ってくればいいんでしょ? と考えたので Blazor で DOM APIにアクセスするライブラリはなーんだ? と探したわけですが

FAQ · aspnet/Blazor Wiki · GitHub

Q: Can I access the DOM from a Blazor app?

You can access the DOM through JavaScript interop from .NET code. However, Blazor is a component based framework that minimizes the need to access the DOM directly.

(あなたはBlazor で DOM にアクセスできるよ! そう、JavaScript ならね!:意訳)

JavaScriptか・・・!

Blazor では JavaScript 相互運用機能(Interop)という、.NET MethodからJavaScriptを呼び出す機能が備わっており、この機能でJavaScriptが取得したDOM APIのデータをJSONなどの形式にまとめて、レスポンスとして .NET 側に返す機能が備わっています。

docs.microsoft.com

できる限り、JavaScriptを書きたくなかった私ですが、ここで諦めてHTMLテーブルのInputをなめて、JSONを返すJavaScriptMethodを書き、対応することにしました。

私が遭遇した課題 その2

しかし、ここでも躓きます。JavaScriptでDOM APIにアクセスして、テーブルの情報を取得することはできました。

ただ、Blazorは各Inputやバインディングの値が変更された際に、DOM要素をレンダリングし直します。

docs.microsoft.com

その結果、JavaScript 相互運用で非同期処理をしていることにより、リクエスト生成ボタンをクリックする(JavaScript が DOM の値を取得する)前に、Blazor が DOMを初期状態に書き換えてしまい、値が取得できないということが発生。

取得できていても非同期処理がレンダリングのタイミングがずれて、裏の変数では値を保持しているのに、レンダリングしている値と一致しないということまで発生しました。

私の試みた解決策 その2

結局私が最終的に試みたアプローチは、テーブルInputが変更される度に JavaScriptで値を取得して、テーブルを構成するためのListプロパティに逐次差し戻し、最新化するというものでした。

以下の「columnInput」がテーブルを構成するための要素をすべて保持しており、

                <BSTable IsSmall="true" IsBordered="true" IsStriped="true" Class="overflow-auto" style="max-height: 200px;">
                    <BSTableHead>
                        <BSTableRow>
                            <BSTableHeadCell>Select</BSTableHeadCell>
                            <BSTableHeadCell>Id</BSTableHeadCell>
                            <BSTableHeadCell>ColumnName</BSTableHeadCell>
                            <BSTableHeadCell>ColumnName(J)</BSTableHeadCell>
                            <BSTableHeadCell>ColumnType</BSTableHeadCell>
                            <BSTableHeadCell>ConditionType</BSTableHeadCell>
                            <BSTableHeadCell>ConditionValue</BSTableHeadCell>
                            <BSTableHeadCell>Description</BSTableHeadCell>
                        </BSTableRow>
                    </BSTableHead>
                    <BSTableBody>
                        @foreach (var column in columnInput)
                            {
                        <BSTableRow>
                            <BSTableCell>
                                <BSBasicInput Class="form-control-sm" InputType="InputType.Checkbox" Value="@column.Select"></BSBasicInput>
                            </BSTableCell>
                            <BSTableCell>@column.No</BSTableCell>
                            <BSTableCell>@column.Name</BSTableCell>
                            <BSTableCell>@column.JapaneseLabel</BSTableCell>
                            <BSTableCell>@column.Type</BSTableCell>
                            <BSTableCell>
                                <BSBasicInput Class="form-control-sm" InputType="InputType.Select" Value="@column.ConditionType" @onselect="@SetColumnInput">
                                    <option></option>
                                    <option>=</option>
                                    <option>like</option>
                                    <option>&lt;</option>
                                    <option>&lt;=</option>
                                    <option>&gt;</option>
                                    <option>&gt;=</option>
                                </BSBasicInput>
                            </BSTableCell>
                            <BSTableCell><BSBasicInput Class="form-control-sm" InputType="InputType.Text" Value="@column.ConditionValue" @onchange="@SetColumnInput"></BSBasicInput></BSTableCell>
                            <BSTableCell>@column.Description</BSTableCell>
                        </BSTableRow>
                            }

                        @if (columnInput.Count == 0)
                            {
                        <tr>
                            <td colspan="8">No Records</td>
                        </tr>
                            }
                    </BSTableBody>

                </BSTable>

SelectボックスなどのInputが操作されるたびに、JavaScriptが値を取り直しています。

        public async void SetColumnInput()
        {
            columnInput = await JSRuntime.InvokeAsync<List<ColumnInput>>("tableDataManager.getTableValues");
        }

これにより、テーブルを構成しているオブジェクトの値を最新に保つようにしています。

Blazor でアプリを作る上で感じたポイント

と、ここまで私の課題と解決アプローチをお話してきたのですが、これが「まっとうな」「ベスト」といえるアプローチに行き着くことができていない、というのが正直なところです。

なので、この発表をした牛タン会議では、最終的に会場に居る素晴らしいMS MVP の方々へ「どうしたらよかっただろう?」というのを聞く! というが本来の目的だったのですが

f:id:sugimomoto:20191217212547p:plain

ちょっと会場の時間が押していたこともあり、なくなく断念となりました(泣

というわけで、せっかくなので、このBlogでもヒドイコードを晒して対応アプローチを募ってみたい、と思っています。

github.com

ちなみに、私がこの経験を通じて得た教訓は「できる限り値はバインディングするに限る。直接的なバインディングなのか、間接的なバインディングに問わず」というものでした。

(こんなBlogでいいのだろうか。)

おしまい