「ASP.NET Core Blazor と Chart.js で入門するWebアプリ作成」の3回目。各回記事の内容は以下のようになります。
導入編:サンプルを動かす
探究編:Blazorの仕組みを理解する
実践編:Chart.jsでグラフを描く(当記事)
Tips編:BlazorやChart.jsのあれこれ
番外編:Ubuntuサーバで公開する
今回の内容は、第1回「導入編」の「作業の流れ」で提示した項目のうち
- サンプルソースから余計なものを削除してシンプルなアプリに変える
- Chart.js の導入
- サーバ側からブラウザ側のJavaScript関数を呼び出す
 です。
今回で Chart.js を使った独自アプリを動かすところまで行きます。
2020/10/23 追記
System.Text.Json.JsonSerializer によって Newtonsoft.Json.JsonConvert.SerializeObject の機能(NullValueHandling.Ignore)が置換できることが判明したので、System.Text.Json.JsonSerializer を使うように変更しました。これに伴い、「ChartJson クラスの用意」に記載の ChartJson クラスの Extension の記述が変更になっています。なお、Newtonsoft.Json のインストールの節は、 NuGet の使い方の例として残しておきます。
サンプルソースをシンプルな作りに変える
前回記事「探究編」で Blazor アプリの構成要素はほぼ理解できたので、今回は導入編で作成したサンプルプロジェクトから不要なものを削除して、いったんシンプルなアプリを作成してみます。その後、Chart.js などいくつかの機能を追加します。
サンプルプロジェクトを作成していない方やすでにいろいろ改造を加えてしまった方は、導入編の「Blazor フレームワークを用いたサンプルプロジェクトの作成と実行」を参照して、新規にプロジェクトを作成してください。
不要な Razor ファイルの削除
まずは「Weather forecast」画面だけを表示するシンプルなアプリに作りかえます。Home ページと Counter ページは使わないので当該ページの Razor ファイルは削除してしまって構いません。具体的には
- Pages/Counter.Razor
- Pages/Index.Razor
- Shared/NavMenu.razor
- Shared/SurveyPrompt.razor
です[^del_cs]。残しておいても問題ないですが、すっきりさせたい方は VS や VS Code でファイルを選択して削除してしまいましょう。もちろん、コンソールを使って del なり rm なりしても構いません。
[^del_cs]: 前回記事で Counter.razor.cs ファイルを作成した場合は、それも不要ファイルになります。
不要ファイルを削除した場合、残る主要ファイルは以下のようになります[^remain_cs]。

[^remain_cs]: 前回記事で FetchData.razor.cs ファイルを作成した場合は、それも残ります。
MainLayout.razor の修正
左側のサイドメニューを構成していた NavMenu.razor が不要になったので、Shared/MainLayout.razor を修正して、 @Body だけを残します。
<!-- MainLayout.razor -->
@inherits LayoutComponentBase
<div class="main">
    <div class="content px-4">
        @Body
    </div>
</div>FetchData.razor の修正
Pages/FetchData.razor を修正してルートページにします。先頭の @page のパスを "/"[^root_path] に修正してください。
[^root_path]: このように MARKDOWN_HASHe575a9f9828cd0f61f1106bf64db9f1dMARKDOWNHASH となっていれば、どんなファイル名の .razor_ ファイルでもルートページになれます。
<!-- Pages/FetchData.razor/先頭部 -->
@page "/"もし、 Pages/Index.razor が残っている場合は、その @page のパスを "/" 以外に変更しておいてください。
ビルドしてみる
VS であればメニューからビルドできますが、VS Code の場合はコンソールから dotnet build でビルドします[^dotnet_build]。

[^dotnet_build]: Ctrl + Shift + B でビルドを実行することもできます。VS Code の場合は Terminal が開いてそこで dotnet build が実行されます。
ビルドに成功しました。VS (Code) に戻って F5 で実行してみます。

意図通り、FetchData画面だけが表示されています。確認したらブラウザを閉じます。VS Code の場合はデバッグ実行も停止してください。
次に、別の Razor コンポーネントを用意し、 FetchData.razor からはそのコンポーネントを呼び出すようにしてみます。
Shared/Chart.razor ファイル[^other-dir]を作成し、以下の内容で置き換えてください。
[^other-dir]: Shared 以外の別の名前のフォルダを作成して、そこに Chart.razor ファイルを作ることもできます。その場合は、 _Imports.razor に @using MyBlazorApp.{FOLDER_NAME} を追加してください。
<!-- Chart.razor -->
<h1>Chartの世界にようこそ!</h1>
ここには Chart.js を使ったグラフが表示されます。イメージは次のようになります。

MARKDOWN_HASH22795336ef07424b04a11ecfcf1c2e23MARKDOWNHASH命令がない .razor_ ファイルは部品になる^blazor_buhinんでしたね。FetchData.razor を修正して Chart コンポーネントを参照するようにします。下記2行以外は全て削除してください。(FetchData.razor.cs がある場合はファイルを削除してください)
<!-- FetchData.razor -->
@page "/"
<Chart />イメージは次のようになります。

修正をすべて保存したら F5 で実行します。

OKです。前回記事でがんばって Blazor の仕組みを(ある程度ですが)理解した甲斐がありました。以下では、この Chart コンポーネントにグラフを表示させていくことにします[^chart_razor_1]。
[^chart_razor_1]: 今回はあえて Chart.razor という別のコンポーネントを作成しましたが、FetchData.razor に実装しても構いません。
Chart.js の導入
プロジェクトに Chart.js を導入します。これは静的なファイルなので配置場所は wwwroot配下になります。
Visual Studio での Chart.js の導入
まず Chart.js を配置するためのフォルダを作成しておきます[^mkdir_lib]。ソリューションエクスプローラで wwwroot を右クリックし、「追加」>「新しいフォルダー」と選択していきます。
[^mkdir_lib]: コンソール操作が得意な方は、mkdir wwwroot/lib で作成しても構いません。

フォルダ名は何でもよいのですが、今回は "lib" にしておきます。

lib フォルダに chart.js を導入します。lib を右クリックし、「追加」>「クライアント側のライブラリ」と選択していきます。

下図のような画面が表示されるので、ライブラリ名に「Chart.js」と入力して、表示された候補から「Chart.js」を選択します。

以下のような画面に変わります。

「インストール」をクリックして完了です。wwwroot/lib の下に Chart.js フォルダが作成されて、その下に .css や .js が置かれているのを確認してください。

VS Code での Chart.js の導入
npm を使える方はそちらでインストールするのが最も簡単だと思います。
Shell を使ってインストールする場合は、VS での導入のときにもちらっと名前が出ていますが cdnjs というサイトからファイルを個別にダウンロードします。最新版のダウンロードURLについては、cdnjs で確認してください。
Power Shell を使う場合
VS Code のメニューから「Terminal」>「New Terminal」と選択してください。画面下部に Power Shell が開きます。プロジェクトフォルダにいることを確認したら、下記のコマンド列をまとめてコピペで実行してください[^ps-foreach]。(VSに合わせたフォルダ構成[^chart_js_dir]にしています)
[^ps-foreach]: 本来は foreach を使べきだと思いますが、筆者は Power Shell に詳しくないので、このような形に・・・。とりあえずはコマンド全体をTERMINALにコピペして実行してください。
[^chart_js_dir]: 必ずしもこのようなフォルダ構成にする必要はないのですが、説明の都合上、 VS 場合に合わせています。
# ~\Dev\dotnet\MyBlazorApp
mkdir -p wwwroot/lib/Chart.js
cd wwwroot/lib/Chart.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.js -OutFile Chart.min.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.js -OutFile Chart.bundle.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.bundle.min.js -OutFile Chart.bundle.min.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.css -OutFile Chart.css
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.js -OutFile Chart.js
Invoke-WebRequest -UseBasicParsing https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.min.css -OutFile Chart.min.css
Git Bash を使う場合
Git Bash を使える場合は、下記を実行してください。
# ~/Dev/dotnet/MyBlazorApp
mkdir -p wwwroot/lib/Chart.js
cd wwwroot/lib/Chart.js
for x in css min.css js min.js bundle.js bundle.min.js; do
    curl https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.9.3/Chart.$x --output Chart.$x
done実行が終わったら、 VS Code の Explorer で lib/Chart.js 配下を確認しておいてください。

Chart.js を参照する
_Host.cshtml を修正して Chart.css と Chart.min.js を参照する
Pages/_Host.cshtml を開き、以下のように <head> の末尾に <link rel="stylesheet" href="lib/Chart.js/Chart.css" /> と挿入し、 Chart.css が参照できるようにします。

さらに <body> の末尾に <script src="lib/Chart.js/Chart.min.js" asp-append-version="true"></script> と挿入し、ルートページにアクセスした際に Chart.js の本体を読み込むようにします。

Chartの canvas を用意する
グラフが描画される場所(canvas)を用意します。Shared/Chart.razor を開き、下記コードを追加してください。
<!-- Chart.razor -->
<div class="chart-container" style="position: relative; height:400px; width:600px">
    <canvas id="myChart"></canvas>
</div>canvas はHTMLの既存要素で、以下のようなものです。

Chart.js は、このcanvasにグラフを描画するわけです。その際、canvas 要素に付与したidを参照します。上記ではmyChart という id を設定しています。
Chart() を呼び出す関数を用意する
ここで初めて JavaScript のコードが登場します。そして JavaScript が必要になるのはここだけとなります。
_Host.cshtml を開き、<body> ブロックの末尾に次のコードを挿入してください。
// _Host.cshtml
    <script>
        function renderChart(chartJson) {
            if (window.chartObj) {
                console.log('destroy chartObj');
                window.chartObj.destroy();
            }
            console.log("json=" + chartJson);
            var ctx = document.getElementById('myChart');
            window.chartObj = new Chart(ctx, JSON.parse(chartJson));
        };
    </script>画像では以下のようになります。

ここでは renderChart() という関数を定義し、そこから Chart.js が提供する Chart() を呼んでいます。
renderChart() は C# から呼び出されることを想定しており、引数として JSONオブジェクトを文字列化したデータを受け取ります。それを parse してJSONオブジェクトに戻したものを Chart() に渡します。
Chart() は、canvas 要素( Chart.razor で定義した、 id が myChart となっている canvas)とグラフデータを表すJSONオブジェクトを引数に取ります。new Chart(・・・) するだけでグラフは描かれるのですが、複数回呼び出されたときに前回作成したグラフを破棄するために chartObj という変数に保存しています。
テスト描画する
グラフ描画に必要な JavaScript はこれだけでよいのですが、一応、テスト用の JavaScript も追加してテストしてみましょう。以下のコード[^stringify]をさらに <body> ブロックの末尾に挿入してください(コピペしましょう)。
[^stringify]: Chart()関数は JSON オブジェクトを受け取るのですが、それを呼び出している renderChart() が C# から呼び出される都合上、JSONオブジェクトを文字列に変換して渡しています。
    テストコードの内容(クリックして開いてください)
        
            
        
    
Chartコンポーネントにこのテスト関数を呼び出すボタンを追加します。
<!-- Chart.razor -->
<button class="btn btn-primary btn-sm py-0" onclick="renderChartTest()">Click me</button>最終的に Chart.razor ファイルの内容は以下のようなイメージになります。

ここまで準備ができたら、F5 を押してデバッグ実行してみます。次のように「Click me」というボタンが表示されます。

このボタンをクリックします。問題がなければ次のようなグラフが描画されるはずです。

ボタンをクリックするたびにグラフがアニメーション付きで再描画されます。ここでブラウザで F12 を押してください。開発画面が開くのでコンソールをクリックします。以下のように、renderChart() 関数に記述した console.log() によりログが出力されているのが分かると思います。

ボタンを押して何も起きなかった場合も、ブラウザで F12 を押して開発画面を開いてください。コンソールに何かエラーが表示されていると思います。以下は実際に筆者の環境で発生したエラー画面です。

この情報を見て、がんばってエラーをつぶしましょう。
サーバ側からJavaScript関数を呼び出す
先ほどのテストでは、JavaScript を使ってブラウザ側でグラフデータを用意し、renderChart() を呼んでいました。つまり、サーバ側は一切かかわっていませんでした。
ここからが本題です。Chartコンポーネントで「Click me」ボタンが押されたら、サーバ側でグラフデータを作成し、それを引数にしてブラウザ側の JavaScript 関数 renderChart() を呼ぶようにしてみましょう。
Newtonsoft.Json のインストール
注: 冒頭の「追記」のところでも触れていますが、Newtonsoft.Json は使わなくなりました。したがってこの節は飛ばして次の「ChartJson クラスの用意」に移ってください。本節は NuGet の使い方の例として残しておきます。
この後で Chart.js 用のグラフデータとなるJSON構造を表す ChartJson という C# のクラスを用意します。
ChartJson データは C# クラスのインスタンスなので、そのままでは Chart() の引数に渡せません。いったん文字列にシリアライズしてから renderChart()関数に渡し、さらにそこで JSON データに戻してから Chart() に渡すことにします。
C# クラスのインスタンスを JSON 構造の文字列にシリアライズしてくれる Newtonsoft.Json というパッケージがあるので、NuGet を使ってインストールしておきます。
Visual Studio の場合
下図のように「依存関係」のところで右クリックし、

NuGet画面が開いたら、「参照」のところで "Newtonsoft.Json" と入力します。

表示された "Newtonsoft.Josn" を選択し、「インストール」をクリックしてインストールしてください。
VS Code の場合
まず VS Code に NuGet パッケージマネージャをインストールする必要があります。未インストールの方は、Extensions で "nuget" を検索し、"NuGet Package Manager" をインストールしてください。

NuGet がインストールできたら、 Explorer で MyBlazorApp.csproj を開き、そこで右クリックします。

「Command Palette...」を選択します。

入力窓が開くので、"Add Package" を入力し Enter を押します。

新しく開いた入力窓で "Newtonsoft.Json" と入力し Enter を押します。

候補が表示されるので、先頭の "Newtonsoft.Json" を選択します。

バージョンを聞かれるので先頭の最新版を選択します。

.csproj にパッケージ参照が挿入されました。保存すると「restore するか」と聞かれるかもしれません。

その場合は「Restore」をクリックしてください。また dotnet build を実行しておいてください。
ChartJson クラスの用意
Chart.js 用のグラフデータとなるJSON構造を表すクラスを用意しておきます。Data[^models_1] フォルダの下に ChartJson.cs というファイルを作成し、以下の内容で書き換えてください。
[^models_1]: MVCの観点からは Models というフォルダを作成して、その下に配置すべきかもしれません。
_ChartJson.cs_ の内容(クリックして開いてください)
// Data/ChartJson.cs
using System;
using System.Linq;
using System.Text.Json;
namespace MyBlazorApp.Data
{
    /// 
    /// Chart.js の Chart() 関数に渡す JSONデータを表すクラス
    /// 
    public class ChartJson
    {
        public string type { get; set; }
        public Data data { get; set; }
        public Options options { get; set; }
        public ChartJson RoundData()
        {
            if (data != null) data.RoundData();
            return this;
        }
    }
    public class Data
    {
        public string[] labels { get; set; }
        public Dataset[] datasets { get; set; }
        public void RoundData()
        {
            if (datasets != null) {
                foreach (var dataset in datasets) {
                    dataset.RoundData();
                }
            }
        }
    }
    public class Dataset
    {
        public string label { get; set; }
        ///  double に解釈されるべきデータ 
        public double?[] data { get; set; }
        public string backgroundColor { get; set; }
        public string borderColor { get; set; }
        public int order { get; set; } = 1;
        public string type { get; set; } = "bar";
        public bool fill { get; set; } = false;
        public int radius { get; set; } = 3;
        public double[] borderDash { get; set; }
        public string yAxisID { get; set; } = "y-1";
        public static Dataset CreateBar(string label, double?[] data, string color)
        {
            return new Dataset { label = label, data = data, borderColor = color, backgroundColor = color, order = 3 };
        }
        public static Dataset CreateLine(string label, double?[] data, string bdColor, string bgColor, string yAxis = "y-1")
        {
            return new Dataset { label = label, data = data, borderColor = bdColor, backgroundColor = bgColor, order = 2, type = "line", yAxisID = yAxis };
        }
        public static Dataset CreateLine2(string label, double?[] data, string bdColor, string bgColor)
        {
            return CreateLine(label, data, bdColor, bgColor, "y-2");
        }
        private static double[] _dashParam = new double[] { 10, 3 };
        public static Dataset CreateDashLine(string label, double?[] data, string color)
        {
            return new Dataset { label = label, data = data, borderColor = color, backgroundColor = color, order = 2, type = "line", radius = 0, borderDash = _dashParam };
        }
        public static Dataset CreateDataset(string type, string label, double?[] data, string color, string bgColor, int order)
        {
            return new Dataset { type = type, label = label, data = data, borderColor = color, backgroundColor = bgColor, order = order };
        }
        public void RoundData()
        {
            if (data != null) {
                data = data.Select(x => x._round(3)).ToArray();
            }
        }
    }
    public class Options
    {
        public int AnimationDuration {
            get { return animation.duration; }
            set { animation.duration = value; }
        }
        public bool maintainAspectRatio { get; set; } = false;
        public Animation animation { get; set; } = new Animation();
        public Scales scales { get; set; }
        public static Options Plain()
        {
            return new Options {
                scales = new Scales {
                    yAxes = new[] {
                        new Yaxis{
                            ticks = new Ticks()
                        }
                    }
                }
            };
        }
        public static Options CreateTwoAxes(Ticks leftTicks = null, Ticks rightTicks = null)
        {
            return new Options {
                scales = new Scales {
                    yAxes = new[] {
                            new Yaxis{ id = "y-1", position = "left", ticks = leftTicks ?? new Ticks() },
                            new Yaxis{ id = "y-2", position = "right", ticks = rightTicks ?? new Ticks() },
                    }
                }
            };
        }
    }
    public class Animation
    {
        public int duration { get; set; } = 1000;
    }
    public class Scales
    {
        public Yaxis[] yAxes { get; set; }
    }
    public class Yaxis
    {
        public string id { get; set; } = "y-1";
        public string position { get; set; } = "left";
        public Ticks ticks { get; set; }
    }
    public class Ticks
    {
        public bool beginAtZero { get; set; } = true;
        public double? min { get; set; }
        public double? max { get; set; }
        public double? stepSize { get; set; }
        public Ticks(double? maxVal = null, double? stepVal = null, double? minVal = null)
        {
            if (maxVal.HasValue) {
                max = maxVal.Value;
                min = minVal.HasValue ? minVal.Value : 0;
                stepSize = stepVal.HasValue ? stepVal.Value : maxVal.Value / 10;
            }
        }
    }
    /// 
    /// ヘルパー拡張メソッド
    /// 
    public static class Extensions
    {
        public static double? _round(this double? d, int digits = 0)
        {
            return d.HasValue ? Math.Round(d.Value, digits) : d;
        }
        private static JsonSerializerOptions SerializerOpt = new JsonSerializerOptions { IgnoreNullValues = true };
        public static string _toString(this ChartJson json)
        {
            return json.RoundData()._jsonSerialize();
        }
        public static string _jsonSerialize<T>(this T json)
        {
            return json == null ? "" : JsonSerializer.Serialize<T>(json, SerializerOpt);
        }
        public static T _jsonDeserialize<T>(this string jsonStr)
        {
            return JsonSerializer.Deserialize<T>(jsonStr);
        }
    }
}
ChartJson クラスは、Chart() に与える JSONデータの構造に合わせたクラス構成[^absent-value]になっています。このクラスには _toString() という拡張メソッドを用意してあります。_toString() を呼び出すと ChartJson インスタンスを JSON文字列に変換することができます[^serializeObject]。
[^absent-value]: datasets[].data が double?[] という null許容型になっているのは、欠損値を null として扱えるようにするためです。次回記事で説明予定。
[^serializeObject]: _toString() で使われている System.Text.Json.JsonSerializer.Serialize() は、引数として IgnoreNullValues = true を指定しています。この場合、 double? のような Nullable型の値が null だったときには、その属性そのものが出力されなくなります。たとえば Ticks クラスはデフォルトで beginAtZero だけが出力されることになります。
ChartJson データを返す関数を用意する
Data/WeatherForecastService.cs を開き、 WeatherForecastService クラスの中味(9行目~23行目)を以下の内容で置き換えます^WeatherForecastService。
WeatherForecastService クラスの内容(クリックして開いてください)
// Data/WeatherForecastService.cs
        public Task<ChartJson> GetForecastAsync(DateTime startDate)
        {
            var rng = new Random();
            return Task.Run(() => {
                var forecast = getForecast(startDate);
                return new ChartJson {
                    type = "bar",
                    data = new Data {
                        labels = forecast.Select(x => x.Date.ToString("MM月dd日")).ToArray(),
                        datasets = new Dataset[] {
                                Dataset.CreateBar("摂氏", forecast.Select(x => (double?)x.TemperatureC).ToArray() , "royalblue"),
                                Dataset.CreateBar("華氏", forecast.Select(x => (double?)x.TemperatureF).ToArray(), "darkblue"),
                            },
                    },
                    options = Options.Plain(),
                };
            });
        }
        private WeatherForecast[] getForecast(DateTime startDate)
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast {
                Date = startDate.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
            }).ToArray();
        }
いったんここで dotnet build を実行し、ビルドに成功することを確認しておいてください。
Chart.razor を修正してグラフ描画
ここまででお膳立ては整ったので、ボタンがクリックされたらサーバ側のコードを実行し、返ってきたJSONデータでグラフ描画をしてみましょう。Chart.razor を次のように修正してください。
まず次のコードを追加します。
<!-- Shared/Chart.razor -->
@using MyBlazorApp.Data
@inject WeatherForecastService ForecastService
@inject IJSRuntime JSRuntime
@code{
    public async Task RenderNewChart()
    {
        var json = await ForecastService.GetForecastAsync(DateTime.Now);
        var jsonStr = json._toString();
        await JSRuntime.InvokeAsync<string>("renderChart", jsonStr);
    }
}ボタンがクリックされたら、ここで定義されたメソッド RenderNewChart を呼ぶように修正します。
<!-- Shared/Chart.razor -->
<button class="btn btn-primary btn-sm py-0" @onclick="RenderNewChart">Click me</button>修正後のイメージは下図のようになります。

F5 を押して実行してみましょう。「Click me」ボタンをクリックして、次のようなグラフが描画されれば成功です。

このグラフは、Excelのグラフでいうところの「集合縦棒」グラフになっています。一つの日付のところに2本の棒グラフが立っていて、左側がセ氏、右側がカ氏の温度を示しています。
RenderNewChart() の説明
Chart.razor に追加した部分を再掲します。

11行目: _Data_フォルダにあるクラスを参照します。この MARKDOWN_HASHc12184c8e1b3128d5352b93a9adff515MARKDOWNHASH を _Imports.razor_ に入れてしまえば、ここでの @using 宣言は不要になります。
12行目: 元々 Startup.cs の ConfigureServices() で services.AddSingleton<WeatherForecastService>() しているので、このように @inject によりシステム側で生成したシングルトンインスタンスを注入してもらうことができます。
13行目: IJSRuntime JSRuntime をInjectしています。このオブジェクトを使うとサーバ側のC#コードからブラウザ側の JavaScript を呼び出せるようになります。
15行目: ここから RenderNewChart() メソッドの定義となります。ブラウザで「Click me」をクリックすると、サーバ側のこのメソッドが呼び出されることになります。
17行目: Injectされた WeatherForecastService インスタンスの GetForecastAsync() を呼び出して ChartJsonデータを作成して返してもらいます。
18行目: ChartJson クラスの拡張メソッド MARKDOWN_HASH8840bfb0e1b8024bbb9d4546ef118c5cMARKDOWNHASH により、ChartJson データをJSON文字列に変換します。
19行目: 変換されたJSON文字列を引数として、 _Host.cshtml_ で定義した JavaScript関数 renderChart() を呼び出します。IJSRuntime や InvokeAsync については次回記事で説明します。
今回の記事はここまでとなります。次回は Blazor や Chart.js のいろいろな Tips 的な話を取り上げたいと思います。
 
          