ASP.NET Core Blazor で .NET メソッドから JavaScript 関数を呼び出す

この記事では、.NET から JavaScript (JS) 関数を呼び出す方法について説明します。 JS から .NET メソッドを呼び出す方法については、「ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す」を参照してください。

.NET から JS を呼び出すには、IJSRuntime 抽象化を挿入し、次のいずれかのメソッドを呼び出します。

JS 関数を呼び出す上記の .NET メソッドについて:

  • 関数の識別子 (String) は、グローバル スコープ (window) を基準とするものです。 window.someScope.someFunction を呼び出す場合の識別子は someScope.someFunction です。 関数は、呼び出す前に登録する必要はありません。
  • Object[] で任意の数の JSON シリアル化可能な引数を JS 関数に渡します。
  • キャンセル トークン (CancellationToken) により、操作を取り消す必要があることを示す通知が伝達されます。
  • TimeSpan は、JS 操作の時間制限を表します。
  • また、戻り値の型 TValue も JSON シリアル化可能である必要があります。 TValue は、返される JSON 型に最適に対応する .NET 型と一致する必要があります。
  • InvokeAsync メソッドに対しては JS Promise が返されます。 InvokeAsync によって Promise のラップが解除され、Promise によって待機されている値が返されます。

Blazor Server アプリでプリレンダリングが有効になっている場合、最初のプリレンダリング中に JS を呼び出すことはできません。 JS 相互運用呼び出しは、ブラウザーとの接続が確立されるまで遅延させる必要があります。 詳細については、「Blazor Server アプリがプリレンダリングされていることを検出する」セクションを参照してください。

次の例は、JS ベースのデコーダーである TextDecoder に基づいています。 この例では、開発者コードから既存の JS API に要件をオフロードする C# メソッドから JS 関数を呼び出す方法を示します。 JS 関数は、C# メソッドからバイト配列を受け取り、配列をデコードし、テキストをコンポーネントに返して表示できるようにします。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の終了 </body> タグの内部に、次の JS コードを追加します。

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

次の CallJsExample1 コンポーネントでは、次を実行します。

  • ボタン ( Convert Array ) を選択するときに、InvokeAsync を使用して convertArray JS 関数を呼び出します。
  • JS 関数が呼び出された後、渡された配列が文字列に変換されます。 文字列は、表示できるようにコンポーネントに返されます (text)。

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

返された値を読み取らずに JavaScript 関数を呼び出す (InvokeVoidAsync)

次の場合に InvokeVoidAsync を使用します。

  • JS の呼び出しの結果を読み取るために .NET が必要ではない。
  • JS 関数で void(0)/void 0 または undefined が返される。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert1 JS 関数を提供します。 関数は InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

コンポーネント (.razor) の例 (InvokeVoidAsync)

TickerChanged により、次の CallJsExample2 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

クラス (.cs) の例 (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged により、次の CallJsExample3 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

JavaScript 関数を呼び出し、戻り値を読み取る (InvokeAsync)

.NET で JS の呼び出しの結果を読み取る必要があるときは、InvokeAsync を使用します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert2 JS 関数を提供します。 次の例では、呼び出し元で表示する文字列が返されます。

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

コンポーネント (.razor) の例 (InvokeAsync)

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample4 コンポーネントに表示されます。

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

クラス (.cs) の例 (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample5 コンポーネントに表示されます。

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

コンテンツの動的生成のシナリオ

BuildRenderTree でコンテンツを動的に生成するには、[Inject] 属性を使用します。

[Inject]
IJSRuntime JS { get; set; }

Blazor Server アプリがプリレンダリングされていることを検出する

このセクションは Razor コンポーネントをプリレンダリングする Blazor Server およびホストされている Blazor WebAssembly アプリに適用されます。プリレンダリングは ASP.NET Core Razor コンポーネントのプリレンダリングと統合を行う で取り扱われています。

アプリでプリレンダリングするとき、JavaScript の呼び出しなどの特定のアクションは不可能です。 コンポーネントは、プリレンダリング時に異なるレンダリングが必要になる場合があります。

次の例の場合、setElementText1 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は JSRuntimeExtensions.InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

このような呼び出しの動作が保証されるまで JavaScript 相互運用呼び出しを遅延させるには、OnAfterRender{Async} ライフサイクル イベントをオーバーライドします。 このイベントは、アプリが完全にレンダリングされた後にのみ呼び出されます。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText1 = (element, text) => element.innerText = text;

次のコンポーネントは、プリレンダリングと互換性のある方法で、コンポーネントの初期化ロジックの一部として JavaScript の相互運用を使用する方法を示しています。 コンポーネントには、OnAfterRenderAsync 内からレンダリングの更新をトリガーできることが示されています。 開発者はこのシナリオで無限ループを作成しないように注意する必要があります。

次の例の場合、setElementText2 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は IJSRuntime.InvokeAsync を指定して呼び出され、次の値を返します。

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

JSRuntime.InvokeAsync が呼び出されるとき、ElementReference は、以前のライフサイクル メソッドではなく OnAfterRenderAsync でのみ使用されます。コンポーネントがレンダリングされるまで JavaScript 要素が存在しないためです。

StateHasChanged は、JavaScript の相互運用呼び出しから取得された新しい状態でコンポーネントを再度レンダリングするために呼び出されます (詳細については、「ASP.NET Core Blazor コンポーネントのレンダリング」を参照してください)。 StateHasChangeddatanull である場合にのみ呼び出されるため、このコードで無限ループが作成されることはありません。

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

JavaScript の場所

JavaScript (JS) 相互運用性 (相互運用) の概要に関する記事で説明されている方法のいずれかを使用して、JavaScript (JS) コードを読み込みます。

JS モジュールでのスクリプトの分離の詳細については、「JavaScript モジュールでの JavaScript の分離」セクションを参照してください。

警告

<script> タグは動的に更新できないため、<script> タグをコンポーネント ファイル (.razor) 内に配置しないでください。

JavaScript モジュールでの JavaScript の分離

Blazor では、標準の JavaScript モジュールに JavaScript (JS) を分離できます (ECMAScript の仕様)。

JS を分離すると、次のようなベネフィットがあります。

  • インポートされる JS によって、グローバル名前空間が汚染されなくなります。
  • ライブラリおよびコンポーネントのコンシューマーは、関連する JS をインポートする必要がありません。

たとえば、次の JS モジュールからは、ブラウザー ウィンドウのプロンプトを表示するための JS 関数がエクスポートされます。 次の JS コードを外部の JS ファイルに配置します。

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

前の JS モジュールを wwwroot フォルダー内の静的 Web アセットとしてアプリまたはクラス ライブラリに追加し、IJSRuntime インスタンスで InvokeAsync を呼び出すことにより、そのモジュールを .NET コードにインポートします。

IJSRuntime により、モジュールが IJSObjectReference としてインポートされます。これは、.NET コードから JS オブジェクトへの参照を表します。 モジュールからエクスポートされた JS 関数を呼び出すには、IJSObjectReference を使用します。

Pages/CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

前の例の場合:

  • 慣例により、import 識別子は、JS モジュールをインポートするために特別に使用される特殊な識別子です。
  • 安定した静的な Web アセット パス ./{SCRIPT PATH AND FILENAME (.js)} を使用して、モジュールの外部 JS ファイルを指定します。
    • JS ファイルへの正しい静的アセット パスを作成するためには、現在のディレクトリ (./) に対するパス セグメントが必要です。
    • {SCRIPT PATH AND FILENAME (.js)} プレースホルダーは、wwwroot の下のパスとファイル名です。

モジュールを動的にインポートするにはネットワーク要求が必要であるため、これは InvokeAsync を呼び出すことによってのみ、非同期的に実現できます。

IJSInProcessObjectReference は、関数を同期的に呼び出すことができる JS オブジェクトへの参照を表します。

注意

Razor クラス ライブラリによって外部の JS ファイルが提供される場合は、安定した静的 Web アセット パス ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)} を使用してモジュールの JS ファイルを指定します。

  • JS ファイルへの正しい静的アセット パスを作成するためには、現在のディレクトリ (./) に対するパス セグメントが必要です。
  • {PACKAGE ID} プレースホルダーは、ライブラリのパッケージ ID です。 プロジェクト ファイルで <PackageId> が指定されていない場合、パッケージ ID の既定値はプロジェクトのアセンブリ名になります。 次の例では、ライブラリのアセンブリ名は ComponentLibrary であり、ライブラリのプロジェクト ファイルで <PackageId> が指定されていません。
  • {SCRIPT PATH AND FILENAME (.js)} プレースホルダーは、wwwroot の下のパスとファイル名です。 次の例では、外部 JS ファイル (script.js) はクラス ライブラリの wwwroot フォルダーに配置されます。
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

詳細については、「ASP.NET Core Razor コンポーネントを Razor クラス ライブラリから使用する」を参照してください。

要素への参照をキャプチャする

一部の JavaScript (JS) 相互運用シナリオでは、HTML 要素への参照が必要です。 たとえば、UI ライブラリで初期化のための要素参照が必要な場合、clickplay などの要素でコマンドのような API の呼び出し必要になる可能性があります。

次の方法を使用して、コンポーネント内の HTML 要素への参照をキャプチャします。

  • @ref 属性を HTML 要素に追加します。
  • 名前が @ref 属性の値に一致する ElementReference 型のフィールドを定義します。

次の例は、username <input> 要素への参照をキャプチャする方法を示しています。

<input @ref="username" ... />

@code {
    private ElementReference username;
}

警告

Blazor とやりとりしない空の要素のコンテンツを変化させるには、要素参照のみを使用します。 このシナリオは、サードパーティの API から要素にコンテンツが提供される場合に便利です。 Blazor は要素とやりとりしないため、Blazor の要素表現とドキュメント オブジェクト モデル (DOM) との間に競合が発生する可能性がありません。

次の例では、Blazor が DOM とやりとりしてこの要素のリスト項目 (<li>) を Todos オブジェクトから設定するため、順序なしリスト (ul) のコンテンツを変化させるのは "危険" です。

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

JS 相互運用により要素 MyList のコンテンツが変更され、Blazor でその要素に差分を適用しようとした場合、差分は DOM と一致しません。

詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

ElementReference は JS 相互運用経由で JS コードに渡されます。 JS コードが HTMLElement インスタンスを受け取り、通常の DOM API で使用できます。 たとえば、次のコードでは、要素にマウス クリックを送信できるようにする .NET 拡張メソッド (TriggerClickEvent) を定義しています。

JS 関数 clickElement により、渡された HTML 要素 (element) で click イベントが作成されます。

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

値を返さない JS 関数を呼び出すには、JSRuntimeExtensions.InvokeVoidAsync を使用します。 次のコードは、キャプチャされた ElementReference で前述の JS 関数を呼び出し、クライアント側の click イベントをトリガーします。

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

拡張メソッドを使用するには、IJSRuntime インスタンスを受け取る静的拡張メソッドを作成します。

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

clickElement メソッドは、オブジェクトで直接呼び出されます。 次の例では、TriggerClickEvent メソッドが JsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

重要

exampleButton 変数は、コンポーネントがレンダリングされた後にのみ設定されます。 未設定の ElementReference が JS コードに渡された場合、JS コードは null の値を受け取ります。 コンポーネントのレンダリングが完了した後で要素の参照を操作するには、OnAfterRenderAsync または OnAfterRender コンポーネント ライフサイクル メソッドを使用します。

ジェネリック型を操作して値を返す場合は、ValueTask<TResult> を使用します。

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

{JAVASCRIPT FUNCTION} プレースホルダーは、JS 関数識別子です。

GenericMethod は、型を持つオブジェクトで直接呼び出されます。 次の例では、GenericMethodJsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

コンポーネント間で要素を参照する

ElementReference は次の理由からコンポーネントで渡すことができません。

親コンポーネントが要素参照を他のコンポーネントで使用できるようにするために、親コンポーネントは次のことを実行できます。

  • 子コンポーネントがコールバックを登録できるようにします。
  • 渡された要素参照を使用して、登録されたコールバックをOnAfterRender イベント中に呼び出します。 間接的には、この方法により、子コンポーネントが親の要素参照とやりとりできるようになります。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> に、次のスタイルを追加します。

<style>
    .red { color: red }
</style>

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の終了 </body> タグの内側に、次のスクリプトを追加します。

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (親コンポーネント):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

前の例で、アプリの名前空間は Pages フォルダー内にコンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

Shared/SurveyPrompt.razor (子コンポーネント):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

前の例で、アプリの名前空間は Shared フォルダー内に共有コンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

JavaScript 相互運用の呼び出しを強化する

このセクションは主に Blazor Server アプリに適用されますが、条件が許せば、Blazor WebAssembly アプリでも JS 相互運用のタイムアウトを設定できます。

Blazor Server アプリでの JavaScript (JS) 相互運用は、ネットワーク エラーにより失敗するおそれがあるため、信頼性の低いものとして扱う必要があります。 既定では、Blazor Server アプリにより JS 相互運用の呼び出しに対して 1 分間のタイムアウトが使用されます。 アプリでより積極的なタイムアウトが許容される場合は、次のいずれかの方法を使用してタイムアウトを設定します。

CircuitOptions.JSInteropDefaultCallTimeout を使用して Startup.csStartup.ConfigureServices メソッドでグローバル タイムアウトを設定します。

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

{TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。

コンポーネントのコードで呼び出しごとにタイムアウトを設定します。 この方法で指定したタイムアウトは、JSInteropDefaultCallTimeout によって設定されたグローバル タイムアウトを上書きします。

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

前の例の場合:

  • {TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。
  • {ID} プレースホルダーは、呼び出す関数の識別子です。 たとえば、値 someScope.someFunction を指定すると、関数 window.someScope.someFunction が呼び出されます。

JS 相互運用のエラーの一般的な原因は、Blazor Server アプリでのネットワーク障害ですが、Blazor WebAssembly アプリで JS 相互運用の呼び出しに対し、呼び出しごとのタイムアウトを設定できます。 Blazor WebAssembly アプリには SignalR 回線は存在しませんが、Blazor WebAssembly アプリに適用される他の理由により、JS 相互運用の呼び出しが失敗するおそれがあります。

リソース枯渇の詳細については、「ASP.NET Core Blazor Server の脅威の緩和のガイダンス」を参照してください。

循環オブジェクト参照の回避

循環参照を含むオブジェクトは、次のいずれに対しても、クライアントでシリアル化することはできません。

  • .NET メソッドの呼び出し。
  • 戻り値の型に循環参照がある場合の、C# からの JavaScript メソッドの呼び出し。

UI をレンダリングする JavaScript ライブラリ

ブラウザーのドキュメント オブジェクト モデル (DOM) 内に表示可能なユーザー インターフェイス要素を生成する JavaScript (JS) ライブラリを使用することが必要になる場合があります。 Blazor の差分システムは、DOM 要素のツリーに対する制御に依存しており、外部コードによって DOM ツリーが変更されて、差分を適用するためのメカニズムが無効になるとエラーが発生するため、一見すると、これは困難に思えるかもしれません。 これは、Blazor に固有の制限ではありません。 差分ベースの UI フレームワークでは同じ課題が発生します。

幸い、外部で生成された UI を Razor コンポーネントの UI に確実に埋め込むのは簡単です。 推奨される方法は、コンポーネントのコード (.razor ファイル) で空の要素を生成することです。 Blazor の差分システムに関する限り、要素は常に空であるため、レンダラーによって要素は再帰されず、代わりにその内容はそのままの状態になります。 これにより、外部で管理されている任意の内容を要素に設定しても安全になります。

次の例はこの概念を示したものです。 if ステートメント内で firstRendertrue のとき、JS 相互運用を使用して Blazor の外部の unmanagedElement と対話します。 たとえば、外部の JS ライブラリを呼び出して要素を設定します。 このコンポーネントが削除されるまで、要素の内容が Blazor によって操作されることはありません。 コンポーネントが削除されると、コンポーネントの DOM サブツリー全体も削除されます。

<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private HtmlElement unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

オープンソースの Mapbox API を使用して対話型マップをレンダリングする次のような例について考えてみます。

次の JS モジュールは、アプリに配置されるか、Razor クラス ライブラリから使用できます。

注意

Mapbox マップを作成するには、Mapbox サインインからアクセス トークンを取得し、次のコードの {ACCESS TOKEN} の場所でそれを指定します。

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

正しいスタイルを生成するには、ホストの HTML ページに次のスタイルシート タグを追加します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) で、次の <link> 要素を <head> 要素マークアップに追加します。

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

Pages/CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference mapModule;
    private IJSObjectReference mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
        => await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude, 
            longitude).AsTask();

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

前の例では、対話型のマップ UI が生成されます。 ユーザーは:

  • ドラッグしてスクロールまたはズームできます。
  • ボタンを選択して、あらかじめ定義されている場所に移動します。

Mapbox による東京の市街地図。英国のブリストルと日本の東京を選択するためのボタンがあります

前の例の場合:

  • @ref="mapElement" が含まれる <div> は、Blazor に関する限り空のままになります。 mapbox-gl.js スクリプトにより、要素を安全に設定し、その内容を変更できます。 この手法は、UI をレンダリングする任意の JS ライブラリで使用します。 ページの他の部分に手を伸ばして変更しようとしない限り、サードパーティの JS SPA フレームワークのコンポーネントを Blazor コンポーネントの内部に埋め込むことができます。 Blazor によって空と見なされない要素を、外部の JS コードで変更することは、安全では ありません
  • このアプローチを使用する場合は、Blazor によって DOM 要素が保持または破棄される方法に関する規則に留意してください。 既定では DOM 要素が可能な限り保持されるため、コンポーネントにより安全にボタン クリック イベントが処理され、既存のマップ インスタンスが更新されます。 @foreach ループの内側からマップ要素のリストをレンダリングしていた場合は、@key を使用して、コンポーネントのインスタンスを確実に保持する必要があります。 そうしないと、リスト データを変更した場合、コンポーネントのインスタンスによって前のインスタンスの状態が望ましくない状態で保持される可能性があります。 詳細については、@key を使用した要素とコンポーネントの保持に関する記事を参照してください。
  • この例では、JS のロジックと依存関係が ES6 モジュール内にカプセル化されており、その import 識別子を使用してモジュールが動的に読み込まれます。 詳細については、「JavaScript モジュールでの JavaScript の分離」を参照してください。

バイト配列のサポート

Blazor では、Base64 へのバイト配列のエンコードおよびデコードを回避する、最適化されたバイト配列 JS 相互運用がサポートされています。 次の例では、JS 相互運用を使用してバイト配列を JavaScript に渡します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の終了 </body> タグの内部で、receiveByteArray JS 関数を提供します。 関数は InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.receiveByteArray = (bytes) => {
    let utf8decoder = new TextDecoder();
    let str = utf8decoder.decode(bytes);
    return str;
  };
</script>

Pages/CallJsExample9.razor:

@page "/call-js-example-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}

JavaScript から .NET を呼び出す際にバイト配列を使用する方法については、ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す を参照してください。

JavaScript 相互運用呼び出しのサイズ制限

"このセクションは、Blazor Server アプリにのみ適用されます。Blazor WebAssembly の場合、フレームワークでは JavaScript (JS) 相互運用機能の入力と出力のサイズに制限を課しません。 "

Blazor Server では、ハブ メソッドで許可される SignalR 受信メッセージの最大サイズによって、JS 相互運用呼び出しのサイズが制限されます。これは、HubOptions.MaximumReceiveMessageSize によって適用されます (既定値: 32 KB)。 JS から .NET への SignalR メッセージが MaximumReceiveMessageSize より大きい場合は、エラーがスローされます。 このフレームワークでは、ハブからクライアントへの SignalR メッセージのサイズが制限されることはありません。

SignalR のログがDebug または Trace に設定されていない場合、メッセージ サイズのエラーはブラウザーの開発者ツール コンソールにのみ表示されます。

エラー :次のエラーで接続が切断されました。"エラー: サーバーが終了時にエラーを返しました:接続はエラーで終了しました。"

SignalR サーバー側のログ Debug または Trace に設定されている場合、サーバー側のログには、メッセージ サイズ エラーの InvalidDataException が表示されます。

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

エラー:

System.IO.InvalidDataException:メッセージの最大サイズ 32,768 B を超えました。 メッセージのサイズは、AddHubOptions で構成できます。

制限値を増やすには、Startup.ConfigureServicesMaximumReceiveMessageSize を設定します。 次の例では、受信メッセージの最大サイズを 64 KB (64 * 1024) に設定します。

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR 受信メッセージ サイズの制限値を増やすと、より多くのサーバー リソースが必要になり、悪意のあるユーザーからのより大きなリスクにサーバーがさらされます。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大きなペイロードを読み取るための 1 つの選択肢は、小さいチャンクでコンテンツを送信し、ペイロードを Stream として処理することです。 これは、大量の JSON ペイロードを読み取る場合、またはデータを JS で生バイトとして利用できる場合に使用できます。 Blazor Server で大規模なバイナリ ペイロードを送信する、InputFile コンポーネントに似た手法を使用する方法の例については、Binary Submit サンプル アプリを参照してください。

注意

ASP.NET Core 参照ソースへのドキュメント リンクを使用すると、リポジトリの main ブランチが読み込まれます。このブランチは、ASP.NET Core の次回リリースに向けて行われている製品単位の現在の開発を表します。 別のリリースのブランチを選択するには、 [Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使用して、そのブランチを選択します。 たとえば、ASP.NET Core 5.0 リリースの場合は、release/5.0 ブランチを選択します。

Blazor Server アプリで JS と Blazor の間で大量のデータを転送するコードを開発するときは、次のガイダンスを考慮してください。

  • データをより小さな部分にスライスし、すべてのデータがサーバーによって受信されるまでデータ セグメントを順番に送信します。
  • JS および C# コードで大きなオブジェクトを割り当てないでください。
  • データを送受信するときに、メイン UI スレッドを長時間ブロックしないでください。
  • プロセスの完了時またはキャンセル時に、消費していたメモリを解放します。
  • セキュリティ上の理由から、次の追加要件を適用します。
    • 渡すことのできるファイルまたはデータの最大サイズを宣言します。
    • クライアントからサーバーへの最小アップロード レートを宣言します。
  • データがサーバーによって受信されたら、データは:
    • すべてのセグメントが収集されるまで、一時的にメモリ バッファーに格納できます。
    • 直ちに消費できます。 たとえば、データは、データベースに直ちに格納することも、セグメントを受信するたびにディスクに書き込むこともできます。

マーシャリングされていない JavaScript 相互運用

.NET オブジェクトが JavaScript (JS) 相互運用のためにシリアル化され、次のいずれかに該当する場合、Blazor WebAssembly コンポーネントのパフォーマンスが低下するおそれがあります。

  • 大量の .NET オブジェクトが短時間でシリアル化される。 たとえば、JS 相互運用の呼び出しが、マウス ホイールの回転など、入力デバイスの動きに基づいて行われるときは、パフォーマンスが低下する場合があります。
  • 大きな .NET オブジェクトまたは多数の .NET オブジェクトを JS 相互運用のためにシリアル化する必要がある。 たとえば、JS 相互運用の呼び出しで数十個のファイルをシリアル化する必要があるときは、パフォーマンスが低下する場合があります。

IJSUnmarshalledObjectReference は、.NET データをシリアル化するオーバーヘッドなしで関数を呼び出すことができる JS オブジェクトへの参照を表します。

次に例を示します。

  • 文字列と整数を含む struct は、シリアル化されずに JS に渡されます。
  • JS 関数によってデータが処理され、ブール値または文字列が呼び出し元に返されます。
  • JS の文字列を .NET の string オブジェクトに直接変換することはできません。 unmarshalledFunctionReturnString 関数によって BINDING.js_string_to_mono_string が呼び出され、JS の文字列の変換が管理されます。

注意

次の例は、JS に渡された struct によってコンポーネントのパフォーマンスは低下しないので、このシナリオの一般的な使用例ではありません。 この例では、シリアル化されていない .NET データを渡すための概念を示すためだけに、小さなオブジェクトを使用しています。

次の <script> ブロックを、wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) に配置します。 または、<script src="{SCRIPT PATH AND FILE NAME (.js)}></script> を使用して終了 </body> タグ内で参照される外部の JS ファイルに、JS を配置することもできます。{SCRIPT PATH AND FILE NAME (.js)} プレースホルダーは、スクリプトのパスとファイル名です。

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

警告

js_string_to_mono_string 関数の名前、動作、および存在は、.NET の今後のリリースで変更される可能性があります。 次に例を示します。

  • 関数の名前が変更される可能性があります。
  • フレームワークによる文字列の自動変換を優先して、関数自体が削除される可能性があります。

Pages/CallJsExample10.razor:

@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

IJSUnmarshalledObjectReference インスタンスが C# コードで破棄されない場合は、JS で破棄することができます。 次の dispose 関数を使用すると、JS から呼び出されたときにオブジェクト参照が破棄されます。

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

配列型は、js_typed_array_to_array を使用して JS オブジェクトから .NET オブジェクトに変換できますが、JS の配列は型指定された配列である必要があります。 JS の配列は、C# コードで .NET オブジェクト配列 (object[]) として読み取ることができます。

文字列配列などの他のデータ型は変換できますが、新しい Mono 配列オブジェクト (mono_obj_array_new) を作成し、その値 (mono_obj_array_set) を設定する必要があります。

警告

js_typed_array_to_arraymono_obj_array_newmono_obj_array_set など、Blazor フレームワークによって提供される JS 関数は、.NET の今後のリリースで、名前の変更、動作の変更、または削除の対象となる場合があります。

JavaScript から .NET へのストリーム

Blazor では、JavaScript から .NET に直接データをストリーミングすることがサポートされます。 ストリームは Microsoft.JSInterop.IJSStreamReference インターフェイスを使用して要求されます。

Microsoft.JSInterop.IJSStreamReference.OpenReadStreamAsync では Stream が返され、次のパラメーターが使用されます。

  • maxAllowedSize: JavaScript からの読み取り操作に許可される最大バイト数。指定されていない場合、既定では 512,000 バイトになります。
  • cancellationToken: 読み取りをキャンセルするための CancellationToken

JavaScript の場合:

function streamToDotNet() {
  return new Uint8Array(10000000);
}

C# コードの場合:

var dataReference = 
    await JS.InvokeAsync<IJSStreamReference>("streamToDotNet");
using var dataReferenceStream = 
    await dataReference.OpenReadStreamAsync(maxAllowedSize: 10_000_000);

var outputPath = Path.Combine(Path.GetTempPath(), "file.txt");
using var outputFileStream = File.OpenWrite(outputPath);
await dataReferenceStream.CopyToAsync(outputFileStream);

前の例の場合:

  • JS は、挿入された IJSRuntime インスタンスです。
  • dataReferenceStreamは、現在のユーザーの一時フォルダー パス (GetTempPath) でディスク (file.txt) に書き込まれます。

.NET から JavaScript へのストリーム

''この機能は ASP.NET Core 6.0 リリース候補 1 以降に適用されます。ASP.NET Core 6.0 リリース候補 1 は 9 月にリリースが予定されています。ASP.NET Core 6.0 は今年後半にリリースされる予定です。 ''

Blazor では、.NET から JavaScript に直接データをストリーミングすることがサポートされます。 ストリームは、Microsoft.JSInterop.DotNetStreamReference を使用して作成されます。

Microsoft.JSInterop.DotNetStreamReference は .NET ストリームを表し、次のパラメーターが使用されます。

  • stream: JavaScript に送信されるストリーム。
  • leaveOpen: 転送後にストリームを開いたままにするかどうかを指定します。 値が指定されていない場合、leaveOpen は既定で false になります。

JavaScript では、配列バッファーまたは読み取り可能なストリームを使用してデータを受信します。

  • ArrayBuffer を使用する場合:

    async function streamToJavaScript(streamRef) {
      const data = await streamRef.arrayBuffer();
    }
    
  • ReadableStream を使用する場合:

    async function streamToJavaScript(streamRef) {
      const stream = await streamRef.stream();
    }
    

C# コードの場合:

using var streamRef = new DotNetStreamReference(stream: {STREAM}, leaveOpen: false);
await JS.InvokeVoidAsync("streamToJavaScript", streamRef);

前の例の場合:

  • {STREAM}プレースホルダーは、JavaScript に送信される Stream を表します。
  • JS は、挿入された IJSRuntime インスタンスです。

JavaScript の例外をキャッチする

JS の例外をキャッチするには、JS の相互運用を try-catch ブロックにラップし、JSException をキャッチします。

次の例では、nonFunction JS 関数は存在しません。 関数が見つからないとき、次のエラーを示す Message を含む JSException がトラップされます。

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

その他のリソース

この記事では、.NET から JavaScript (JS) 関数を呼び出す方法について説明します。 JS から .NET メソッドを呼び出す方法については、「ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す」を参照してください。

.NET から JS を呼び出すには、IJSRuntime 抽象化を挿入し、次のいずれかのメソッドを呼び出します。

JS 関数を呼び出す上記の .NET メソッドについて:

  • 関数の識別子 (String) は、グローバル スコープ (window) を基準とするものです。 window.someScope.someFunction を呼び出す場合の識別子は someScope.someFunction です。 関数は、呼び出す前に登録する必要はありません。
  • Object[] で任意の数の JSON シリアル化可能な引数を JS 関数に渡します。
  • キャンセル トークン (CancellationToken) により、操作を取り消す必要があることを示す通知が伝達されます。
  • TimeSpan は、JS 操作の時間制限を表します。
  • また、戻り値の型 TValue も JSON シリアル化可能である必要があります。 TValue は、返される JSON 型に最適に対応する .NET 型と一致する必要があります。
  • InvokeAsync メソッドに対しては JS Promise が返されます。 InvokeAsync によって Promise のラップが解除され、Promise によって待機されている値が返されます。

Blazor Server アプリでプリレンダリングが有効になっている場合、最初のプリレンダリング中に JS を呼び出すことはできません。 JS 相互運用呼び出しは、ブラウザーとの接続が確立されるまで遅延させる必要があります。 詳細については、「Blazor Server アプリがプリレンダリングされていることを検出する」セクションを参照してください。

次の例は、JS ベースのデコーダーである TextDecoder に基づいています。 この例では、開発者コードから既存の JS API に要件をオフロードする C# メソッドから JS 関数を呼び出す方法を示します。 JS 関数は、C# メソッドからバイト配列を受け取り、配列をデコードし、テキストをコンポーネントに返して表示できるようにします。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部に、次の JS コードを追加します。

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

次の CallJsExample1 コンポーネントでは、次を実行します。

  • ボタン ( Convert Array ) を選択するときに、InvokeAsync を使用して convertArray JS 関数を呼び出します。
  • JS 関数が呼び出された後、渡された配列が文字列に変換されます。 文字列は、表示できるようにコンポーネントに返されます (text)。

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

返された値を読み取らずに JavaScript 関数を呼び出す (InvokeVoidAsync)

次の場合に InvokeVoidAsync を使用します。

  • JS の呼び出しの結果を読み取るために .NET が必要ではない。
  • JS 関数で void(0)/void 0 または undefined が返される。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert1 JS 関数を提供します。 関数は InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

コンポーネント (.razor) の例 (InvokeVoidAsync)

TickerChanged により、次の CallJsExample2 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

クラス (.cs) の例 (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged により、次の CallJsExample3 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

JavaScript 関数を呼び出し、戻り値を読み取る (InvokeAsync)

.NET で JS の呼び出しの結果を読み取る必要があるときは、InvokeAsync を使用します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert2 JS 関数を提供します。 次の例では、呼び出し元で表示する文字列が返されます。

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

コンポーネント (.razor) の例 (InvokeAsync)

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample4 コンポーネントに表示されます。

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

クラス (.cs) の例 (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample5 コンポーネントに表示されます。

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

コンテンツの動的生成のシナリオ

BuildRenderTree でコンテンツを動的に生成するには、[Inject] 属性を使用します。

[Inject]
IJSRuntime JS { get; set; }

Blazor Server アプリがプリレンダリングされていることを検出する

このセクションは Razor コンポーネントをプリレンダリングする Blazor Server およびホストされている Blazor WebAssembly アプリに適用されます。プリレンダリングは ASP.NET Core Razor コンポーネントのプリレンダリングと統合を行う で取り扱われています。

アプリでプリレンダリングするとき、JavaScript の呼び出しなどの特定のアクションは不可能です。 コンポーネントは、プリレンダリング時に異なるレンダリングが必要になる場合があります。

次の例の場合、setElementText1 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は JSRuntimeExtensions.InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

このような呼び出しの動作が保証されるまで JavaScript 相互運用呼び出しを遅延させるには、OnAfterRender{Async} ライフサイクル イベントをオーバーライドします。 このイベントは、アプリが完全にレンダリングされた後にのみ呼び出されます。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText1 = (element, text) => element.innerText = text;

次のコンポーネントは、プリレンダリングと互換性のある方法で、コンポーネントの初期化ロジックの一部として JavaScript の相互運用を使用する方法を示しています。 コンポーネントには、OnAfterRenderAsync 内からレンダリングの更新をトリガーできることが示されています。 開発者はこのシナリオで無限ループを作成しないように注意する必要があります。

次の例の場合、setElementText2 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は IJSRuntime.InvokeAsync を指定して呼び出され、次の値を返します。

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

JSRuntime.InvokeAsync が呼び出されるとき、ElementReference は、以前のライフサイクル メソッドではなく OnAfterRenderAsync でのみ使用されます。コンポーネントがレンダリングされるまで JavaScript 要素が存在しないためです。

StateHasChanged は、JavaScript の相互運用呼び出しから取得された新しい状態でコンポーネントを再度レンダリングするために呼び出されます (詳細については、「ASP.NET Core Blazor コンポーネントのレンダリング」を参照してください)。 StateHasChangeddatanull である場合にのみ呼び出されるため、このコードで無限ループが作成されることはありません。

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

JavaScript の場所

JavaScript (JS) 相互運用性 (相互運用) の概要に関する記事で説明されている方法のいずれかを使用して、JavaScript (JS) コードを読み込みます。

JS モジュールでのスクリプトの分離の詳細については、「JavaScript モジュールでの JavaScript の分離」セクションを参照してください。

警告

<script> タグは動的に更新できないため、<script> タグをコンポーネント ファイル (.razor) 内に配置しないでください。

JavaScript モジュールでの JavaScript の分離

Blazor では、標準の JavaScript モジュールに JavaScript (JS) を分離できます (ECMAScript の仕様)。

JS を分離すると、次のようなベネフィットがあります。

  • インポートされる JS によって、グローバル名前空間が汚染されなくなります。
  • ライブラリおよびコンポーネントのコンシューマーは、関連する JS をインポートする必要がありません。

たとえば、次の JS モジュールからは、ブラウザー ウィンドウのプロンプトを表示するための JS 関数がエクスポートされます。 次の JS コードを外部の JS ファイルに配置します。

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

前の JS モジュールを wwwroot フォルダー内の静的 Web アセットとしてアプリまたはクラス ライブラリに追加し、IJSRuntime インスタンスで InvokeAsync を呼び出すことにより、そのモジュールを .NET コードにインポートします。

IJSRuntime により、モジュールが IJSObjectReference としてインポートされます。これは、.NET コードから JS オブジェクトへの参照を表します。 モジュールからエクスポートされた JS 関数を呼び出すには、IJSObjectReference を使用します。

Pages/CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

前の例の場合:

  • 慣例により、import 識別子は、JS モジュールをインポートするために特別に使用される特殊な識別子です。
  • 安定した静的な Web アセット パス ./{SCRIPT PATH AND FILENAME (.js)} を使用して、モジュールの外部 JS ファイルを指定します。
    • JS ファイルへの正しい静的アセット パスを作成するためには、現在のディレクトリ (./) に対するパス セグメントが必要です。
    • {SCRIPT PATH AND FILENAME (.js)} プレースホルダーは、wwwroot の下のパスとファイル名です。

モジュールを動的にインポートするにはネットワーク要求が必要であるため、これは InvokeAsync を呼び出すことによってのみ、非同期的に実現できます。

IJSInProcessObjectReference は、関数を同期的に呼び出すことができる JS オブジェクトへの参照を表します。

注意

Razor クラス ライブラリによって外部の JS ファイルが提供される場合は、安定した静的 Web アセット パス ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)} を使用してモジュールの JS ファイルを指定します。

  • JS ファイルへの正しい静的アセット パスを作成するためには、現在のディレクトリ (./) に対するパス セグメントが必要です。
  • {PACKAGE ID} プレースホルダーは、ライブラリのパッケージ ID です。 プロジェクト ファイルで <PackageId> が指定されていない場合、パッケージ ID の既定値はプロジェクトのアセンブリ名になります。 次の例では、ライブラリのアセンブリ名は ComponentLibrary であり、ライブラリのプロジェクト ファイルで <PackageId> が指定されていません。
  • {SCRIPT PATH AND FILENAME (.js)} プレースホルダーは、wwwroot の下のパスとファイル名です。 次の例では、外部 JS ファイル (script.js) はクラス ライブラリの wwwroot フォルダーに配置されます。
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

詳細については、「ASP.NET Core Razor コンポーネントを Razor クラス ライブラリから使用する」を参照してください。

要素への参照をキャプチャする

一部の JavaScript (JS) 相互運用シナリオでは、HTML 要素への参照が必要です。 たとえば、UI ライブラリで初期化のための要素参照が必要な場合、clickplay などの要素でコマンドのような API の呼び出し必要になる可能性があります。

次の方法を使用して、コンポーネント内の HTML 要素への参照をキャプチャします。

  • @ref 属性を HTML 要素に追加します。
  • 名前が @ref 属性の値に一致する ElementReference 型のフィールドを定義します。

次の例は、username <input> 要素への参照をキャプチャする方法を示しています。

<input @ref="username" ... />

@code {
    private ElementReference username;
}

警告

Blazor とやりとりしない空の要素のコンテンツを変化させるには、要素参照のみを使用します。 このシナリオは、サードパーティの API から要素にコンテンツが提供される場合に便利です。 Blazor は要素とやりとりしないため、Blazor の要素表現とドキュメント オブジェクト モデル (DOM) との間に競合が発生する可能性がありません。

次の例では、Blazor が DOM とやりとりしてこの要素のリスト項目 (<li>) を Todos オブジェクトから設定するため、順序なしリスト (ul) のコンテンツを変化させるのは "危険" です。

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

JS 相互運用により要素 MyList のコンテンツが変更され、Blazor でその要素に差分を適用しようとした場合、差分は DOM と一致しません。

詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

ElementReference は JS 相互運用経由で JS コードに渡されます。 JS コードが HTMLElement インスタンスを受け取り、通常の DOM API で使用できます。 たとえば、次のコードでは、要素にマウス クリックを送信できるようにする .NET 拡張メソッド (TriggerClickEvent) を定義しています。

JS 関数 clickElement により、渡された HTML 要素 (element) で click イベントが作成されます。

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

値を返さない JS 関数を呼び出すには、JSRuntimeExtensions.InvokeVoidAsync を使用します。 次のコードは、キャプチャされた ElementReference で前述の JS 関数を呼び出し、クライアント側の click イベントをトリガーします。

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

拡張メソッドを使用するには、IJSRuntime インスタンスを受け取る静的拡張メソッドを作成します。

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

clickElement メソッドは、オブジェクトで直接呼び出されます。 次の例では、TriggerClickEvent メソッドが JsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

重要

exampleButton 変数は、コンポーネントがレンダリングされた後にのみ設定されます。 未設定の ElementReference が JS コードに渡された場合、JS コードは null の値を受け取ります。 コンポーネントのレンダリングが完了した後で要素の参照を操作するには、OnAfterRenderAsync または OnAfterRender コンポーネント ライフサイクル メソッドを使用します。

ジェネリック型を操作して値を返す場合は、ValueTask<TResult> を使用します。

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

{JAVASCRIPT FUNCTION} プレースホルダーは、JS 関数識別子です。

GenericMethod は、型を持つオブジェクトで直接呼び出されます。 次の例では、GenericMethodJsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

コンポーネント間で要素を参照する

ElementReference は次の理由からコンポーネントで渡すことができません。

親コンポーネントが要素参照を他のコンポーネントで使用できるようにするために、親コンポーネントは次のことを実行できます。

  • 子コンポーネントがコールバックを登録できるようにします。
  • 渡された要素参照を使用して、登録されたコールバックをOnAfterRender イベント中に呼び出します。 間接的には、この方法により、子コンポーネントが親の要素参照とやりとりできるようになります。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の <head> に、次のスタイルを追加します。

<style>
    .red { color: red }
</style>

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内側に、次のスクリプトを追加します。

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (親コンポーネント):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

前の例で、アプリの名前空間は Pages フォルダー内にコンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

Shared/SurveyPrompt.razor (子コンポーネント):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

前の例で、アプリの名前空間は Shared フォルダー内に共有コンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

JavaScript 相互運用の呼び出しを強化する

このセクションは主に Blazor Server アプリに適用されますが、条件が許せば、Blazor WebAssembly アプリでも JS 相互運用のタイムアウトを設定できます。

Blazor Server アプリでの JavaScript (JS) 相互運用は、ネットワーク エラーにより失敗するおそれがあるため、信頼性の低いものとして扱う必要があります。 既定では、Blazor Server アプリにより JS 相互運用の呼び出しに対して 1 分間のタイムアウトが使用されます。 アプリでより積極的なタイムアウトが許容される場合は、次のいずれかの方法を使用してタイムアウトを設定します。

CircuitOptions.JSInteropDefaultCallTimeout を使用して Startup.csStartup.ConfigureServices メソッドでグローバル タイムアウトを設定します。

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

{TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。

コンポーネントのコードで呼び出しごとにタイムアウトを設定します。 この方法で指定したタイムアウトは、JSInteropDefaultCallTimeout によって設定されたグローバル タイムアウトを上書きします。

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

前の例の場合:

  • {TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。
  • {ID} プレースホルダーは、呼び出す関数の識別子です。 たとえば、値 someScope.someFunction を指定すると、関数 window.someScope.someFunction が呼び出されます。

JS 相互運用のエラーの一般的な原因は、Blazor Server アプリでのネットワーク障害ですが、Blazor WebAssembly アプリで JS 相互運用の呼び出しに対し、呼び出しごとのタイムアウトを設定できます。 Blazor WebAssembly アプリには SignalR 回線は存在しませんが、Blazor WebAssembly アプリに適用される他の理由により、JS 相互運用の呼び出しが失敗するおそれがあります。

リソース枯渇の詳細については、「ASP.NET Core Blazor Server の脅威の緩和のガイダンス」を参照してください。

循環オブジェクト参照の回避

循環参照を含むオブジェクトは、次のいずれに対しても、クライアントでシリアル化することはできません。

  • .NET メソッドの呼び出し。
  • 戻り値の型に循環参照がある場合の、C# からの JavaScript メソッドの呼び出し。

UI をレンダリングする JavaScript ライブラリ

ブラウザーのドキュメント オブジェクト モデル (DOM) 内に表示可能なユーザー インターフェイス要素を生成する JavaScript (JS) ライブラリを使用することが必要になる場合があります。 Blazor の差分システムは、DOM 要素のツリーに対する制御に依存しており、外部コードによって DOM ツリーが変更されて、差分を適用するためのメカニズムが無効になるとエラーが発生するため、一見すると、これは困難に思えるかもしれません。 これは、Blazor に固有の制限ではありません。 差分ベースの UI フレームワークでは同じ課題が発生します。

幸い、外部で生成された UI を Razor コンポーネントの UI に確実に埋め込むのは簡単です。 推奨される方法は、コンポーネントのコード (.razor ファイル) で空の要素を生成することです。 Blazor の差分システムに関する限り、要素は常に空であるため、レンダラーによって要素は再帰されず、代わりにその内容はそのままの状態になります。 これにより、外部で管理されている任意の内容を要素に設定しても安全になります。

次の例はこの概念を示したものです。 if ステートメント内で firstRendertrue のとき、JS 相互運用を使用して Blazor の外部の unmanagedElement と対話します。 たとえば、外部の JS ライブラリを呼び出して要素を設定します。 このコンポーネントが削除されるまで、要素の内容が Blazor によって操作されることはありません。 コンポーネントが削除されると、コンポーネントの DOM サブツリー全体も削除されます。

<h1>Hello! This is a Blazor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private HtmlElement unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

オープンソースの Mapbox API を使用して対話型マップをレンダリングする次のような例について考えてみます。

次の JS モジュールは、アプリに配置されるか、Razor クラス ライブラリから使用できます。

注意

Mapbox マップを作成するには、Mapbox サインインからアクセス トークンを取得し、次のコードの {ACCESS TOKEN} の場所でそれを指定します。

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

正しいスタイルを生成するには、ホストの HTML ページに次のスタイルシート タグを追加します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) で、次の <link> 要素を <head> 要素マークアップに追加します。

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

Pages/CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference mapModule;
    private IJSObjectReference mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
        => await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, latitude, 
            longitude).AsTask();

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

前の例では、対話型のマップ UI が生成されます。 ユーザーは:

  • ドラッグしてスクロールまたはズームできます。
  • ボタンを選択して、あらかじめ定義されている場所に移動します。

Mapbox による東京の市街地図。英国のブリストルと日本の東京を選択するためのボタンがあります

前の例の場合:

  • @ref="mapElement" が含まれる <div> は、Blazor に関する限り空のままになります。 mapbox-gl.js スクリプトにより、要素を安全に設定し、その内容を変更できます。 この手法は、UI をレンダリングする任意の JS ライブラリで使用します。 ページの他の部分に手を伸ばして変更しようとしない限り、サードパーティの JS SPA フレームワークのコンポーネントを Blazor コンポーネントの内部に埋め込むことができます。 Blazor によって空と見なされない要素を、外部の JS コードで変更することは、安全では ありません
  • このアプローチを使用する場合は、Blazor によって DOM 要素が保持または破棄される方法に関する規則に留意してください。 既定では DOM 要素が可能な限り保持されるため、コンポーネントにより安全にボタン クリック イベントが処理され、既存のマップ インスタンスが更新されます。 @foreach ループの内側からマップ要素のリストをレンダリングしていた場合は、@key を使用して、コンポーネントのインスタンスを確実に保持する必要があります。 そうしないと、リスト データを変更した場合、コンポーネントのインスタンスによって前のインスタンスの状態が望ましくない状態で保持される可能性があります。 詳細については、@key を使用した要素とコンポーネントの保持に関する記事を参照してください。
  • この例では、JS のロジックと依存関係が ES6 モジュール内にカプセル化されており、その import 識別子を使用してモジュールが動的に読み込まれます。 詳細については、「JavaScript モジュールでの JavaScript の分離」を参照してください。

JavaScript 相互運用呼び出しのサイズ制限

"このセクションは、Blazor Server アプリにのみ適用されます。Blazor WebAssembly の場合、フレームワークでは JavaScript (JS) 相互運用機能の入力と出力のサイズに制限を課しません。 "

Blazor Server では、ハブ メソッドで許可される SignalR 受信メッセージの最大サイズによって、JS 相互運用呼び出しのサイズが制限されます。これは、HubOptions.MaximumReceiveMessageSize によって適用されます (既定値: 32 KB)。 JS から .NET への SignalR メッセージが MaximumReceiveMessageSize より大きい場合は、エラーがスローされます。 このフレームワークでは、ハブからクライアントへの SignalR メッセージのサイズが制限されることはありません。

SignalR のログがDebug または Trace に設定されていない場合、メッセージ サイズのエラーはブラウザーの開発者ツール コンソールにのみ表示されます。

エラー :次のエラーで接続が切断されました。"エラー: サーバーが終了時にエラーを返しました:接続はエラーで終了しました。"

SignalR サーバー側のログ Debug または Trace に設定されている場合、サーバー側のログには、メッセージ サイズ エラーの InvalidDataException が表示されます。

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

エラー:

System.IO.InvalidDataException:メッセージの最大サイズ 32,768 B を超えました。 メッセージのサイズは、AddHubOptions で構成できます。

制限値を増やすには、Startup.ConfigureServicesMaximumReceiveMessageSize を設定します。 次の例では、受信メッセージの最大サイズを 64 KB (64 * 1024) に設定します。

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR 受信メッセージ サイズの制限値を増やすと、より多くのサーバー リソースが必要になり、悪意のあるユーザーからのより大きなリスクにサーバーがさらされます。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大きなペイロードを読み取るための 1 つの選択肢は、小さいチャンクでコンテンツを送信し、ペイロードを Stream として処理することです。 これは、大量の JSON ペイロードを読み取る場合、またはデータを JS で生バイトとして利用できる場合に使用できます。 Blazor Server で大規模なバイナリ ペイロードを送信する、InputFile コンポーネントに似た手法を使用する方法の例については、Binary Submit サンプル アプリを参照してください。

注意

ASP.NET Core 参照ソースへのドキュメント リンクを使用すると、リポジトリの main ブランチが読み込まれます。このブランチは、ASP.NET Core の次回リリースに向けて行われている製品単位の現在の開発を表します。 別のリリースのブランチを選択するには、 [Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使用して、そのブランチを選択します。 たとえば、ASP.NET Core 5.0 リリースの場合は、release/5.0 ブランチを選択します。

Blazor Server アプリで JS と Blazor の間で大量のデータを転送するコードを開発するときは、次のガイダンスを考慮してください。

  • データをより小さな部分にスライスし、すべてのデータがサーバーによって受信されるまでデータ セグメントを順番に送信します。
  • JS および C# コードで大きなオブジェクトを割り当てないでください。
  • データを送受信するときに、メイン UI スレッドを長時間ブロックしないでください。
  • プロセスの完了時またはキャンセル時に、消費していたメモリを解放します。
  • セキュリティ上の理由から、次の追加要件を適用します。
    • 渡すことのできるファイルまたはデータの最大サイズを宣言します。
    • クライアントからサーバーへの最小アップロード レートを宣言します。
  • データがサーバーによって受信されたら、データは:
    • すべてのセグメントが収集されるまで、一時的にメモリ バッファーに格納できます。
    • 直ちに消費できます。 たとえば、データは、データベースに直ちに格納することも、セグメントを受信するたびにディスクに書き込むこともできます。

マーシャリングされていない JavaScript 相互運用

.NET オブジェクトが JavaScript (JS) 相互運用のためにシリアル化され、次のいずれかに該当する場合、Blazor WebAssembly コンポーネントのパフォーマンスが低下するおそれがあります。

  • 大量の .NET オブジェクトが短時間でシリアル化される。 たとえば、JS 相互運用の呼び出しが、マウス ホイールの回転など、入力デバイスの動きに基づいて行われるときは、パフォーマンスが低下する場合があります。
  • 大きな .NET オブジェクトまたは多数の .NET オブジェクトを JS 相互運用のためにシリアル化する必要がある。 たとえば、JS 相互運用の呼び出しで数十個のファイルをシリアル化する必要があるときは、パフォーマンスが低下する場合があります。

IJSUnmarshalledObjectReference は、.NET データをシリアル化するオーバーヘッドなしで関数を呼び出すことができる JS オブジェクトへの参照を表します。

次に例を示します。

  • 文字列と整数を含む struct は、シリアル化されずに JS に渡されます。
  • JS 関数によってデータが処理され、ブール値または文字列が呼び出し元に返されます。
  • JS の文字列を .NET の string オブジェクトに直接変換することはできません。 unmarshalledFunctionReturnString 関数によって BINDING.js_string_to_mono_string が呼び出され、JS の文字列の変換が管理されます。

注意

次の例は、JS に渡された struct によってコンポーネントのパフォーマンスは低下しないので、このシナリオの一般的な使用例ではありません。 この例では、シリアル化されていない .NET データを渡すための概念を示すためだけに、小さなオブジェクトを使用しています。

次の <script> ブロックを、wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) に配置します。 または、<script src="{SCRIPT PATH AND FILE NAME (.js)}></script> を使用して終了 </body> タグ内で参照される外部の JS ファイルに、JS を配置することもできます。{SCRIPT PATH AND FILE NAME (.js)} プレースホルダーは、スクリプトのパスとファイル名です。

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

警告

js_string_to_mono_string 関数の名前、動作、および存在は、.NET の今後のリリースで変更される可能性があります。 次に例を示します。

  • 関数の名前が変更される可能性があります。
  • フレームワークによる文字列の自動変換を優先して、関数自体が削除される可能性があります。

Pages/CallJsExample10.razor:

@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

IJSUnmarshalledObjectReference インスタンスが C# コードで破棄されない場合は、JS で破棄することができます。 次の dispose 関数を使用すると、JS から呼び出されたときにオブジェクト参照が破棄されます。

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

配列型は、js_typed_array_to_array を使用して JS オブジェクトから .NET オブジェクトに変換できますが、JS の配列は型指定された配列である必要があります。 JS の配列は、C# コードで .NET オブジェクト配列 (object[]) として読み取ることができます。

文字列配列などの他のデータ型は変換できますが、新しい Mono 配列オブジェクト (mono_obj_array_new) を作成し、その値 (mono_obj_array_set) を設定する必要があります。

警告

js_typed_array_to_arraymono_obj_array_newmono_obj_array_set など、Blazor フレームワークによって提供される JS 関数は、.NET の今後のリリースで、名前の変更、動作の変更、または削除の対象となる場合があります。

JavaScript の例外をキャッチする

JS の例外をキャッチするには、JS の相互運用を try-catch ブロックにラップし、JSException をキャッチします。

次の例では、nonFunction JS 関数は存在しません。 関数が見つからないとき、次のエラーを示す Message を含む JSException がトラップされます。

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

その他のリソース

この記事では、.NET から JavaScript (JS) 関数を呼び出す方法について説明します。 JS から .NET メソッドを呼び出す方法については、「ASP.NET Core Blazor で JavaScript 関数から .NET メソッドを呼び出す」を参照してください。

.NET から JS を呼び出すには、IJSRuntime 抽象化を挿入し、次のいずれかのメソッドを呼び出します。

JS 関数を呼び出す上記の .NET メソッドについて:

  • 関数の識別子 (String) は、グローバル スコープ (window) を基準とするものです。 window.someScope.someFunction を呼び出す場合の識別子は someScope.someFunction です。 関数は、呼び出す前に登録する必要はありません。
  • Object[] で任意の数の JSON シリアル化可能な引数を JS 関数に渡します。
  • キャンセル トークン (CancellationToken) により、操作を取り消す必要があることを示す通知が伝達されます。
  • TimeSpan は、JS 操作の時間制限を表します。
  • また、戻り値の型 TValue も JSON シリアル化可能である必要があります。 TValue は、返される JSON 型に最適に対応する .NET 型と一致する必要があります。
  • InvokeAsync メソッドに対しては JS Promise が返されます。 InvokeAsync によって Promise のラップが解除され、Promise によって待機されている値が返されます。

Blazor Server アプリでプリレンダリングが有効になっている場合、最初のプリレンダリング中に JS を呼び出すことはできません。 JS 相互運用呼び出しは、ブラウザーとの接続が確立されるまで遅延させる必要があります。 詳細については、「Blazor Server アプリがプリレンダリングされていることを検出する」セクションを参照してください。

次の例は、JS ベースのデコーダーである TextDecoder に基づいています。 この例では、開発者コードから既存の JS API に要件をオフロードする C# メソッドから JS 関数を呼び出す方法を示します。 JS 関数は、C# メソッドからバイト配列を受け取り、配列をデコードし、テキストをコンポーネントに返して表示できるようにします。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部に、次の JS コードを追加します。

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    console.log(decodedArray);
    return decodedArray;
  };
</script>

次の CallJsExample1 コンポーネントでは、次を実行します。

  • ボタン ( Convert Array ) を選択するときに、InvokeAsync を使用して convertArray JS 関数を呼び出します。
  • JS 関数が呼び出された後、渡された配列が文字列に変換されます。 文字列は、表示できるようにコンポーネントに返されます (text)。

Pages/CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new MarkupString(await JS.InvokeAsync<string>("convertArray", 
            quoteArray));
    }
}

返された値を読み取らずに JavaScript 関数を呼び出す (InvokeVoidAsync)

次の場合に InvokeVoidAsync を使用します。

  • JS の呼び出しの結果を読み取るために .NET が必要ではない。
  • JS 関数で void(0)/void 0 または undefined が返される。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert1 JS 関数を提供します。 関数は InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

コンポーネント (.razor) の例 (InvokeVoidAsync)

TickerChanged により、次の CallJsExample2 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

クラス (.cs) の例 (InvokeVoidAsync)

JsInteropClasses1.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged により、次の CallJsExample3 コンポーネント内の handleTickerChanged1 メソッドが呼び出されます。

Pages/CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses1(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

JavaScript 関数を呼び出し、戻り値を読み取る (InvokeAsync)

.NET で JS の呼び出しの結果を読み取る必要があるときは、InvokeAsync を使用します。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内部で、displayTickerAlert2 JS 関数を提供します。 次の例では、呼び出し元で表示する文字列が返されます。

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

コンポーネント (.razor) の例 (InvokeAsync)

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample4 コンポーネントに表示されます。

Pages/CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

クラス (.cs) の例 (InvokeAsync)

JsInteropClasses2.cs:

using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged によって handleTickerChanged2 メソッドが呼び出され、返された文字列が次の CallJsExample5 コンポーネントに表示されます。

Pages/CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses2(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

コンテンツの動的生成のシナリオ

BuildRenderTree でコンテンツを動的に生成するには、[Inject] 属性を使用します。

[Inject]
IJSRuntime JS { get; set; }

Blazor Server アプリがプリレンダリングされていることを検出する

このセクションは Razor コンポーネントをプリレンダリングする Blazor Server およびホストされている Blazor WebAssembly アプリに適用されます。プリレンダリングは ASP.NET Core Razor コンポーネントのプリレンダリングと統合を行う で取り扱われています。

アプリでプリレンダリングするとき、JavaScript の呼び出しなどの特定のアクションは不可能です。 コンポーネントは、プリレンダリング時に異なるレンダリングが必要になる場合があります。

次の例の場合、setElementText1 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は JSRuntimeExtensions.InvokeVoidAsync を指定して呼び出され、値を返しません。

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

このような呼び出しの動作が保証されるまで JavaScript 相互運用呼び出しを遅延させるには、OnAfterRender{Async} ライフサイクル イベントをオーバーライドします。 このイベントは、アプリが完全にレンダリングされた後にのみ呼び出されます。

Pages/PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText1 = (element, text) => element.innerText = text;

次のコンポーネントは、プリレンダリングと互換性のある方法で、コンポーネントの初期化ロジックの一部として JavaScript の相互運用を使用する方法を示しています。 コンポーネントには、OnAfterRenderAsync 内からレンダリングの更新をトリガーできることが示されています。 開発者はこのシナリオで無限ループを作成しないように注意する必要があります。

次の例の場合、setElementText2 関数は wwwroot/index.html (Blazor WebAssembly) または Pages/_Layout.cshtml (Blazor Server) の <head> 要素内に配置されます。 関数は IJSRuntime.InvokeAsync を指定して呼び出され、次の値を返します。

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

警告

前の例では、デモンストレーションのみを目的として、ドキュメント オブジェクト モデル (DOM) を直接変更しています。 JavaScript での DOM の直接変更は、ほとんどのシナリオでは推奨されません。JavaScript が Blazor の変更追跡に影響する可能性があるためです。 詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

JSRuntime.InvokeAsync が呼び出されるとき、ElementReference は、以前のライフサイクル メソッドではなく OnAfterRenderAsync でのみ使用されます。コンポーネントがレンダリングされるまで JavaScript 要素が存在しないためです。

StateHasChanged は、JavaScript の相互運用呼び出しから取得された新しい状態でコンポーネントを再度レンダリングするために呼び出されます (詳細については、「ASP.NET Core Blazor コンポーネントのレンダリング」を参照してください)。 StateHasChangeddatanull である場合にのみ呼び出されるため、このコードで無限ループが作成されることはありません。

Pages/PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(data ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call:
</p>

<div id="val-set-by-interop" @ref="divElement"></div>

@code {
    private string data;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && data == null)
        {
            data = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

注意

前の例ではグローバル メソッドでクライアントが汚染されます。 実稼働アプリでのより適切なアプローチについては、「JavaScript モジュール内の JavaScript 分離」を参照してください。

例:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

JavaScript の場所

JavaScript (JS) 相互運用性 (相互運用) の概要に関する記事で説明されている方法のいずれかを使用して、JavaScript (JS) コードを読み込みます。

警告

<script> タグは動的に更新できないため、<script> タグをコンポーネント ファイル (.razor) 内に配置しないでください。

要素への参照をキャプチャする

一部の JavaScript (JS) 相互運用シナリオでは、HTML 要素への参照が必要です。 たとえば、UI ライブラリで初期化のための要素参照が必要な場合、clickplay などの要素でコマンドのような API の呼び出し必要になる可能性があります。

次の方法を使用して、コンポーネント内の HTML 要素への参照をキャプチャします。

  • @ref 属性を HTML 要素に追加します。
  • 名前が @ref 属性の値に一致する ElementReference 型のフィールドを定義します。

次の例は、username <input> 要素への参照をキャプチャする方法を示しています。

<input @ref="username" ... />

@code {
    private ElementReference username;
}

警告

Blazor とやりとりしない空の要素のコンテンツを変化させるには、要素参照のみを使用します。 このシナリオは、サードパーティの API から要素にコンテンツが提供される場合に便利です。 Blazor は要素とやりとりしないため、Blazor の要素表現とドキュメント オブジェクト モデル (DOM) との間に競合が発生する可能性がありません。

次の例では、Blazor が DOM とやりとりしてこの要素のリスト項目 (<li>) を Todos オブジェクトから設定するため、順序なしリスト (ul) のコンテンツを変化させるのは "危険" です。

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

JS 相互運用により要素 MyList のコンテンツが変更され、Blazor でその要素に差分を適用しようとした場合、差分は DOM と一致しません。

詳細については、「Blazor JavaScript の相互運用性 (JS 相互運用)」を参照してください。

ElementReference は JS 相互運用経由で JS コードに渡されます。 JS コードが HTMLElement インスタンスを受け取り、通常の DOM API で使用できます。 たとえば、次のコードでは、要素にマウス クリックを送信できるようにする .NET 拡張メソッド (TriggerClickEvent) を定義しています。

JS 関数 clickElement により、渡された HTML 要素 (element) で click イベントが作成されます。

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

値を返さない JS 関数を呼び出すには、JSRuntimeExtensions.InvokeVoidAsync を使用します。 次のコードは、キャプチャされた ElementReference で前述の JS 関数を呼び出し、クライアント側の click イベントをトリガーします。

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

拡張メソッドを使用するには、IJSRuntime インスタンスを受け取る静的拡張メソッドを作成します。

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

clickElement メソッドは、オブジェクトで直接呼び出されます。 次の例では、TriggerClickEvent メソッドが JsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

重要

exampleButton 変数は、コンポーネントがレンダリングされた後にのみ設定されます。 未設定の ElementReference が JS コードに渡された場合、JS コードは null の値を受け取ります。 コンポーネントのレンダリングが完了した後で要素の参照を操作するには、OnAfterRenderAsync または OnAfterRender コンポーネント ライフサイクル メソッドを使用します。

ジェネリック型を操作して値を返す場合は、ValueTask<TResult> を使用します。

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

{JAVASCRIPT FUNCTION} プレースホルダーは、JS 関数識別子です。

GenericMethod は、型を持つオブジェクトで直接呼び出されます。 次の例では、GenericMethodJsInteropClasses 名前空間から使用できることを前提としています。

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

コンポーネント間で要素を参照する

ElementReference は次の理由からコンポーネントで渡すことができません。

親コンポーネントが要素参照を他のコンポーネントで使用できるようにするために、親コンポーネントは次のことを実行できます。

  • 子コンポーネントがコールバックを登録できるようにします。
  • 渡された要素参照を使用して、登録されたコールバックをOnAfterRender イベント中に呼び出します。 間接的には、この方法により、子コンポーネントが親の要素参照とやりとりできるようになります。

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の <head> に、次のスタイルを追加します。

<style>
    .red { color: red }
</style>

wwwroot/index.html (Blazor WebAssembly) または Pages/_Host.cshtml (Blazor Server) の終了 </body> タグの内側に、次のスクリプトを追加します。

<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

Pages/CallJsExample7.razor (親コンポーネント):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="@this" Title="How is Blazor working for you?" />

Pages/CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

前の例で、アプリの名前空間は Pages フォルダー内にコンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

Shared/SurveyPrompt.razor (子コンポーネント):

@inject IJSRuntime JS

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

Shared/SurveyPrompt.razor.cs:

using System;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

前の例で、アプリの名前空間は Shared フォルダー内に共有コンポーネントがある BlazorSample です。 コードをローカル環境でテストする場合は、名前空間を更新します。

JavaScript 相互運用の呼び出しを強化する

このセクションは主に Blazor Server アプリに適用されますが、条件が許せば、Blazor WebAssembly アプリでも JS 相互運用のタイムアウトを設定できます。

Blazor Server アプリでの JavaScript (JS) 相互運用は、ネットワーク エラーにより失敗するおそれがあるため、信頼性の低いものとして扱う必要があります。 既定では、Blazor Server アプリにより JS 相互運用の呼び出しに対して 1 分間のタイムアウトが使用されます。 アプリでより積極的なタイムアウトが許容される場合は、次のいずれかの方法を使用してタイムアウトを設定します。

CircuitOptions.JSInteropDefaultCallTimeout を使用して Startup.csStartup.ConfigureServices メソッドでグローバル タイムアウトを設定します。

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

{TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。

コンポーネントのコードで呼び出しごとにタイムアウトを設定します。 この方法で指定したタイムアウトは、JSInteropDefaultCallTimeout によって設定されたグローバル タイムアウトを上書きします。

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

前の例の場合:

  • {TIMEOUT} プレースホルダーは、TimeSpanです (たとえば、TimeSpan.FromSeconds(80))。
  • {ID} プレースホルダーは、呼び出す関数の識別子です。 たとえば、値 someScope.someFunction を指定すると、関数 window.someScope.someFunction が呼び出されます。

JS 相互運用のエラーの一般的な原因は、Blazor Server アプリでのネットワーク障害ですが、Blazor WebAssembly アプリで JS 相互運用の呼び出しに対し、呼び出しごとのタイムアウトを設定できます。 Blazor WebAssembly アプリには SignalR 回線は存在しませんが、Blazor WebAssembly アプリに適用される他の理由により、JS 相互運用の呼び出しが失敗するおそれがあります。

リソース枯渇の詳細については、「ASP.NET Core Blazor Server の脅威の緩和のガイダンス」を参照してください。

循環オブジェクト参照の回避

循環参照を含むオブジェクトは、次のいずれに対しても、クライアントでシリアル化することはできません。

  • .NET メソッドの呼び出し。
  • 戻り値の型に循環参照がある場合の、C# からの JavaScript メソッドの呼び出し。

JavaScript 相互運用呼び出しのサイズ制限

"このセクションは、Blazor Server アプリにのみ適用されます。Blazor WebAssembly の場合、フレームワークでは JavaScript (JS) 相互運用機能の入力と出力のサイズに制限を課しません。 "

Blazor Server では、ハブ メソッドで許可される SignalR 受信メッセージの最大サイズによって、JS 相互運用呼び出しのサイズが制限されます。これは、HubOptions.MaximumReceiveMessageSize によって適用されます (既定値: 32 KB)。 JS から .NET への SignalR メッセージが MaximumReceiveMessageSize より大きい場合は、エラーがスローされます。 このフレームワークでは、ハブからクライアントへの SignalR メッセージのサイズが制限されることはありません。

SignalR のログがDebug または Trace に設定されていない場合、メッセージ サイズのエラーはブラウザーの開発者ツール コンソールにのみ表示されます。

エラー :次のエラーで接続が切断されました。"エラー: サーバーが終了時にエラーを返しました:接続はエラーで終了しました。"

SignalR サーバー側のログ Debug または Trace に設定されている場合、サーバー側のログには、メッセージ サイズ エラーの InvalidDataException が表示されます。

appsettings.Development.json:

{
  "DetailedErrors": true,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "Microsoft.AspNetCore.SignalR": "Debug"
    }
  }
}

エラー:

System.IO.InvalidDataException:メッセージの最大サイズ 32,768 B を超えました。 メッセージのサイズは、AddHubOptions で構成できます。

制限値を増やすには、Startup.ConfigureServicesMaximumReceiveMessageSize を設定します。 次の例では、受信メッセージの最大サイズを 64 KB (64 * 1024) に設定します。

services.AddServerSideBlazor()
   .AddHubOptions(options => options.MaximumReceiveMessageSize = 64 * 1024);

SignalR 受信メッセージ サイズの制限値を増やすと、より多くのサーバー リソースが必要になり、悪意のあるユーザーからのより大きなリスクにサーバーがさらされます。 また、大量のコンテンツを文字列またはバイト配列としてメモリに読み取ると、ガベージ コレクターがうまく機能しない割り当てが発生する可能性もあり、その結果、パフォーマンスがさらに低下します。

大きなペイロードを読み取るための 1 つの選択肢は、小さいチャンクでコンテンツを送信し、ペイロードを Stream として処理することです。 これは、大量の JSON ペイロードを読み取る場合、またはデータを JS で生バイトとして利用できる場合に使用できます。 Blazor Server で大規模なバイナリ ペイロードを送信する、InputFile コンポーネントに似た手法を使用する方法の例については、Binary Submit サンプル アプリを参照してください。

注意

ASP.NET Core 参照ソースへのドキュメント リンクを使用すると、リポジトリの main ブランチが読み込まれます。このブランチは、ASP.NET Core の次回リリースに向けて行われている製品単位の現在の開発を表します。 別のリリースのブランチを選択するには、 [Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使用して、そのブランチを選択します。 たとえば、ASP.NET Core 5.0 リリースの場合は、release/5.0 ブランチを選択します。

Blazor Server アプリで JS と Blazor の間で大量のデータを転送するコードを開発するときは、次のガイダンスを考慮してください。

  • データをより小さな部分にスライスし、すべてのデータがサーバーによって受信されるまでデータ セグメントを順番に送信します。
  • JS および C# コードで大きなオブジェクトを割り当てないでください。
  • データを送受信するときに、メイン UI スレッドを長時間ブロックしないでください。
  • プロセスの完了時またはキャンセル時に、消費していたメモリを解放します。
  • セキュリティ上の理由から、次の追加要件を適用します。
    • 渡すことのできるファイルまたはデータの最大サイズを宣言します。
    • クライアントからサーバーへの最小アップロード レートを宣言します。
  • データがサーバーによって受信されたら、データは:
    • すべてのセグメントが収集されるまで、一時的にメモリ バッファーに格納できます。
    • 直ちに消費できます。 たとえば、データは、データベースに直ちに格納することも、セグメントを受信するたびにディスクに書き込むこともできます。

JavaScript の例外をキャッチする

JS の例外をキャッチするには、JS の相互運用を try-catch ブロックにラップし、JSException をキャッチします。

次の例では、nonFunction JS 関数は存在しません。 関数が見つからないとき、次のエラーを示す Message を含む JSException がトラップされます。

Could not find 'nonFunction' ('nonFunction' was undefined).

Pages/CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

その他のリソース