Blazor から JavaScript ライブラリを利用する

ブラウザー内で C# のコードを実行できる Blazor ですが、DOM 要素を操作するには JavaScript による処理に頼る必要があります。

この記事では、Blazor における C# と JavaScript との相互運用 (JavaScript Interop) 、また同機能を用いた JavaScript ライブラリの利用方法について解説します。

**環境:**

- .NET 5
- Blazor WebAssembly

## JavaScript Interop の挙動を確認する

Blazor で JavaScript Interop を利用する際は、以下のステップが必要になります。

1. `/wwwroot` 配下に JavaScript モジュールを配置する
2. C# から JavaScript モジュールをインポートする
3. モジュールが公開している関数を C# から呼び出す

まず、呼び出される JavaScript モジュールを以下の通り定義します。

```js:wwwroot/js/interop-sample.js
export function outputLog(obj) {
   console.log(typeof obj, obj);
}
```

C# のソースコードから上記モジュールをインポートし、関数を呼び出します。まず、次の名前空間への参照を追加します。

```c#:InteropSample.razor
@inject IJSRuntime JSRuntime
```

これにより、以下の手順で JavaScript モジュールを参照できます。

```c#
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
   "import", "./js/interop-sample.js"
);

await module.InvokeVoidAsync("outputLog", "my first interop.");
```

`InvokeVoidAsync()` では、実行する関数名を第一引数で、関数へ渡す引数項目をそれ以降の引数で指定します。

これにより引数 `obj` へ渡した値の型 `string` とその内容 `"my first interop"` が、ブラウザーのコンソール上へ出力されます。

```log
string my first interop.
```

引数として渡せる値は JavaScript 向けに変換が行われます。変換結果の例としては以下の通りです。

```c#
await module.InvokeVoidAsync("outputLog", null);
await module.InvokeVoidAsync("outputLog", true);
await module.InvokeVoidAsync("outputLog", false);
// null は undefined へ、bool は boolean へそれぞれ変換される。
//   undefined undefined
//   boolean true
//   boolean false

await module.InvokeVoidAsync("outputLog", 123);
await module.InvokeVoidAsync("outputLog", "456");
await module.InvokeVoidAsync("outputLog", "foo");
// 数値を渡した場合は number へ、文字列を渡した場合は string へ変換される。
//   number 123
//   string 456
//   string foo

// enum MyEnum { Value1 = 7, Value2 = 5, Value3 = 6 }
await module.InvokeVoidAsync("outputLog", MyEnum.Value1);
await module.InvokeVoidAsync("outputLog", MyEnum.Value2);
await module.InvokeVoidAsync("outputLog", MyEnum.Value3);
// 列挙型のメンバーは、対応する整数値へ変換される。
//   number 7
//   number 5
//   number 6

var sampleList = new List<string>() { "aaa", "bbb", "ccc" };
await module.InvokeVoidAsync("outputLog", sampleList);
await module.InvokeVoidAsync("outputLog", sampleList as IEnumerable<string>);
// IEnumerable インターフェイスを備えた型であれば配列へ変換される。
//   object (3) ["aaa", "bbb", "ccc"]
//   object (3) ["aaa", "bbb", "ccc"]

// class SampleClass
// {
//    private string _X = "private field";
//    public string X = "public field";
//    private string _Y { get; set; } = "private property";
//    public string Y { get; set; } = "public property";
// }
var sampleInstance = new SampleClass();
await module.InvokeVoidAsync("outputLog", sampleInstance);
// クラスインスタンスは public プロパティのみを含んだ object へ変換される。
//   object {y: "public property"}

await module.InvokeVoidAsync("outputLog", new Dictionary<int, string>() {
    { 1, "value1" },
    { 2, "value2" },
    { 3, "value3" },
});
// Dictionary は object へ変換される。
//   object {1: "value1", 2: "value2", 3: "value3"}

await module.InvokeVoidAsync("outputLog", new
{
   Item1 = "foo",
   Item2 = "hoo",
   Item3 = 123
});
// 匿名型も object へ変換される。
//   object {item1: "foo", item2: "hoo", item3: 123}

var sampleTuple = (909, 703);
await module.InvokeVoidAsync("outputLog", sampleTuple);
await module.InvokeVoidAsync("outputLog", sampleTuple.ToString());
// Tuple は変換を行えない。
//   object {}
//   string (909, 703)

var sampleValueTuple = (x: 123, y: "sample");
await module.InvokeVoidAsync("outputLog", sampleValueTuple);
await module.InvokeVoidAsync("outputLog", sampleValueTuple.ToString());
// ValueTuple も同様。
//   object {}
//   string (123, sample)
```

この変換処理は以下の注意点があります。

- クラス インスタンスの持つプロパティ名は、先頭を小文字に変換される
- `Tuple` や `ValueTuple` といった型を持つ値は変換を行えない

`Tuple` が利用できないのは意外な気もしますが、JavaScript 側で対応するデータ構造がないことが理由なのかもしれません。

## JavaScript ライブラリを利用した処理をモジュールとして定義する

JavaScript Interop による関数の呼び出しと、関数へ渡した値が変換される際の挙動について確認しました。続いて、JavaScript ライブラリを用いて DOM を操作するパターンについて確認します。

例として、Chart.js を利用して画面上へグラフを出力するまでの流れを確認します。

Blazor のテンプレート プロジェクトでは、連日の気温を一覧表示する `FetchData.razor` というサンプル コンポーネントが含まれています（気温の数値自体はダミーデータとなっています）。これをもとに、気温の変化を日付順のグラフとして出力させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509844/42779c8b-8543-37c0-4e78-5534bc4a9df4.png)

まず、Chart.js のライブラリ本体をダウンロードします。

[インストール · Chart.js 日本語ドキュメント]:https://misc.0o0o.org/chartjs-doc-ja/getting-started/installation.html "インストール · Chart.js 日本語ドキュメント"

- [インストール · Chart.js 日本語ドキュメント]

これを `/wwwroot/js/lib` へ配置し、`/wwwroot/index.html` にライブラリへの参照を追加します。

```diff:wwwroot/index.html
	<head>
	   <meta charset="utf-8" />
	   ... 中略
	   <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
+	   <script src="js/lib/Chart.min.js"></script>
	</head>
```

`/wwwroot/js` 配下に、Chart.js を呼び出す以下のモジュールを定義します。

```js:chart-companion.js
export function createGraph(context, graph) {
   new Chart(context, graph)
}
```

Razor コンポーネント ファイル `FetchData.razor` にて `IJSRuntime` への参照と、グラフの出力先となる `canvas` 要素、要素への参照を格納する `ElementReference` 型のフィールドを追加します。

```diff:Pages/FetchData.razor
+	@inject IJSRuntime JSRuntime

	<h1>Weather forecast</h1>
	... 中略

	@if (forecasts == null)
	{
	   <p><em>Loading...</em></p>
	}
	else
	{
+	   <canvas @ref="graphCanvas"></canvas>
+	
	   <table class="table">
	      <thead>
	         <tr>
	    ... 中略
	}

	@code {
	   private WeatherForecast[] forecasts;
+	   private ElementReference graphCanvas;
```

上記 Razor コンポーネントに対するコードビハインドとして、`FetchData.razor.cs` を作成します。

```c#:Pages/FetchData.razor.cs
namespace BlazorSamples.Client.Pages
{
   public partial class FetchData { }
}
```

こちらに Chart.js へ渡すパラメータと対応するデータクラスを定義します。

```c#:Pages/FetchData.razor.cs
public partial class FetchData
{
   class LineData
   {
      public string Label { get; set; }
      public IEnumerable<int> Data { get; set; }
      public double Tension { get; set; } = 0.5;
      public string BorderColor { get; set; }
   }

   class LineGraphData
   {
      public IEnumerable<string> Labels { get; set; }
      public IEnumerable<LineData> Datasets { get; set; }
   }

   class LineGraph
   {
      public string Type { get; } = "line";
      public LineGraphData Data { get; set; }
   }
}
```

次に、これらデータクラスを用いてパラメータを構成する処理を追加します。

```c#:Pages/FetchData.razor.cs
protected async void CreateGraph()
{
   // WeatherForecastService から得られたデータをグラフ出力用データに変換する
   var temparetures = new LineGraphData()
   {
      // 各データの日付をグラフの軸とする
      Labels = forecasts.Select(f => f.Date.ToShortDateString()),

      // 摂氏／華氏それぞれの気温情報をグラフの要素として指定する
      Datasets = new List<LineData>()
      {
         new LineData()
         {
            Label = "Temp. (C)",
            Data = forecasts.Select(f => f.TemperatureC),
            BorderColor = "coral",
         },
         new LineData()
         {
            Label = "Temp. (F)",
            Data = forecasts.Select(f => f.TemperatureF),
            BorderColor = "lightgreen",
         }
      }
   };

   // コンパニオン モジュールへデータを渡す
   var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
      "import", "./js/chart-companion.js"
   );

   await module.InvokeVoidAsync(
      "createGraph",
      graphCanvas,
      new LineGraph() { Data = temparetures }
   );
}
```

上記関数の呼び出しを、ページの初期化処理中に追加します。

```diff:Pages/FetchData.razor
	protected override async Task OnInitializedAsync()
	{
	   forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
+	   CreateGraph();
	}
```

以上の変更により、画面上へ以下の通りグラフが出力されるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/509844/9263bbf7-f4af-8de1-b186-ce94812d290b.png)

ライブラリの関数を呼び出す JavaScript モジュールを用意することで、C# のコードから DOM を操作する手順について確認できたと思います。

ここでは `FetchData.razor` コンポーネントへ直接 `canvas` 要素や各種データクラスを追加しましたが、実際にはグラフを描画する Razor コンポーネントとして定義することが推奨されます。こちらについては別途取り上げたいと思います。

**参考:**
[ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す | Microsoft Docs]:https://docs.microsoft.com/ja-jp/aspnet/core/blazor/call-javascript-from-dotnet?view=aspnetcore-5.0 "ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す | Microsoft Docs"

- [ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す | Microsoft Docs]