Вызов функций JavaScript из методов .NET в ASP.NET Core Blazor

В этой статье рассматривается вызов функций JavaScript (JS) из .NET. Сведения о том, как вызывать методы .NET из JS, см. в статье Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor.

Для вызова JS из .NET внедрите абстракцию IJSRuntime и вызовите один из следующих методов:

Для предыдущих методов .NET, которые вызывают функции JS:

  • Идентификатор функции (String) задается относительно глобальной области (window). Чтобы вызвать функцию window.someScope.someFunction, необходимо использовать идентификатор someScope.someFunction. Регистрировать функцию перед ее вызовом не требуется.
  • Передайте любое количество аргументов, сериализуемых в JSON, в Object[] для функции JS.
  • Токен отмены (CancellationToken) распространяет уведомление о том, что операции должны быть отменены.
  • TimeSpan представляет предельное время для операции JS.
  • Тип возвращаемого значения TValue также должен сериализоваться в JSON. Тип TValue должен соответствовать типу .NET, который лучше всего соответствует возвращаемому типу JSON.
  • JS Promise возвращается для методов InvokeAsync. InvokeAsync распаковывает Promise и возвращает значение, ожидаемое Promise.

Для приложений Blazor Server с включенной предварительной обработкой вызвать JS нельзя во время первоначальной предварительной обработки. Вызовы взаимодействия с JS должны быть отложены до тех пор, пока не будет установлено соединение с браузером. См. раздел Обнаружение предварительной обработки в приложении Blazor Server.

Приведенный ниже пример основан на TextDecoder, декодере на базе JS. В примере показано, как вызвать функцию JS из метода C#, которая переносит требование из кода разработчика в существующий API JS. Функция JS принимает массив байтов из метода C#, декодирует его и возвращает компоненту текст для отображения.

Добавьте следующий код JS в закрывающий тег </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server):

<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 делает следующее.

  • Вызывает функцию JS convertArray с помощью метода InvokeAsync при нажатии кнопки ( Convert Array ).
  • После вызова функции 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 следует использовать в следующих случаях:

  • если .NET не нужно считывать результат вызова JS;
  • для функций JS, возвращающих значение void(0)/void 0 или undefined.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server) укажите функцию JS displayTickerAlert1. Функция вызывается с помощью метода InvokeVoidAsync и не возвращает значение:

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

Пример (InvokeVoidAsync) компонента (.razor)

TickerChanged вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample2.

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

Пример (InvokeVoidAsync) класса (.cs)

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 вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample3.

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)

Используйте InvokeAsync, если .NET нужно считать результат вызова JS.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server) укажите функцию JS displayTickerAlert2. В следующем примере возвращается строка для отображения вызывающим объектом:

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

Пример (InvokeAsync) компонента (.razor)

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}";
    }
}

Пример (InvokeAsync) класса (.cs)

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

Этот раздел относится к приложениям Blazor Server и размещенным приложениям Blazor WebAssembly с предварительной отрисовкой компонентов Razor. Предварительная отрисовка рассматривается здесь: Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

Когда приложение выполняет предварительную отрисовку, некоторые действия, такие как вызов в JavaScript, невозможны. При предварительной отрисовке компоненты могут отрисовываться иначе.

В следующем примере функция setElementText1 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода JSRuntimeExtensions.InvokeVoidAsync и не возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку 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 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода IJSRuntime.InvokeAsync и возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку JavaScript может повлиять на отслеживание изменений Blazor. Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

При вызове JSRuntime.InvokeAsync структура ElementReference используется только в методе OnAfterRenderAsync, а не в предыдущем методе жизненного цикла, так как элемент JavaScript появляется только после отрисовки компонента.

StateHasChanged вызывается для повторной отрисовки компонента с новым состоянием, полученным из вызова взаимодействия с JavaScript (дополнительные сведения см. здесь: Отрисовка компонента Blazor ASP.NET Core). Код не создает бесконечный цикл, так как метод StateHasChanged вызывается, только если data имеет значение null.

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

Расположение кода JavaScipt

Загрузите код JavaScript (JS) любым из методов, описанных в обзорной статье о взаимодействии с JavaScript (JS):

Сведения об изоляции скриптов в модулях JS см. в разделе Изоляция JavaScript в модулях JavaScript.

Предупреждение

Не помещайте тег <script> в файл компонента (.razor), так как тег <script> не может изменяться динамически.

Изоляция JavaScript в модулях JavaScript

Blazor реализует изоляцию JavaScript (JS) в стандартных модулях JavaScript (см. спецификацию ECMAScript).

Изоляция JS обеспечивает следующие преимущества:

  • Импортированный JS не засоряет глобальное пространство имен.
  • Пользователям библиотеки и компонентов не требуется импортировать связанный код JS.

Например, следующий модуль JS экспортирует функцию JS для отображения запроса в окне браузера. Поместите следующий код JS во внешний файл JS.

wwwroot/scripts.js:

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

Добавьте предыдущий модуль JS в приложение или библиотеку классов в виде статического веб-ресурса в папке wwwroot, а затем импортируйте модуль в код .NET, вызвав InvokeAsync для экземпляра IJSRuntime.

IJSRuntime импортирует модуль как IJSObjectReference. Это представление ссылки на объект JS из кода .NET. Используйте IJSObjectReference для вызова экспортированных функций JS из модуля.

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.
  • Укажите внешний файл JS модуля, используя путь к статистическому стабильному веб-ресурсу: ./{SCRIPT PATH AND FILENAME (.js)}, где:
    • Сегмент пути для текущего каталога (./) необходим для создания корректного пути к статическому ресурсу в файле JS.
    • Заполнитель {SCRIPT PATH AND FILENAME (.js)} — это путь и имя файла в папке wwwroot.

Для динамического импорта модуля требуется сетевой запрос, поэтому его можно выполнить только асинхронно, вызвав InvokeAsync.

IJSInProcessObjectReference представляет ссылку на объект JS, функции которого могут вызываться синхронно.

Примечание

Если внешний файл JS предоставляется библиотекой классов Razor, укажите файл JS модуля, используя путь к статическому стабильному веб-ресурсу ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • Сегмент пути для текущего каталога (./) необходим для создания корректного пути к статическому ресурсу в файле JS.
  • Заполнитель {PACKAGE ID} — это идентификатор пакета библиотеки. Идентификатор пакета по умолчанию имеет имя сборки проекта, если значение <PackageId> не указано в файле проекта. В следующем примере имя сборки библиотеки — ComponentLibrary, а в файле проекта библиотеки не указан <PackageId>.
  • Заполнитель {SCRIPT PATH AND FILENAME (.js)} — это путь и имя файла в папке wwwroot. В следующем примере внешний файл JS (script.js) помещается в папку wwwroot библиотеки классов.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

Для получения дополнительной информации см. Использование компонентов Razor ASP.NET Core из библиотеки классов Razor.

Получение ссылок на элементы

В некоторых сценариях взаимодействия с JS требуются ссылки на элементы HTML. Например, ссылка на элемент может требоваться библиотеке пользовательского интерфейса для инициализации либо необходимо вызывать командные интерфейсы API для элемента, такого как click или play.

Для получения ссылок на элементы HTML в компоненте используйте описанный ниже подход.

  • Добавьте атрибут @ref к элементу HTML.
  • Определите поле типа ElementReference, имя которого совпадает со значением атрибута @ref.

В следующем примере показано получение ссылки на элемент username <input>:

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

@code {
    private ElementReference username;
}

Предупреждение

Ссылку на элемент следует использовать только для изменения содержимого пустого элемента, который не взаимодействует с Blazor. Этот сценарий полезен, если сторонний интерфейс API предоставляет содержимое элементу. Так как Blazor не взаимодействует с элементом, риск конфликта между представлением Blazor элемента и моделью DOM отсутствует.

В следующем примере изменять содержимое неупорядоченного списка (ul) опасно, так как Blazor взаимодействует с моделью DOM для заполнения элементов этого списка (<li>) из объекта Todos:

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

Если при взаимодействии с JS содержимое элемента MyList изменяется и Blazor пытается применить изменения к элементу, эти изменения не будут соответствовать модели DOM.

Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

ElementReference передается в код JS посредством взаимодействия с JS. Код HTMLElement получает экземпляр JS, который может использоваться с обычными интерфейсами API DOM. Например, в приведенном ниже коде определяется метод расширения .NET (TriggerClickEvent), который позволяет отправить щелчок мыши в элемент.

Функция JS clickElement создает событие click в переданном элементе HTML (element):

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

Для вызова функции JS, которая не возвращает значение, используйте метод JSRuntimeExtensions.InvokeVoidAsync. Следующий код активирует событие click на стороне клиента, вызывая предыдущую функцию JS с захваченным ElementReference:

@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 заполняется только после отрисовки компонента. Если в код JS передается пустая ссылка ElementReference, код 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 вызывается для объекта с типом напрямую. В следующем примере предполагается, что метод GenericMethod доступен из пространства имен JsInteropClasses:

@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 с помощью переданной ссылки на элемент. Такой подход позволяет дочерним компонентам взаимодействовать со ссылкой на элемент родительского компонента косвенным образом.

Добавьте в тег <head> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server) следующий параметр style:

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

Добавьте следующий скрипт в закрывающий тег </body> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server):

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с компонентами в папке Pages. При локальном тестировании кода обновите пространство имен.

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с общими компонентами в папке Shared. При локальном тестировании кода обновите пространство имен.

Повышение надежности вызовов взаимодействия с JavaScript

Сведения в этом разделе в первую очередь актуальны для приложений Blazor Server. Но приложения Blazor WebAssembly также могут устанавливать время ожидания взаимодействия с JS, если этого требуют условия.

В приложениях Blazor Server взаимодействие с JavaScript (JS) может завершаться сбоем из-за ошибок сети и в этом случае должно считаться ненадежным. По умолчанию приложения Blazor Server используют время ожидания, равное 1 минуте, для вызовов взаимодействия с JS. Если для приложения допустимо более короткое время ожидания, установите его одним из указанных ниже способов.

Задайте глобальное время ожидания в методе Startup.ConfigureServices файла Startup.cs с помощью свойства CircuitOptions.JSInteropDefaultCallTimeout:

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, вы можете задать значения времени ожидания для отдельного вызова JS в приложениях Blazor WebAssembly. Несмотря на то что в приложении Blazor WebAssembly отсутствует канал SignalR, вызовы взаимодействия с JS могут завершиться ошибкой по другим причинам, которые возникают в приложениях Blazor WebAssembly.

Дополнительные сведения о нехватке ресурсов см. в статье Руководство по предотвращению угроз для ASP.NET Core Blazor Server.

Исключение циклических ссылок на объекты

Объекты, содержащие циклические ссылки, не могут быть сериализованы на клиенте для:

  • вызовов метода .NET.
  • Вызов метода JavaScript из C#, если тип возвращаемого значения имеет циклические ссылки.

Библиотеки JavaScript, отображающие пользовательский интерфейс

Иногда может потребоваться использовать библиотеки JavaScript (JS), которые создают видимые элементы пользовательского интерфейса в модели DOM браузера. На первый взгляд, это может показаться затруднительным, так как система сравнения Blazor предполагает наличие контроля над деревом элементов DOM и в ней возникают ошибки, если какой-либо внешний код изменяет дерево DOM и механизм применения различий становится недействительным. Это ограничение не относится лишь к Blazor. Такая же проблема возникает при использовании любой платформы пользовательского интерфейса на основе сравнения.

К счастью, в пользовательский интерфейс компонента Razor можно легко внедрить внешний пользовательский интерфейс. Рекомендуемым методом является создание пустого элемента кодом компонента (в файле .razor). С точки зрения системы сравнения Blazor элемент всегда пуст, поэтому отрисовщик не выполняет рекурсию в него и не обрабатывает его содержимое. Это позволяет спокойно заполнить элемент произвольным содержимым, управляемым извне.

Данный принцип демонстрируется в приведенном ниже примере. В инструкции if, когда firstRender имеет значение true, используйте unmanagedElement за пределами Blazor с помощью взаимодействия JS. Например, вызовите внешнюю библиотеку 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)
        {
            ...
        }
    }
}

Рассмотрим указанный ниже пример, который отрисовывает интерактивную карту с помощью интерфейсов API Mapbox с открытым кодом.

Следующий модуль 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) добавьте в разметку элемента <head> следующий элемент <link>:

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

В предыдущем примере создается интерактивный пользовательский интерфейс карты. Пользователь может выполнять следующие действия:

  • прокручивать карту или изменять масштаб перетаскиванием;
  • переходить к заданным расположениям нажатием кнопок.

Карта улиц Mapbox для Токио (Япония) с кнопками для выбора Бристоля (Великобритания) и Токио (Япония)

В предшествующем примере:

  • В случае с Blazor <div> с @ref="mapElement" остается пустым. С помощью скрипта mapbox-gl.js можно безопасно заполнять элемент и изменять его содержимое. Используйте этот метод с любой библиотекой JS, которая отрисовывает пользовательский интерфейс. В компоненты Blazor можно даже внедрять компоненты из сторонней платформы одностраничных приложений JS, если они не пытаются изменить другие части страницы. Ситуация, когда внешний код JS изменяет элементы, которые Blazor не считает пустыми, небезопасна.
  • При использовании этого подхода необходимо учитывать то, как Blazor удерживает или уничтожает элементы DOM. Компонент безопасно обрабатывает события нажатия кнопок и обновляет существующий экземпляр карты, так как по умолчанию элементы DOM по возможности сохраняются без изменений. При отрисовке списка элементов карты из цикла @foreach необходимо использовать @key, чтобы гарантировать сохранность экземпляров компонента. В противном случае изменения в данных списка могут приводить к нежелательному сохранению состояния предыдущих экземпляров компонента. Дополнительные сведения см. в разделе об использовании @key для сохранения элементов и компонентов.
  • В примере выполняется инкапсуляция логики и зависимостей JS в модуле ES6 и динамическая загрузка модуля с помощью идентификатора import. Дополнительные сведения см. в разделе Изоляция JavaScript в модулях JavaScript.

Поддержка массивов байтов

Blazor поддерживает оптимизированное взаимодействие с массивом байтов JS, которое позволяет избежать кодирования и декодирования массивов байтов в Base64. В следующем примере используется взаимодействие JS для передачи массива байтов в JavaScript.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server) укажите функцию JS receiveByteArray. Функция вызывается с помощью метода 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);
    }
}

Сведения об использовании массива байтов при вызове .NET из JavaScript см. в разделе Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor.

Ограничения размера для вызовов взаимодействия с JavaScript

Этот раздел относится только к приложениям Blazor Server. В Blazor WebAssembly платформа не накладывает ограничений на размер входных и выходных данных в вызовах взаимодействия с JavaScript (JS).

В Blazor Server размер данных в вызовах взаимодействия с JS ограничен максимальным размером входящего сообщения SignalR, разрешенным для методов концентратора, который задается с помощью HubOptions.MaximumReceiveMessageSize (по умолчанию: 32 КБ). Если размер сообщений SignalR JS для .NET превышает MaximumReceiveMessageSize, возникает ошибка. Платформа не накладывает ограничение на размер сообщений SignalR от концентратора клиенту.

Если для ведения журнала SignalR не установлен уровень Отладка или Трассировка, ошибка в связи с недопустимым размером сообщения отображается только в консоли средств разработчика браузера:

Ошибка: Подключение разорвано с ошибкой "Ошибка. Сервер вернул ошибку при закрытии: соединение закрыто с ошибкой".

Если для ведения журнала на стороне сервера SignalR установлен уровень Отладка или Трассировка, функция ведения журнала на стороне сервера предоставляет InvalidDataException для ошибки в связи с недопустимым размером.

appsettings.Development.json:

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

Ошибка:

System.IO.InvalidDataException: Превышен максимальный размер сообщения 32768 Б. Размер сообщения можно настроить в AddHubOptions.

Увеличьте ограничение, настроив MaximumReceiveMessageSize в Startup.ConfigureServices. В следующем примере для размера получаемого сообщения устанавливается максимальный размер 64 КБ (64 * 1024):

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

Увеличение предельного размера для входящих сообщений SignalR потребует больше серверных ресурсов для обработки, а также повысит уязвимость сервера к злонамеренным действиям пользователей. Кроме того, считывание большого объема содержимого в память в виде строк или массивов байтов может привести к неудачным выделениям памяти, которые плохо обрабатываются сборщиком мусора, что дополнительно снизит производительность.

Чтобы считывать полезные данные большого размера, можно отправлять такое содержимое меньшими фрагментами и обрабатывать их как Stream. Это удобно для чтения полезных данных большого объема в формате JSON или необработанных байтов из JS. Вы можете изучить отправку больших двоичных данных в Blazor Server с помощью методов, эквивалентных работе компонента InputFile, в примере приложения с отправкой двоичных данных.

Примечание

По предыдущим ссылкам в документации на справочные материалы по ASP.NET Core загружается ветвь main репозитория, которая представляет текущую разработку единицы продукта для следующего выпуска ASP.NET Core. Чтобы выбрать ветвь для другого выпуска, используйте раскрывающийся список Switch branches/tags (Переключение ветвей или тегов). Например, выберите ветвь release/5.0 для выпуска ASP.NET Core 5.0.

При разработке кода, который передает большие объемы данных между JavaScript и Blazor в приложениях Blazor Server, учитывайте следующие рекомендации:

  • Разделите данные на небольшие части и отправляйте сегменты данных последовательно, пока все данные не будут получены сервером.
  • Не выделяйте большие объекты в коде C# и JS.
  • Не блокируйте основной поток пользовательского интерфейса на длительные периоды при отправке или получении данных.
  • Освободите занятую память при завершении или отмене процесса.
  • Применяйте следующие дополнительные требования в целях безопасности:
    • Объявите максимальный размер файла или данных, который может быть передан.
    • Объявите минимальную скорость передачи от клиента к серверу.
  • После получения данных сервером данные могут быть:
    • Временно сохранены в буфере памяти до тех пор, пока не будут собраны все сегменты.
    • Использованы немедленно. Например, данные могут храниться сразу в базе данных или записываться на диск по мере получения каждого сегмента.

Демаршалированные вызовы взаимодействия с JavaScript

Производительность компонентов Blazor WebAssembly может снизиться при сериализации объектов .NET для взаимодействия с JavaScript (JS) и наличии одного из следующих условий:

  • Быстро сериализуется большой объем объектов .NET. Например, если выполняются вызовы взаимодействия с JS на основе перемещения устройства ввода, например прокрутки колесика мыши, это может снизить производительность.
  • Для взаимодействия с JS нужно сериализовать большие объекты .NET или много объектов .NET. Например, если для вызовов взаимодействия с JS требуется сериализовать десятки файлов, это может снизить производительность.

IJSUnmarshalledObjectReference представляет ссылку на объект JS, функции которого могут вызываться без дополнительных затрат, связанных с сериализацией данных .NET.

В следующем примере:

  • Структура, содержащая строку и целое число, передается в JS без сериализации.
  • Функции JS обрабатывают данные и возвращают вызывающему объекту логическое значение или строку.
  • Строку JS нельзя напрямую преобразовать в объект string .NET. Функция unmarshalledFunctionReturnString вызывает BINDING.js_string_to_mono_string для управления преобразованием строки JS.

Примечание

Следующие примеры не являются типичными вариантами использования для этого сценария, так как структура, передаваемая в JS, не приводит к ухудшению производительности компонента. В примере мы используем небольшой объект, только чтобы продемонстрировать концепцию передачи несериализованных данных .NET.

Поместите следующий блок <script> в wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Кроме того, вы можете поместить JS во внешний файл JS, на который ссылаются в закрывающемся теге </body> с помощью <script src="{SCRIPT PATH AND FILE NAME (.js)}></script>, где заполнитель {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>

Предупреждение

Возможно, в одном из будущих выпусков .NET. имя и поведение функции js_string_to_mono_string изменится либо она будет удалена. Пример:

  • Скорее всего, функция будет переименована.
  • Возможно, сама функция будет удалена, а вместо нее будет реализовано автоматическое преобразование строк платформой.

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 в объекты .NET с помощью js_typed_array_to_array, но при этом массив JS должен быть типизированным. Массивы из JS могут считываться в коде C# как массив объектов .NET (object[]).

Можно преобразовать и другие типы данных, например массивы строк, но при этом нужно создать новый объект массива Mono (mono_obj_array_new) и задать его значение (mono_obj_array_set).

Предупреждение

Возможно, в будущих выпусках .NET. функции JS (такие как js_typed_array_to_array, mono_obj_array_new и mono_obj_array_set), предоставляемые платформой Blazor, будут удалены либо изменятся их имена или поведение.

Потоковая передача из 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 записывается на диск (file.txt) по пути временной папки текущего пользователя (GetTempPath).

Потоковая передача из .NET в JavaScript

Эта функция применима к ASP.NET Core 6.0 (версия-кандидат 1) или более поздней версии. Выпуск ASP.NET Core 6.0 (версия-кандидат 1) запланирован на сентябрь. Выпуск 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} представляет Stream, отправленный в JavaScript.
  • JS является внедренным экземпляром IJSRuntime.

Перехват исключений JavaScript

Чтобы перехватить исключения JS, заключите взаимодействие JS в блок try-catch и перехватите JSException.

В следующем примере функция nonFunction JS не существует. Если функция не найдена, объект JSException перехватывается с помощью Message, который указывает на следующую ошибку:

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}";
        }
    }
}

Дополнительные ресурсы

В этой статье рассматривается вызов функций JavaScript (JS) из .NET. Сведения о том, как вызывать методы .NET из JS, см. в статье Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor.

Для вызова JS из .NET внедрите абстракцию IJSRuntime и вызовите один из следующих методов:

Для предыдущих методов .NET, которые вызывают функции JS:

  • Идентификатор функции (String) задается относительно глобальной области (window). Чтобы вызвать функцию window.someScope.someFunction, необходимо использовать идентификатор someScope.someFunction. Регистрировать функцию перед ее вызовом не требуется.
  • Передайте любое количество аргументов, сериализуемых в JSON, в Object[] для функции JS.
  • Токен отмены (CancellationToken) распространяет уведомление о том, что операции должны быть отменены.
  • TimeSpan представляет предельное время для операции JS.
  • Тип возвращаемого значения TValue также должен сериализоваться в JSON. Тип TValue должен соответствовать типу .NET, который лучше всего соответствует возвращаемому типу JSON.
  • JS Promise возвращается для методов InvokeAsync. InvokeAsync распаковывает Promise и возвращает значение, ожидаемое Promise.

Для приложений Blazor Server с включенной предварительной обработкой вызвать JS нельзя во время первоначальной предварительной обработки. Вызовы взаимодействия с JS должны быть отложены до тех пор, пока не будет установлено соединение с браузером. См. раздел Обнаружение предварительной обработки в приложении Blazor Server.

Приведенный ниже пример основан на TextDecoder, декодере на базе JS. В примере показано, как вызвать функцию JS из метода C#, которая переносит требование из кода разработчика в существующий API JS. Функция JS принимает массив байтов из метода C#, декодирует его и возвращает компоненту текст для отображения.

Добавьте следующий код JS в закрывающий тег </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server):

<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 делает следующее.

  • Вызывает функцию JS convertArray с помощью метода InvokeAsync при нажатии кнопки ( Convert Array ).
  • После вызова функции 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 следует использовать в следующих случаях:

  • если .NET не нужно считывать результат вызова JS;
  • для функций JS, возвращающих значение void(0)/void 0 или undefined.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) укажите функцию JS displayTickerAlert1. Функция вызывается с помощью метода InvokeVoidAsync и не возвращает значение:

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

Пример (InvokeVoidAsync) компонента (.razor)

TickerChanged вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample2.

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

Пример (InvokeVoidAsync) класса (.cs)

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 вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample3.

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)

Используйте InvokeAsync, если .NET нужно считать результат вызова JS.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) укажите функцию JS displayTickerAlert2. В следующем примере возвращается строка для отображения вызывающим объектом:

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

Пример (InvokeAsync) компонента (.razor)

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}";
    }
}

Пример (InvokeAsync) класса (.cs)

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

Этот раздел относится к приложениям Blazor Server и размещенным приложениям Blazor WebAssembly с предварительной отрисовкой компонентов Razor. Предварительная отрисовка рассматривается здесь: Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

Когда приложение выполняет предварительную отрисовку, некоторые действия, такие как вызов в JavaScript, невозможны. При предварительной отрисовке компоненты могут отрисовываться иначе.

В следующем примере функция setElementText1 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода JSRuntimeExtensions.InvokeVoidAsync и не возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку 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 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода IJSRuntime.InvokeAsync и возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку JavaScript может повлиять на отслеживание изменений Blazor. Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

При вызове JSRuntime.InvokeAsync структура ElementReference используется только в методе OnAfterRenderAsync, а не в предыдущем методе жизненного цикла, так как элемент JavaScript появляется только после отрисовки компонента.

StateHasChanged вызывается для повторной отрисовки компонента с новым состоянием, полученным из вызова взаимодействия с JavaScript (дополнительные сведения см. здесь: Отрисовка компонента Blazor ASP.NET Core). Код не создает бесконечный цикл, так как метод StateHasChanged вызывается, только если data имеет значение null.

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

Расположение кода JavaScipt

Загрузите код JavaScript (JS) любым из методов, описанных в обзорной статье о взаимодействии с JavaScript (JS):

Сведения об изоляции скриптов в модулях JS см. в разделе Изоляция JavaScript в модулях JavaScript.

Предупреждение

Не помещайте тег <script> в файл компонента (.razor), так как тег <script> не может изменяться динамически.

Изоляция JavaScript в модулях JavaScript

Blazor реализует изоляцию JavaScript (JS) в стандартных модулях JavaScript (см. спецификацию ECMAScript).

Изоляция JS обеспечивает следующие преимущества:

  • Импортированный JS не засоряет глобальное пространство имен.
  • Пользователям библиотеки и компонентов не требуется импортировать связанный код JS.

Например, следующий модуль JS экспортирует функцию JS для отображения запроса в окне браузера. Поместите следующий код JS во внешний файл JS.

wwwroot/scripts.js:

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

Добавьте предыдущий модуль JS в приложение или библиотеку классов в виде статического веб-ресурса в папке wwwroot, а затем импортируйте модуль в код .NET, вызвав InvokeAsync для экземпляра IJSRuntime.

IJSRuntime импортирует модуль как IJSObjectReference. Это представление ссылки на объект JS из кода .NET. Используйте IJSObjectReference для вызова экспортированных функций JS из модуля.

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.
  • Укажите внешний файл JS модуля, используя путь к статистическому стабильному веб-ресурсу: ./{SCRIPT PATH AND FILENAME (.js)}, где:
    • Сегмент пути для текущего каталога (./) необходим для создания корректного пути к статическому ресурсу в файле JS.
    • Заполнитель {SCRIPT PATH AND FILENAME (.js)} — это путь и имя файла в папке wwwroot.

Для динамического импорта модуля требуется сетевой запрос, поэтому его можно выполнить только асинхронно, вызвав InvokeAsync.

IJSInProcessObjectReference представляет ссылку на объект JS, функции которого могут вызываться синхронно.

Примечание

Если внешний файл JS предоставляется библиотекой классов Razor, укажите файл JS модуля, используя путь к статическому стабильному веб-ресурсу ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • Сегмент пути для текущего каталога (./) необходим для создания корректного пути к статическому ресурсу в файле JS.
  • Заполнитель {PACKAGE ID} — это идентификатор пакета библиотеки. Идентификатор пакета по умолчанию имеет имя сборки проекта, если значение <PackageId> не указано в файле проекта. В следующем примере имя сборки библиотеки — ComponentLibrary, а в файле проекта библиотеки не указан <PackageId>.
  • Заполнитель {SCRIPT PATH AND FILENAME (.js)} — это путь и имя файла в папке wwwroot. В следующем примере внешний файл JS (script.js) помещается в папку wwwroot библиотеки классов.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

Для получения дополнительной информации см. Использование компонентов Razor ASP.NET Core из библиотеки классов Razor.

Получение ссылок на элементы

В некоторых сценариях взаимодействия с JS требуются ссылки на элементы HTML. Например, ссылка на элемент может требоваться библиотеке пользовательского интерфейса для инициализации либо необходимо вызывать командные интерфейсы API для элемента, такого как click или play.

Для получения ссылок на элементы HTML в компоненте используйте описанный ниже подход.

  • Добавьте атрибут @ref к элементу HTML.
  • Определите поле типа ElementReference, имя которого совпадает со значением атрибута @ref.

В следующем примере показано получение ссылки на элемент username <input>:

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

@code {
    private ElementReference username;
}

Предупреждение

Ссылку на элемент следует использовать только для изменения содержимого пустого элемента, который не взаимодействует с Blazor. Этот сценарий полезен, если сторонний интерфейс API предоставляет содержимое элементу. Так как Blazor не взаимодействует с элементом, риск конфликта между представлением Blazor элемента и моделью DOM отсутствует.

В следующем примере изменять содержимое неупорядоченного списка (ul) опасно, так как Blazor взаимодействует с моделью DOM для заполнения элементов этого списка (<li>) из объекта Todos:

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

Если при взаимодействии с JS содержимое элемента MyList изменяется и Blazor пытается применить изменения к элементу, эти изменения не будут соответствовать модели DOM.

Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

ElementReference передается в код JS посредством взаимодействия с JS. Код HTMLElement получает экземпляр JS, который может использоваться с обычными интерфейсами API DOM. Например, в приведенном ниже коде определяется метод расширения .NET (TriggerClickEvent), который позволяет отправить щелчок мыши в элемент.

Функция JS clickElement создает событие click в переданном элементе HTML (element):

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

Для вызова функции JS, которая не возвращает значение, используйте метод JSRuntimeExtensions.InvokeVoidAsync. Следующий код активирует событие click на стороне клиента, вызывая предыдущую функцию JS с захваченным ElementReference:

@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 заполняется только после отрисовки компонента. Если в код JS передается пустая ссылка ElementReference, код 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 вызывается для объекта с типом напрямую. В следующем примере предполагается, что метод GenericMethod доступен из пространства имен JsInteropClasses:

@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 с помощью переданной ссылки на элемент. Такой подход позволяет дочерним компонентам взаимодействовать со ссылкой на элемент родительского компонента косвенным образом.

Добавьте в тег <head> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) следующий параметр style:

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

Добавьте следующий скрипт в закрывающий тег </body> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server):

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с компонентами в папке Pages. При локальном тестировании кода обновите пространство имен.

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с общими компонентами в папке Shared. При локальном тестировании кода обновите пространство имен.

Повышение надежности вызовов взаимодействия с JavaScript

Сведения в этом разделе в первую очередь актуальны для приложений Blazor Server. Но приложения Blazor WebAssembly также могут устанавливать время ожидания взаимодействия с JS, если этого требуют условия.

В приложениях Blazor Server взаимодействие с JavaScript (JS) может завершаться сбоем из-за ошибок сети и в этом случае должно считаться ненадежным. По умолчанию приложения Blazor Server используют время ожидания, равное 1 минуте, для вызовов взаимодействия с JS. Если для приложения допустимо более короткое время ожидания, установите его одним из указанных ниже способов.

Задайте глобальное время ожидания в методе Startup.ConfigureServices файла Startup.cs с помощью свойства CircuitOptions.JSInteropDefaultCallTimeout:

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, вы можете задать значения времени ожидания для отдельного вызова JS в приложениях Blazor WebAssembly. Несмотря на то что в приложении Blazor WebAssembly отсутствует канал SignalR, вызовы взаимодействия с JS могут завершиться ошибкой по другим причинам, которые возникают в приложениях Blazor WebAssembly.

Дополнительные сведения о нехватке ресурсов см. в статье Руководство по предотвращению угроз для ASP.NET Core Blazor Server.

Исключение циклических ссылок на объекты

Объекты, содержащие циклические ссылки, не могут быть сериализованы на клиенте для:

  • вызовов метода .NET.
  • Вызов метода JavaScript из C#, если тип возвращаемого значения имеет циклические ссылки.

Библиотеки JavaScript, отображающие пользовательский интерфейс

Иногда может потребоваться использовать библиотеки JavaScript (JS), которые создают видимые элементы пользовательского интерфейса в модели DOM браузера. На первый взгляд, это может показаться затруднительным, так как система сравнения Blazor предполагает наличие контроля над деревом элементов DOM и в ней возникают ошибки, если какой-либо внешний код изменяет дерево DOM и механизм применения различий становится недействительным. Это ограничение не относится лишь к Blazor. Такая же проблема возникает при использовании любой платформы пользовательского интерфейса на основе сравнения.

К счастью, в пользовательский интерфейс компонента Razor можно легко внедрить внешний пользовательский интерфейс. Рекомендуемым методом является создание пустого элемента кодом компонента (в файле .razor). С точки зрения системы сравнения Blazor элемент всегда пуст, поэтому отрисовщик не выполняет рекурсию в него и не обрабатывает его содержимое. Это позволяет спокойно заполнить элемент произвольным содержимым, управляемым извне.

Данный принцип демонстрируется в приведенном ниже примере. В инструкции if, когда firstRender имеет значение true, используйте unmanagedElement за пределами Blazor с помощью взаимодействия JS. Например, вызовите внешнюю библиотеку 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)
        {
            ...
        }
    }
}

Рассмотрим указанный ниже пример, который отрисовывает интерактивную карту с помощью интерфейсов API Mapbox с открытым кодом.

Следующий модуль 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) добавьте в разметку элемента <head> следующий элемент <link>:

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

В предыдущем примере создается интерактивный пользовательский интерфейс карты. Пользователь может выполнять следующие действия:

  • прокручивать карту или изменять масштаб перетаскиванием;
  • переходить к заданным расположениям нажатием кнопок.

Карта улиц Mapbox для Токио (Япония) с кнопками для выбора Бристоля (Великобритания) и Токио (Япония)

В предшествующем примере:

  • В случае с Blazor <div> с @ref="mapElement" остается пустым. С помощью скрипта mapbox-gl.js можно безопасно заполнять элемент и изменять его содержимое. Используйте этот метод с любой библиотекой JS, которая отрисовывает пользовательский интерфейс. В компоненты Blazor можно даже внедрять компоненты из сторонней платформы одностраничных приложений JS, если они не пытаются изменить другие части страницы. Ситуация, когда внешний код JS изменяет элементы, которые Blazor не считает пустыми, небезопасна.
  • При использовании этого подхода необходимо учитывать то, как Blazor удерживает или уничтожает элементы DOM. Компонент безопасно обрабатывает события нажатия кнопок и обновляет существующий экземпляр карты, так как по умолчанию элементы DOM по возможности сохраняются без изменений. При отрисовке списка элементов карты из цикла @foreach необходимо использовать @key, чтобы гарантировать сохранность экземпляров компонента. В противном случае изменения в данных списка могут приводить к нежелательному сохранению состояния предыдущих экземпляров компонента. Дополнительные сведения см. в разделе об использовании @key для сохранения элементов и компонентов.
  • В примере выполняется инкапсуляция логики и зависимостей JS в модуле ES6 и динамическая загрузка модуля с помощью идентификатора import. Дополнительные сведения см. в разделе Изоляция JavaScript в модулях JavaScript.

Ограничения размера для вызовов взаимодействия с JavaScript

Этот раздел относится только к приложениям Blazor Server. В Blazor WebAssembly платформа не накладывает ограничений на размер входных и выходных данных в вызовах взаимодействия с JavaScript (JS).

В Blazor Server размер данных в вызовах взаимодействия с JS ограничен максимальным размером входящего сообщения SignalR, разрешенным для методов концентратора, который задается с помощью HubOptions.MaximumReceiveMessageSize (по умолчанию: 32 КБ). Если размер сообщений SignalR JS для .NET превышает MaximumReceiveMessageSize, возникает ошибка. Платформа не накладывает ограничение на размер сообщений SignalR от концентратора клиенту.

Если для ведения журнала SignalR не установлен уровень Отладка или Трассировка, ошибка в связи с недопустимым размером сообщения отображается только в консоли средств разработчика браузера:

Ошибка: Подключение разорвано с ошибкой "Ошибка. Сервер вернул ошибку при закрытии: соединение закрыто с ошибкой".

Если для ведения журнала на стороне сервера SignalR установлен уровень Отладка или Трассировка, функция ведения журнала на стороне сервера предоставляет InvalidDataException для ошибки в связи с недопустимым размером.

appsettings.Development.json:

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

Ошибка:

System.IO.InvalidDataException: Превышен максимальный размер сообщения 32768 Б. Размер сообщения можно настроить в AddHubOptions.

Увеличьте ограничение, настроив MaximumReceiveMessageSize в Startup.ConfigureServices. В следующем примере для размера получаемого сообщения устанавливается максимальный размер 64 КБ (64 * 1024):

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

Увеличение предельного размера для входящих сообщений SignalR потребует больше серверных ресурсов для обработки, а также повысит уязвимость сервера к злонамеренным действиям пользователей. Кроме того, считывание большого объема содержимого в память в виде строк или массивов байтов может привести к неудачным выделениям памяти, которые плохо обрабатываются сборщиком мусора, что дополнительно снизит производительность.

Чтобы считывать полезные данные большого размера, можно отправлять такое содержимое меньшими фрагментами и обрабатывать их как Stream. Это удобно для чтения полезных данных большого объема в формате JSON или необработанных байтов из JS. Вы можете изучить отправку больших двоичных данных в Blazor Server с помощью методов, эквивалентных работе компонента InputFile, в примере приложения с отправкой двоичных данных.

Примечание

По предыдущим ссылкам в документации на справочные материалы по ASP.NET Core загружается ветвь main репозитория, которая представляет текущую разработку единицы продукта для следующего выпуска ASP.NET Core. Чтобы выбрать ветвь для другого выпуска, используйте раскрывающийся список Switch branches/tags (Переключение ветвей или тегов). Например, выберите ветвь release/5.0 для выпуска ASP.NET Core 5.0.

При разработке кода, который передает большие объемы данных между JavaScript и Blazor в приложениях Blazor Server, учитывайте следующие рекомендации:

  • Разделите данные на небольшие части и отправляйте сегменты данных последовательно, пока все данные не будут получены сервером.
  • Не выделяйте большие объекты в коде C# и JS.
  • Не блокируйте основной поток пользовательского интерфейса на длительные периоды при отправке или получении данных.
  • Освободите занятую память при завершении или отмене процесса.
  • Применяйте следующие дополнительные требования в целях безопасности:
    • Объявите максимальный размер файла или данных, который может быть передан.
    • Объявите минимальную скорость передачи от клиента к серверу.
  • После получения данных сервером данные могут быть:
    • Временно сохранены в буфере памяти до тех пор, пока не будут собраны все сегменты.
    • Использованы немедленно. Например, данные могут храниться сразу в базе данных или записываться на диск по мере получения каждого сегмента.

Демаршалированные вызовы взаимодействия с JavaScript

Производительность компонентов Blazor WebAssembly может снизиться при сериализации объектов .NET для взаимодействия с JavaScript (JS) и наличии одного из следующих условий:

  • Быстро сериализуется большой объем объектов .NET. Например, если выполняются вызовы взаимодействия с JS на основе перемещения устройства ввода, например прокрутки колесика мыши, это может снизить производительность.
  • Для взаимодействия с JS нужно сериализовать большие объекты .NET или много объектов .NET. Например, если для вызовов взаимодействия с JS требуется сериализовать десятки файлов, это может снизить производительность.

IJSUnmarshalledObjectReference представляет ссылку на объект JS, функции которого могут вызываться без дополнительных затрат, связанных с сериализацией данных .NET.

В следующем примере:

  • Структура, содержащая строку и целое число, передается в JS без сериализации.
  • Функции JS обрабатывают данные и возвращают вызывающему объекту логическое значение или строку.
  • Строку JS нельзя напрямую преобразовать в объект string .NET. Функция unmarshalledFunctionReturnString вызывает BINDING.js_string_to_mono_string для управления преобразованием строки JS.

Примечание

Следующие примеры не являются типичными вариантами использования для этого сценария, так как структура, передаваемая в JS, не приводит к ухудшению производительности компонента. В примере мы используем небольшой объект, только чтобы продемонстрировать концепцию передачи несериализованных данных .NET.

Поместите следующий блок <script> в wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server). Кроме того, вы можете поместить JS во внешний файл JS, на который ссылаются в закрывающемся теге </body> с помощью <script src="{SCRIPT PATH AND FILE NAME (.js)}></script>, где заполнитель {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>

Предупреждение

Возможно, в одном из будущих выпусков .NET. имя и поведение функции js_string_to_mono_string изменится либо она будет удалена. Пример:

  • Скорее всего, функция будет переименована.
  • Возможно, сама функция будет удалена, а вместо нее будет реализовано автоматическое преобразование строк платформой.

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 в объекты .NET с помощью js_typed_array_to_array, но при этом массив JS должен быть типизированным. Массивы из JS могут считываться в коде C# как массив объектов .NET (object[]).

Можно преобразовать и другие типы данных, например массивы строк, но при этом нужно создать новый объект массива Mono (mono_obj_array_new) и задать его значение (mono_obj_array_set).

Предупреждение

Возможно, в будущих выпусках .NET. функции JS (такие как js_typed_array_to_array, mono_obj_array_new и mono_obj_array_set), предоставляемые платформой Blazor, будут удалены либо изменятся их имена или поведение.

Перехват исключений JavaScript

Чтобы перехватить исключения JS, заключите взаимодействие JS в блок try-catch и перехватите JSException.

В следующем примере функция nonFunction JS не существует. Если функция не найдена, объект JSException перехватывается с помощью Message, который указывает на следующую ошибку:

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}";
        }
    }
}

Дополнительные ресурсы

В этой статье рассматривается вызов функций JavaScript (JS) из .NET. Сведения о том, как вызывать методы .NET из JS, см. в статье Вызов методов .NET из функций JavaScript в ASP.NET Core Blazor.

Для вызова JS из .NET внедрите абстракцию IJSRuntime и вызовите один из следующих методов:

Для предыдущих методов .NET, которые вызывают функции JS:

  • Идентификатор функции (String) задается относительно глобальной области (window). Чтобы вызвать функцию window.someScope.someFunction, необходимо использовать идентификатор someScope.someFunction. Регистрировать функцию перед ее вызовом не требуется.
  • Передайте любое количество аргументов, сериализуемых в JSON, в Object[] для функции JS.
  • Токен отмены (CancellationToken) распространяет уведомление о том, что операции должны быть отменены.
  • TimeSpan представляет предельное время для операции JS.
  • Тип возвращаемого значения TValue также должен сериализоваться в JSON. Тип TValue должен соответствовать типу .NET, который лучше всего соответствует возвращаемому типу JSON.
  • JS Promise возвращается для методов InvokeAsync. InvokeAsync распаковывает Promise и возвращает значение, ожидаемое Promise.

Для приложений Blazor Server с включенной предварительной обработкой вызвать JS нельзя во время первоначальной предварительной обработки. Вызовы взаимодействия с JS должны быть отложены до тех пор, пока не будет установлено соединение с браузером. См. раздел Обнаружение предварительной обработки в приложении Blazor Server.

Приведенный ниже пример основан на TextDecoder, декодере на базе JS. В примере показано, как вызвать функцию JS из метода C#, которая переносит требование из кода разработчика в существующий API JS. Функция JS принимает массив байтов из метода C#, декодирует его и возвращает компоненту текст для отображения.

Добавьте следующий код JS в закрывающий тег </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server):

<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 делает следующее.

  • Вызывает функцию JS convertArray с помощью метода InvokeAsync при нажатии кнопки ( Convert Array ).
  • После вызова функции 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 следует использовать в следующих случаях:

  • если .NET не нужно считывать результат вызова JS;
  • для функций JS, возвращающих значение void(0)/void 0 или undefined.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) укажите функцию JS displayTickerAlert1. Функция вызывается с помощью метода InvokeVoidAsync и не возвращает значение:

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

Пример (InvokeVoidAsync) компонента (.razor)

TickerChanged вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample2.

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

Пример (InvokeVoidAsync) класса (.cs)

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 вызывает метод handleTickerChanged1 в следующем компоненте CallJsExample3.

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)

Используйте InvokeAsync, если .NET нужно считать результат вызова JS.

Внутри закрывающего тега </body> для wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) укажите функцию JS displayTickerAlert2. В следующем примере возвращается строка для отображения вызывающим объектом:

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

Пример (InvokeAsync) компонента (.razor)

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}";
    }
}

Пример (InvokeAsync) класса (.cs)

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

Этот раздел относится к приложениям Blazor Server и размещенным приложениям Blazor WebAssembly с предварительной отрисовкой компонентов Razor. Предварительная отрисовка рассматривается здесь: Компоненты Razor для предварительной визуализации и интеграции ASP.NET Core.

Когда приложение выполняет предварительную отрисовку, некоторые действия, такие как вызов в JavaScript, невозможны. При предварительной отрисовке компоненты могут отрисовываться иначе.

В следующем примере функция setElementText1 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода JSRuntimeExtensions.InvokeVoidAsync и не возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку 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 помещается в элемент <head> в файле wwwroot/index.html (Blazor WebAssembly) или Pages/_Layout.cshtml (Blazor Server). Функция вызывается с помощью метода IJSRuntime.InvokeAsync и возвращает значение:

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

Предупреждение

В предыдущем примере прямое изменение модели DOM показано только в демонстрационных целях. В большинстве сценариев выполнять непосредственное изменение модели DOM с помощью JavaScript не рекомендуется, поскольку JavaScript может повлиять на отслеживание изменений Blazor. Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

При вызове JSRuntime.InvokeAsync структура ElementReference используется только в методе OnAfterRenderAsync, а не в предыдущем методе жизненного цикла, так как элемент JavaScript появляется только после отрисовки компонента.

StateHasChanged вызывается для повторной отрисовки компонента с новым состоянием, полученным из вызова взаимодействия с JavaScript (дополнительные сведения см. здесь: Отрисовка компонента Blazor ASP.NET Core). Код не создает бесконечный цикл, так как метод StateHasChanged вызывается, только если data имеет значение null.

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

Расположение кода JavaScipt

Загрузите код JavaScript (JS) любым из методов, описанных в обзорной статье о взаимодействии с JavaScript (JS):

Предупреждение

Не помещайте тег <script> в файл компонента (.razor), так как тег <script> не может изменяться динамически.

Получение ссылок на элементы

В некоторых сценариях взаимодействия с JS требуются ссылки на элементы HTML. Например, ссылка на элемент может требоваться библиотеке пользовательского интерфейса для инициализации либо необходимо вызывать командные интерфейсы API для элемента, такого как click или play.

Для получения ссылок на элементы HTML в компоненте используйте описанный ниже подход.

  • Добавьте атрибут @ref к элементу HTML.
  • Определите поле типа ElementReference, имя которого совпадает со значением атрибута @ref.

В следующем примере показано получение ссылки на элемент username <input>:

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

@code {
    private ElementReference username;
}

Предупреждение

Ссылку на элемент следует использовать только для изменения содержимого пустого элемента, который не взаимодействует с Blazor. Этот сценарий полезен, если сторонний интерфейс API предоставляет содержимое элементу. Так как Blazor не взаимодействует с элементом, риск конфликта между представлением Blazor элемента и моделью DOM отсутствует.

В следующем примере изменять содержимое неупорядоченного списка (ul) опасно, так как Blazor взаимодействует с моделью DOM для заполнения элементов этого списка (<li>) из объекта Todos:

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

Если при взаимодействии с JS содержимое элемента MyList изменяется и Blazor пытается применить изменения к элементу, эти изменения не будут соответствовать модели DOM.

Для получения дополнительной информации см. Взаимодействие Blazor с JavaScript (JS-взаимодействие).

ElementReference передается в код JS посредством взаимодействия с JS. Код HTMLElement получает экземпляр JS, который может использоваться с обычными интерфейсами API DOM. Например, в приведенном ниже коде определяется метод расширения .NET (TriggerClickEvent), который позволяет отправить щелчок мыши в элемент.

Функция JS clickElement создает событие click в переданном элементе HTML (element):

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

Для вызова функции JS, которая не возвращает значение, используйте метод JSRuntimeExtensions.InvokeVoidAsync. Следующий код активирует событие click на стороне клиента, вызывая предыдущую функцию JS с захваченным ElementReference:

@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 заполняется только после отрисовки компонента. Если в код JS передается пустая ссылка ElementReference, код 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 вызывается для объекта с типом напрямую. В следующем примере предполагается, что метод GenericMethod доступен из пространства имен JsInteropClasses:

@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 с помощью переданной ссылки на элемент. Такой подход позволяет дочерним компонентам взаимодействовать со ссылкой на элемент родительского компонента косвенным образом.

Добавьте в тег <head> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server) следующий параметр style:

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

Добавьте следующий скрипт в закрывающий тег </body> файла wwwroot/index.html (Blazor WebAssembly) или Pages/_Host.cshtml (Blazor Server):

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с компонентами в папке Pages. При локальном тестировании кода обновите пространство имен.

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

В предыдущем примере в качестве пространства имен приложения указано BlazorSample с общими компонентами в папке Shared. При локальном тестировании кода обновите пространство имен.

Повышение надежности вызовов взаимодействия с JavaScript

Сведения в этом разделе в первую очередь актуальны для приложений Blazor Server. Но приложения Blazor WebAssembly также могут устанавливать время ожидания взаимодействия с JS, если этого требуют условия.

В приложениях Blazor Server взаимодействие с JavaScript (JS) может завершаться сбоем из-за ошибок сети и в этом случае должно считаться ненадежным. По умолчанию приложения Blazor Server используют время ожидания, равное 1 минуте, для вызовов взаимодействия с JS. Если для приложения допустимо более короткое время ожидания, установите его одним из указанных ниже способов.

Задайте глобальное время ожидания в методе Startup.ConfigureServices файла Startup.cs с помощью свойства CircuitOptions.JSInteropDefaultCallTimeout:

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, вы можете задать значения времени ожидания для отдельного вызова JS в приложениях Blazor WebAssembly. Несмотря на то что в приложении Blazor WebAssembly отсутствует канал SignalR, вызовы взаимодействия с JS могут завершиться ошибкой по другим причинам, которые возникают в приложениях Blazor WebAssembly.

Дополнительные сведения о нехватке ресурсов см. в статье Руководство по предотвращению угроз для ASP.NET Core Blazor Server.

Исключение циклических ссылок на объекты

Объекты, содержащие циклические ссылки, не могут быть сериализованы на клиенте для:

  • вызовов метода .NET.
  • Вызов метода JavaScript из C#, если тип возвращаемого значения имеет циклические ссылки.

Ограничения размера для вызовов взаимодействия с JavaScript

Этот раздел относится только к приложениям Blazor Server. В Blazor WebAssembly платформа не накладывает ограничений на размер входных и выходных данных в вызовах взаимодействия с JavaScript (JS).

В Blazor Server размер данных в вызовах взаимодействия с JS ограничен максимальным размером входящего сообщения SignalR, разрешенным для методов концентратора, который задается с помощью HubOptions.MaximumReceiveMessageSize (по умолчанию: 32 КБ). Если размер сообщений SignalR JS для .NET превышает MaximumReceiveMessageSize, возникает ошибка. Платформа не накладывает ограничение на размер сообщений SignalR от концентратора клиенту.

Если для ведения журнала SignalR не установлен уровень Отладка или Трассировка, ошибка в связи с недопустимым размером сообщения отображается только в консоли средств разработчика браузера:

Ошибка: Подключение разорвано с ошибкой "Ошибка. Сервер вернул ошибку при закрытии: соединение закрыто с ошибкой".

Если для ведения журнала на стороне сервера SignalR установлен уровень Отладка или Трассировка, функция ведения журнала на стороне сервера предоставляет InvalidDataException для ошибки в связи с недопустимым размером.

appsettings.Development.json:

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

Ошибка:

System.IO.InvalidDataException: Превышен максимальный размер сообщения 32768 Б. Размер сообщения можно настроить в AddHubOptions.

Увеличьте ограничение, настроив MaximumReceiveMessageSize в Startup.ConfigureServices. В следующем примере для размера получаемого сообщения устанавливается максимальный размер 64 КБ (64 * 1024):

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

Увеличение предельного размера для входящих сообщений SignalR потребует больше серверных ресурсов для обработки, а также повысит уязвимость сервера к злонамеренным действиям пользователей. Кроме того, считывание большого объема содержимого в память в виде строк или массивов байтов может привести к неудачным выделениям памяти, которые плохо обрабатываются сборщиком мусора, что дополнительно снизит производительность.

Чтобы считывать полезные данные большого размера, можно отправлять такое содержимое меньшими фрагментами и обрабатывать их как Stream. Это удобно для чтения полезных данных большого объема в формате JSON или необработанных байтов из JS. Вы можете изучить отправку больших двоичных данных в Blazor Server с помощью методов, эквивалентных работе компонента InputFile, в примере приложения с отправкой двоичных данных.

Примечание

По предыдущим ссылкам в документации на справочные материалы по ASP.NET Core загружается ветвь main репозитория, которая представляет текущую разработку единицы продукта для следующего выпуска ASP.NET Core. Чтобы выбрать ветвь для другого выпуска, используйте раскрывающийся список Switch branches/tags (Переключение ветвей или тегов). Например, выберите ветвь release/5.0 для выпуска ASP.NET Core 5.0.

При разработке кода, который передает большие объемы данных между JavaScript и Blazor в приложениях Blazor Server, учитывайте следующие рекомендации:

  • Разделите данные на небольшие части и отправляйте сегменты данных последовательно, пока все данные не будут получены сервером.
  • Не выделяйте большие объекты в коде C# и JS.
  • Не блокируйте основной поток пользовательского интерфейса на длительные периоды при отправке или получении данных.
  • Освободите занятую память при завершении или отмене процесса.
  • Применяйте следующие дополнительные требования в целях безопасности:
    • Объявите максимальный размер файла или данных, который может быть передан.
    • Объявите минимальную скорость передачи от клиента к серверу.
  • После получения данных сервером данные могут быть:
    • Временно сохранены в буфере памяти до тех пор, пока не будут собраны все сегменты.
    • Использованы немедленно. Например, данные могут храниться сразу в базе данных или записываться на диск по мере получения каждого сегмента.

Перехват исключений JavaScript

Чтобы перехватить исключения JS, заключите взаимодействие JS в блок try-catch и перехватите JSException.

В следующем примере функция nonFunction JS не существует. Если функция не найдена, объект JSException перехватывается с помощью Message, который указывает на следующую ошибку:

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}";
        }
    }
}

Дополнительные ресурсы