Llamada a funciones de JavaScript con métodos de .NET en Blazor de ASP.NET Core

En este artículo se explica cómo invocar funciones de JavaScript (JS) desde .NET.

Para obtener información sobre cómo llamar a métodos de .NET desde JS, vea JS.

El marco Blazor registra IJSRuntime. Para llamar a JS desde .NET, inserte la abstracción IJSRuntime y llame a uno de los métodos siguientes:

Para los métodos de .NET anteriores que invocan funciones de JS:

  • El identificador de función (String) es relativo al ámbito global (window). Para llamar a window.someScope.someFunction, el identificador es someScope.someFunction. No es necesario registrar la función para poder llamarla.
  • Pase cualquier número de argumentos serializables de JSON en Object[] a una función de JS.
  • El token de cancelación (CancellationToken) propaga una notificación de que se deben cancelar las operaciones.
  • TimeSpan representa un límite de tiempo para una operación de JS.
  • El tipo de TValue devuelto también debe ser serializable con JSON. TValue debe coincidir con el tipo de .NET que mejor asignación tenga con el tipo de JSON devuelto.
  • Se devuelve un objeto JS Promise para los métodos InvokeAsync. InvokeAsync desencapsula el objeto Promise y devuelve el valor que el objeto Promise espera.

En el caso de las aplicaciones de Blazor Server que tienen habilitada la representación previa, no se puede llamar a JS durante la representación previa inicial. Las llamadas de interoperabilidad de JS se deben aplazar hasta que se establezca la conexión con el explorador. Para obtener más información, vea la sección Detección de cuándo se está obteniendo una representación previa de una aplicación .

El siguiente ejemplo se basa en TextDecoder, un descodificador basado en JS. En el ejemplo se muestra cómo invocar una función de JS desde un método de C# que descarga un requisito del código de desarrollador a una API de JS existente. La función de JS acepta una matriz de bytes procedente de un método de C#, la descodifica y devuelve el texto al componente para que lo muestre.

Agregue el código de JS siguiente dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o 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>

El componente CallJsExample1 siguiente:

  • Invoca la función de convertArrayJS con InvokeAsync al seleccionar un botón (Convert Array).
  • Después de llamar a la función de JS, la matriz que se ha pasado se convierte en una cadena. Dicha cadena se devuelve al componente para que la muestre (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 ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">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));
    }
}

API de JavaScript restringida a los gestos del usuario

Esta sección solo se aplica a las aplicaciones .

Algunas API de JavaScript (JS) del explorador solo se pueden ejecutar en el contexto de un gesto del usuario, como el uso de JS. No se puede llamar a estas API mediante el mecanismo de interoperabilidad de JS en las aplicaciones de Blazor Server porque el control de eventos de la interfaz de usuario se realiza de forma asincrónica y, por lo general, no en el contexto del gesto del usuario. La aplicación debe controlar completamente el evento de interfaz de usuario en JavaScript, por lo que debe usar (documentación de MDN) en lugar del atributo de directiva @onclick de Blazor.

Invocación de funciones de JavaScript sin leer un valor devuelto (InvokeVoidAsync)

Utilice InvokeVoidAsync si:

  • .NET no es necesario para leer el resultado de una llamada de JS.
  • Las funciones de JS devuelven JS o undefined.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server), proporciona una función displayTickerAlert1 de JS. Se llama a la función con InvokeVoidAsync y no se devuelve un valor:

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

Ejemplo (InvokeVoidAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged1 en el componente CallJsExample2 siguiente.

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

Ejemplo (InvokeVoidAsync) de clase (.cs)

JsInteropClasses1.cs:

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 llama al método handleTickerChanged1 en el componente CallJsExample3 siguiente.

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()
    {
        if (jsClass is not null)
        {
            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();
}

Invocación de funciones de JavaScript y lectura de un valor devuelto (InvokeAsync)

Use InvokeAsync cuando .NET debe leer el resultado de una llamada de JS.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server), proporciona una función displayTickerAlert2 de JS. En el ejemplo siguiente se devuelve una cadena para que la muestre el llamador:

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

Ejemplo (InvokeAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample4 siguiente.

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

Ejemplo (InvokeAsync) de clase (.cs)

JsInteropClasses2.cs:

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 llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample5 siguiente.

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()
    {
        if (jsClass is not null)
        {
            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();
}

Escenarios de generación de contenido dinámico

Para generar contenido dinámico con BuildRenderTree, use el atributo :

[Inject]
IJSRuntime JS { get; set; }

Detección de cuándo se está obteniendo una representación previa de la aplicación Blazor Server

Esta sección se aplica a Blazor Server y a las aplicaciones Blazor WebAssembly hospedadas que representan previamente los componentes de Razor. La representación previa se describe en Integración y representación previa de componentes de ASP.NET Core.

Durante la representación previa de una aplicación, no es posible realizar ciertas acciones, como llamar a JavaScript. Es posible que los componentes tengan que representarse de forma diferente cuando se representen previamente.

En el ejemplo siguiente, la función setElementText1 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con JSRuntimeExtensions.InvokeVoidAsync y no se devuelve un valor:

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Para retrasar las llamadas de interoperabilidad de JavaScript hasta un punto en el que se garantice que estas llamadas funcionan, invalide el evento de ciclo de vida . Solo se llama a este evento después de que la aplicación se represente por completo.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

En el componente siguiente se muestra cómo usar la interoperabilidad de JavaScript como parte de la lógica de inicialización de un componente de una manera compatible con la representación previa. El componente muestra que es posible desencadenar una actualización de representación desde dentro de OnAfterRenderAsync. El desarrollador debe tener cuidado y evitar la creación de un bucle infinito en este escenario.

En el ejemplo siguiente, la función setElementText2 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con IJSRuntime.InvokeAsync y se devuelve un valor.

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Cuando se llama a JSRuntime.InvokeAsync, ElementReference solo se utiliza en OnAfterRenderAsync y no en ningún otro método de ciclo de vida anterior porque no existe ningún elemento de JavaScript hasta después de representar el componente.

Se llama a StateHasChanged para volver a representar el componente con el nuevo estado obtenido de la llamada de interoperabilidad de JavaScript (para más información, vea StateHasChanged). El código no crea un bucle infinito porque solo se llama a StateHasChanged cuando null es data.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

Interoperabilidad JS sincrónica en aplicaciones Blazor WebAssembly

Esta sección solo se aplica a las aplicaciones .

De forma predeterminada, las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asincrónicas de forma predeterminada para asegurarse de que los componentes son compatibles en ambos modelos Blazor de hospedaje, Blazor Server y Blazor WebAssembly. En Blazor Server, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabe con certeza que la aplicación solo se ejecuta en Blazor WebAssembly, puede optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para hacer una llamada sincrónica de .NET a JavaScript en una aplicación Blazor WebAssembly, convierta IJSRuntime en IJSInProcessRuntime para hacer la llamada de interoperabilidad de JS:

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

Al trabajar con IJSObjectReference en las aplicaciones ASP.NET Core 5.0 o posterioresBlazor WebAssembly, puede usar IJSInProcessObjectReference sincrónicamente en su lugar:

private IJSInProcessObjectReference module;

...

module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", "./scripts.js");

Ubicación de JavaScript

Cargue código de JavaScript (JS) mediante cualquiera de los enfoques descritos en el JS:

Para obtener información sobre cómo aislar scripts en los módulos , consulte la sección Aislamiento de JavaScript en módulos de JavaScript.

Advertencia

No coloque una etiqueta <script> en un archivo de componente (.razor), porque la etiqueta <script> no se puede actualizar dinámicamente.

Aislamiento de JavaScript en módulos de JavaScript

Blazor permite el aislamiento de JavaScript (JS) en Blazor estándar (JS).

El aislamiento de JS proporciona las siguientes ventajas:

  • El JS importado no contamina el espacio de nombres global.
  • No es necesario que los consumidores de una biblioteca y los componentes importen el código de JS relacionado.

Por ejemplo, el siguiente módulo de JS exporta una función de JS para mostrar un JS. Coloque el código de JS siguiente en un archivo de JS externo.

wwwroot/scripts.js:

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

Agregue el módulo de JS anterior a una aplicación o biblioteca de clases como un recurso web estático en la carpeta wwwroot y, después, importe dicho módulo al código .NET llamando a InvokeAsync en la instancia IJSRuntime.

IJSRuntime importa el módulo como un elemento IJSObjectReference, que es una referencia a un objeto de JS hecha desde código .NET. Use IJSObjectReference para invocar funciones de JS exportadas desde el módulo.

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) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

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

En el ejemplo anterior:

  • Por convención, el identificador de import es un identificador especial que se usa específicamente para importar un módulo de JS.
  • Especifique el archivo JS externo del módulo mediante su ruta de acceso de recurso web estático estable: ./{SCRIPT PATH AND FILENAME (.js)}, donde:
    • El segmento de trazado del directorio actual (./) es necesario para crear la ruta de recurso estático correcta para el archivo JS.
    • El marcador de posición {SCRIPT PATH AND FILENAME (.js)} es la ruta de acceso y el nombre de archivo en wwwroot.
  • Elimina el elemento IJSObjectReference para la IJSObjectReference en IAsyncDisposable.DisposeAsync.

La importación dinámica de un módulo requiere una solicitud de red, por lo que solo se puede lograr de forma asincrónica llamando a InvokeAsync.

IJSInProcessObjectReference representa una referencia a un objeto JS cuyas funciones se pueden invocar de forma sincrónica en las aplicaciones de Blazor WebAssembly. Para más información, consulte la sección Interoperabilidad sincrónica de JS en aplicaciones Blazor WebAssembly.

Nota

Cuando una JS proporciona el archivo externo JS, especifique el archivo JS del módulo mediante su ruta de acceso estática estable del recurso web: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • El segmento de trazado del directorio actual (./) es necesario para crear la ruta de recurso estático correcta para el archivo JS.
  • El marcador de posición {PACKAGE ID} es el id. de paquete de la biblioteca. El id. de paquete tiene como valor predeterminado el nombre de ensamblado del proyecto si <PackageId> no se especifica en el archivo del proyecto. En el ejemplo siguiente, el nombre del ensamblado de la biblioteca es ComponentLibrary y el archivo de proyecto de la biblioteca no especifica <PackageId>.
  • El marcador de posición {SCRIPT PATH AND FILENAME (.js)} es la ruta de acceso y el nombre de archivo en wwwroot. En el ejemplo siguiente, el archivo JS externo (script.js) se coloca en la carpeta wwwroot de la biblioteca de clases.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

Para obtener más información, vea Consumo de componentes de ASP.NET Core a partir de una biblioteca de clases de Razor (RCL).

Captura de referencias a elementos

Algunos escenarios de interoperabilidad de JavaScript (JS) requieren referencias a elementos HTML. Por ejemplo, una biblioteca de interfaz de usuario puede requerir una referencia de elemento para inicializarse, o puede que necesite llamar a API de tipo comando en un elemento, como click o play.

Use el siguiente método para capturar referencias a elementos HTML en un componente:

  • Agregue un atributo @ref al elemento HTML.
  • Defina un campo de tipo ElementReference cuyo nombre coincida con el valor del atributo @ref.

En el siguiente ejemplo se muestra la captura de una referencia al elemento username<input>:

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

@code {
    private ElementReference username;
}

Advertencia

Use únicamente una referencia de elemento para mutar el contenido de un elemento vacío que no interactúa con Blazor. Este escenario es útil cuando una API de terceros proporciona contenido al elemento. Dado que Blazor no interactúa con el elemento, no existe ninguna posibilidad de que se produzca un conflicto entre la representación de Blazor del elemento y el Document Object Model (DOM).

En el siguiente ejemplo, es peligroso mutar el contenido de la lista sin ordenar () porque Blazor interactúa con el DOM para rellenar los elementos de lista de este elemento (<li>) del objeto Todos:

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

Si la interoperabilidad de JS muta el contenido del elemento MyList y Blazor intenta realizar comparaciones con ese elemento, las comparaciones no coincidirán con el DOM.

Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

ElementReference se pasa al código de JS por medio de la interoperabilidad de JS. El código de JS recibe una instancia de HTMLElement que puede usar con las API de DOM habituales. Por ejemplo, el siguiente código define un método de extensión de .NET (TriggerClickEvent) que permite enviar un clic del mouse a un elemento.

La función de JSclickElement crea un evento click en el elemento HTML pasado (element):

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

Para llamar a una función de JS que no devuelve un valor, use JSRuntimeExtensions.InvokeVoidAsync. El siguiente código desencadena un evento click de cliente al llamar a la función de JS anterior con el elemento ElementReference capturado:

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

Para usar un método de extensión, cree un método de extensión estático que reciba la instancia de IJSRuntime:

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

Se llama al método clickElement directamente en el objeto. En el siguiente ejemplo se da por hecho que el método TriggerClickEvent está disponible en el espacio de nombres 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);
    }
}

Importante

La variable exampleButton solo se rellena después de que el componente se represente. Si se pasa un elemento ElementReference sin rellenar al código de JS, el código de JS recibe un valor null. Para manipular referencias de elemento una vez finalizada la representación del componente, use los métodos de ciclo de vida del componente o OnAfterRender.

Cuando se trabaje con tipos genéricos y se devuelva un valor, use ValueTask<TResult>:

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

El marcador de posición {JAVASCRIPT FUNCTION} es el identificador de la función JS.

Se llama al método GenericMethod directamente en el objeto con un tipo. En el siguiente ejemplo se da por hecho que el método GenericMethod está disponible en el espacio de nombres 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);
    }
}

Referencia a elementos en componentes

ElementReference no se puede pasar entre componentes porque:

Para que un componente primario haga que una referencia de elemento esté disponible para otros componentes, el componente primario puede:

  • Permitir que los componentes secundarios registren devoluciones de llamada.
  • Invoca las devoluciones de llamada registradas durante el evento OnAfterRender con la referencia de elemento que se ha pasado. Este método permite de forma indirecta que los componentes secundarios interactúen con la referencia del elemento primario.

Agregue el estilo siguiente a la etiqueta <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server):

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

Agregue el siguiente script dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server):

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

Pages/CallJsExample7.razor (componente primario):

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes en la carpeta Pages. Si prueba el código de forma local, actualice el espacio de nombres.

Shared/SurveyPrompt.razor (componente secundario):

@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 is not null ? Parent.Subscribe(this) : null;
        }

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes compartidos en la carpeta Shared. Si prueba el código de forma local, actualice el espacio de nombres.

Protección de las llamadas de interoperabilidad de JavaScript

Esta sección se aplica principalmente a las aplicaciones , pero las aplicaciones Blazor WebAssembly también pueden establecer tiempos de espera de interoperabilidad de JS si las condiciones lo permiten.

En aplicaciones Blazor Server, la interoperabilidad de JavaScript (JS) puede no funcionar debido a errores de red y debe tratarse como no confiable. De forma predeterminada, las aplicaciones Blazor Server usan un tiempo de espera de un minuto para las llamadas de interoperabilidad de JS. Si una aplicación puede tolerar un tiempo de espera más restrictivo, establézcalo recurriendo a uno de los siguientes métodos.

Establezca un tiempo de espera global en Program.cs con CircuitOptions.JSInteropDefaultCallTimeout:

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

El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).

Establezca un tiempo de espera por cada invocación en el código de componente. El tiempo de espera especificado invalida el tiempo de espera global establecido por JSInteropDefaultCallTimeout:

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

En el ejemplo anterior:

  • El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).
  • El marcador de posición {ID} es el identificador de la función que se va a invocar. Por ejemplo, el valor someScope.someFunction invoca la función window.someScope.someFunction.

Aunque una causa común de los errores de interoperabilidad de JS son los errores de red en las aplicaciones Blazor Server, se pueden establecer tiempos de espera por cada invocación para las llamadas de interoperabilidad de JS en las aplicaciones Blazor WebAssembly. Aunque no existe ningún circuito SignalR en una aplicación Blazor WebAssembly, las llamadas de interoperabilidad de JS pueden presentar errores por otros motivos que se aplican a las aplicaciones Blazor WebAssembly.

Para más información sobre el agotamiento de recursos, vea Guía de mitigación de amenazas para ASP.NET Core .

Evitar referencias de objetos circulares

Los objetos que contienen referencias circulares no se pueden serializar en el cliente para:

  • Llamadas de método .NET.
  • Llamadas de método JavaScript desde C# cuando el tipo de valor devuelto tiene referencias circulares.

Bibliotecas de JavaScript que representan la interfaz de usuario

En ocasiones, es posible que quiera usar bibliotecas de JavaScript (JS) que generen elementos de la interfaz de usuario visibles en el Document Object Model (DOM) del explorador. A primera vista, esto podría parecer difícil porque el sistema de comparación de Blazor se basa en tener control sobre el árbol de elementos DOM y encuentra errores si algún código externo muta el árbol DOM e invalida su mecanismo para aplicar las comparaciones. No se trata de una limitación específica de Blazor. El mismo desafío se produce con cualquier marco de interfaz de usuario basado en comparaciones.

Afortunadamente, es sencillo insertar una interfaz de usuario generada externamente dentro de una interfaz de usuario de componentes de Razor de forma confiable. La técnica recomendada consiste en hacer que el código del componente (archivo .razor) produzca un elemento vacío. En lo que se refiere al sistema de comparación de Blazor, el elemento siempre está vacío, por lo que el representador no recorre en el elemento y no interferirá con sus contenidos. Esto hace que sea seguro rellenar el elemento con contenido arbitrario administrado externamente.

En el siguiente ejemplo se muestra el concepto. Dentro de la instrucción if cuando firstRender es true, interactúe con unmanagedElement fuera de Blazor mediante la interoperabilidad de JS. Por ejemplo, llame a una biblioteca de JS externa para rellenar el elemento. Blazor no hará nada con el contenido del elemento hasta que se quite este componente. Cuando se quite el componente, también se quitará el subárbol DOM completo del componente.

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

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

@code {
    private ElementReference unmanagedElement;

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

Considere el ejemplo siguiente que representa un mapa interactivo mediante las API de código abierto de Mapbox.

El módulo de JS siguiente se coloca en la aplicación o está disponible en una biblioteca de clases de Razor.

Nota

Para crear el mapa de Mapbox, obtenga un token de acceso de Inicio de sesión de Mapbox y proporciónelo donde aparece en el código siguiente.

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

Para generar el estilo correcto, agregue la siguiente etiqueta de hoja de estilos a la página HTML del host.

En wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server), agregue el elemento <link> siguiente al marcado del elemento <head>:

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

Pages/CallJsExample8.razor:

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

<h1>Call JS Example 8</h1>

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

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

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

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

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            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();
        }
    }
}

En el ejemplo anterior se genera una interfaz de usuario de mapa interactiva. El usuario:

  • Puede arrastrar para desplazarse o hacer zoom.
  • Puede hacer clic en los botones para saltar a ubicaciones predefinidas.

Mapbox street map of Tokyo, Japan with buttons to select Bristol, United Kingdom and Tokyo, Japan

En el ejemplo anterior:

  • En cuanto a Blazor, <div> con @ref="mapElement" se deja vacío. El script mapbox-gl.js puede rellenar de forma segura el elemento y modificar su contenido. Utilice esta técnica con cualquier biblioteca de JS que represente la interfaz de usuario. Puede insertar componentes de un marco de JS SPA de terceros dentro de los componentes de Razor, siempre y cuando no intenten modificar otras partes de la página. No es seguro que el código externo modifique los elementos que Blazor no considere vacíos.
  • Al utilizar este enfoque, tenga en cuenta las reglas sobre cómo Blazor conserva o destruye los elementos DOM. El componente controla de forma segura los eventos de clic de botón y actualiza la instancia del mapa existente porque, de forma predeterminada, los elementos DOM se conservan siempre que sea posible. Si estuviera representando una lista de elementos de mapa desde dentro de un bucle @foreach, querría usar @key para garantizar la preservación de las instancias del componente. De lo contrario, los cambios en los datos de la lista podrían hacer que las instancias del componente conservaran el estado de las instancias anteriores de forma no deseada. Para obtener más información, vea using @key to preserve elements and components.
  • El ejemplo encapsula la lógica de JS y las dependencias dentro de un módulo ES6 y carga el módulo dinámicamente mediante el identificador import. Para más información, consulte Aislamiento de JavaScript en módulos de JavaScript.

Compatibilidad con matrices de bytes

Blazor admite la interoperabilidad de JS de matriz de bytes optimizada que evita la codificación o descodificación de matrices de bytes en Base64. En el ejemplo siguiente se usa la interoperabilidad de JS para pasar una matriz de bytes a JavaScript.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server), proporciona una función receiveByteArray de JS. Se llama a la función con InvokeVoidAsync y no se devuelve un valor:

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

Para obtener información sobre el uso de una matriz de bytes al llamar a .NET desde JavaScript, vea Llamada a métodos de .NET desde funciones de JavaScript en ASP.NET Core .

Límites de tamaño en las llamadas de interoperabilidad de JavaScript

Esta sección solo se aplica a las aplicaciones Blazor Server. En Blazor WebAssembly, el marco no impone límites en cuanto al tamaño de las entradas y salidas de las llamadas de interoperabilidad de JavaScript (JS).

En Blazor Server, las llamadas de interoperabilidad de JS presentan un tamaño limitado por el tamaño máximo de los mensajes SignalR entrantes que se permite para los métodos del concentrador. Esto se aplica por medio de HubOptions.MaximumReceiveMessageSize (valor predeterminado: 32 KB). Los mensajes de JS a .NET SignalR mayores que MaximumReceiveMessageSize producen un error. El marco no impone ningún límite de tamaño para un mensaje SignalR desde el concentrador a un cliente.

Cuando el registro de SignalR no está establecido en SignalR o Seguimiento, solo aparece un error relativo al tamaño del mensaje en la consola de herramientas de desarrollo del explorador:

Error: Conexión desconectada con el error "Error: El servidor ha devuelto un error al cerrarse: Conexión cerrada con un error.".

Cuando el registro del lado servidor de se establece en Depurar o Seguimiento, el registro del lado servidor inicia una excepción relativa a un error del tamaño del mensaje.

appsettings.Development.json:

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

Error:

System.IO.InvalidDataException: Se ha superado el tamaño máximo del mensaje de 32768B. El tamaño del mensaje se puede configurar en AddHubOptions.

Para aumentar el límite, establezca MaximumReceiveMessageSize en Program.cs. En el ejemplo siguiente se establece el tamaño máximo del mensaje de recepción en 64 KB (64*1024):

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

El aumento del límite de tamaño del mensaje entrante SignalR implica que se necesitan más recursos del servidor y lo expone a más riesgos por parte de un usuario malintencionado. Además, la lectura de una gran cantidad de contenido en la memoria, como cadenas o matrices de bytes, también puede dar lugar a que las asignaciones funcionen de forma deficiente con el recolector de elementos no utilizados, lo que puede reducir significativamente el rendimiento.

Tenga en cuenta las instrucciones siguientes al desarrollar código que transfiera un gran volumen de datos entre JS y Blazor en aplicaciones Blazor Server:

  • Aproveche la compatibilidad de interoperabilidad de transmisión nativa para transferir datos mayores que el límite de tamaño del mensaje entrante SignalR:
  • Sugerencias generales:
    • No asigne objetos grandes en código JS y C#.
    • Libere la memoria consumida al completar o cancelar el proceso.
    • Aplique los requisitos adicionales siguientes por motivos de seguridad:
      • Declare el tamaño máximo del archivo o los datos que se pueden pasar.
      • Declare la tasa mínima de carga desde el cliente al servidor.
    • Después de que el servidor reciba los datos, los datos se pueden:
      • Almacenar temporalmente en un búfer de memoria hasta que se recopilen todos los segmentos.
      • Consumir inmediatamente. Por ejemplo, los datos se pueden almacenar inmediatamente en una base de datos o escribir en el disco a medida que se reciba cada segmento.

Interoperabilidad de JavaScript deserializada

Los componentes de Blazor WebAssembly pueden experimentar un rendimiento deficiente cuando se serializan objetos .NET para la interoperabilidad de JavaScript (JS) y se cumple cualquiera de las siguientes condiciones:

  • Un gran volumen de objetos .NET se serializan rápidamente. Por ejemplo, se puede producir un rendimiento deficiente cuando las llamadas de interoperabilidad de JS se realizan en función del movimiento de un dispositivo de entrada, como girar una rueda del mouse.
  • Los objetos .NET grandes o muchos objetos .NET deben serializarse para la interoperabilidad de JS. Por ejemplo, se puede producir un rendimiento deficiente cuando las llamadas de interoperabilidad de JS requieren la serialización de docenas de archivos.

IJSUnmarshalledObjectReference representa una referencia a un objeto de JS cuyas funciones se pueden invocar sin la sobrecarga de serializar los datos de .NET.

En el ejemplo siguiente:

  • Un struct que contiene una cadena y un entero se pasa sin serializar a .
  • Las funciones de JS procesan los datos y devuelven un valor booleano o una cadena al autor de la llamada.
  • Una cadena de JS no se puede convertir directamente en un objeto string de .NET. La función unmarshalledFunctionReturnString llama a BINDING.js_string_to_mono_string para administrar la conversión de una cadena de JS.

Nota

Los ejemplos siguientes no son casos de uso típicos para este escenario porque el struct pasado a no da lugar a un rendimiento deficiente del componente. En el ejemplo se usa un objeto pequeño simplemente para mostrar los conceptos para pasar datos de .NET no serializados.

Coloque el bloque <script> siguiente en wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Como alternativa, agregue JS a un archivo externo al que se hace referencia dentro de la etiqueta de cierre </body>. Por ejemplo, <script src="{SCRIPT PATH AND FILE NAME (.js)}"></script>, donde el marcador de posición {SCRIPT PATH AND FILE NAME (.js)} es la ruta de acceso y el nombre de archivo del script.

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

Advertencia

El nombre, el comportamiento y la existencia de la función js_string_to_mono_string están sujetos a cambios en una versión futura de .NET. Por ejemplo:

  • Es probable que se cambie el nombre de la función.
  • La propia función podría quitarse en favor de la conversión automática de cadenas por el marco.

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

Si una instancia de IJSUnmarshalledObjectReference no se elimina en el código de C#, se puede eliminar en JS. La siguiente función de dispose elimina la referencia de objeto cuando se llama desde JS:

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

    ...
  };
}

Los tipos de matriz se pueden convertir a partir de objetos JS en objetos .NET mediante js_typed_array_to_array, pero la matriz de JS debe ser una matriz con tipo. Las matrices de JS se pueden leer en código de C# como una matriz de objetos .NET (object[]).

Otros tipos de datos, como las matrices de cadenas, se pueden convertir pero requieren la creación de un nuevo objeto de matriz Mono (mono_obj_array_new) y el establecimiento de su valor (mono_obj_array_set).

Advertencia

Las funciones de JS proporcionadas por el marco de Blazor, como js_typed_array_to_array, mono_obj_array_new, y mono_obj_array_set, están sujetas a cambios de nombre, cambios de comportamiento o eliminación en versiones futuras de .NET.

Transmisión de .NET a JavaScript

Blazor admite la transmisión de datos directamente desde .NET a JavaScript. Los flujos se crean mediante el objeto DotNetStreamReference.

DotNetStreamReference representa una secuencia de .NET y usa los parámetros siguientes:

  • stream: la secuencia enviada a JavaScript.
  • leaveOpen: determina si la secuencia sigue abierta después de la transmisión. Si no se proporciona un valor, leaveOpen tiene como valor predeterminado false.

En JavaScript, use un búfer de matriz o una secuencia legible para recibir los datos:

  • Mediante un objeto ArrayBuffer:

    async function streamToJavaScript(streamRef) {
      const data = await streamRef.arrayBuffer();
    }
    
  • Mediante un objeto ReadableStream:

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

En código de C#:

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

En el ejemplo anterior:

  • El marcador de posición {STREAM} representa el objeto Stream enviado a JavaScript.
  • JS es una instancia de IJSRuntime insertada.

En Llamada a métodos de .NET desde funciones de JavaScript en ASP.NET Core se describe la operación inversa, el streaming de JavaScript a .NET.

En Descargas de archivos de ASP.NET Core se describe cómo descargar un archivo en Blazor.

Detección de excepciones de JavaScript

Para detectar excepciones JS, encapsule la interoperabilidad de JS en un JS y detecte una excepción JSException.

En el siguiente ejemplo, la función nonFunctionJS no existe. Cuando no se encuentra la función, el objeto JSExceptionqueda atrapado con una excepción Message que indica el siguiente error:

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

Anulación de una función de JavaScript de ejecución larga

Use JSAbortController con CancellationTokenSource en el componente para anular una función de JavaScript de ejecución larga desde código de C#.

La clase JSHelpers siguiente contiene una función simulada de ejecución larga, longRunningFn, que se cuenta continuamente hasta que AbortController.signal indica que se ha llamado a AbortController.abort. La función sleep tiene fines de demostración para simular la ejecución lenta de la función de ejecución larga y no estaría presente en el código de producción. Cuando un componente llama a stopFn, se indica a longRunningFn que se anule a través de la comprobación condicional del bucle while en AbortSignal.aborted.

Agregue el código de JS siguiente dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server):

<script>
  class Helpers {
    static #controller = new AbortController();

    static async #sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    static async longRunningFn() {
      var i = 0;
      while (!this.#controller.signal.aborted) {
        i++;
        console.log(`longRunningFn: ${i}`);
        await this.#sleep(1000);
      }
    }

    static stopFn() {
      this.#controller.abort();
      console.log('longRunningFn aborted!');
    }
  }

  window.Helpers = Helpers;
</script>

El componente CallJsExample12 siguiente:

Pages/CallJsExample12.razor:

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

<h1>Cancel long-running JS interop</h1>

<p>
    <button @onclick="StartTask">Start Task</button>
    <button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
    private CancellationTokenSource? cts;

    private async Task StartTask()
    {
        cts = new CancellationTokenSource();
        cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));

        await JS.InvokeVoidAsync("Helpers.longRunningFn");
    }

    private void CancelTask()
    {
        cts?.Cancel();
    }

    public void Dispose()
    {
        cts?.Cancel();
        cts?.Dispose();
    }
}

La consola de herramientas del desarrollador de un explorador indica la ejecución de la función de duración larga JS después de seleccionar el botón Start Task y cuando se anula la función después de seleccionar el botón Cancel Task:

longRunningFn: 1
longRunningFn: 2
longRunningFn: 3
longRunningFn aborted!

Recursos adicionales

Para obtener información sobre cómo llamar a métodos de .NET desde JS, vea JS.

Para llamar a JS desde .NET, inserte la abstracción IJSRuntime y llame a uno de los métodos siguientes:

Para los métodos de .NET anteriores que invocan funciones de JS:

  • El identificador de función (String) es relativo al ámbito global (window). Para llamar a window.someScope.someFunction, el identificador es someScope.someFunction. No es necesario registrar la función para poder llamarla.
  • Pase cualquier número de argumentos serializables de JSON en Object[] a una función de JS.
  • El token de cancelación (CancellationToken) propaga una notificación de que se deben cancelar las operaciones.
  • TimeSpan representa un límite de tiempo para una operación de JS.
  • El tipo de TValue devuelto también debe ser serializable con JSON. TValue debe coincidir con el tipo de .NET que mejor asignación tenga con el tipo de JSON devuelto.
  • Se devuelve un objeto JS Promise para los métodos InvokeAsync. InvokeAsync desencapsula el objeto Promise y devuelve el valor que el objeto Promise espera.

En el caso de las aplicaciones de Blazor Server que tienen habilitada la representación previa, no se puede llamar a JS durante la representación previa inicial. Las llamadas de interoperabilidad de JS se deben aplazar hasta que se establezca la conexión con el explorador. Para obtener más información, vea la sección Detección de cuándo se está obteniendo una representación previa de una aplicación .

El siguiente ejemplo se basa en TextDecoder, un descodificador basado en JS. En el ejemplo se muestra cómo invocar una función de JS desde un método de C# que descarga un requisito del código de desarrollador a una API de JS existente. La función de JS acepta una matriz de bytes procedente de un método de C#, la descodifica y devuelve el texto al componente para que lo muestre.

Agregue el código de JS siguiente dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o 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>

El componente CallJsExample1 siguiente:

  • Invoca la función de convertArrayJS con InvokeAsync al seleccionar un botón (Convert Array).
  • Después de llamar a la función de JS, la matriz que se ha pasado se convierte en una cadena. Dicha cadena se devuelve al componente para que la muestre (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-2005">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));
    }
}

API de JavaScript restringida a los gestos del usuario

Esta sección solo se aplica a las aplicaciones .

Algunas API de JavaScript (JS) del explorador solo se pueden ejecutar en el contexto de un gesto del usuario, como el uso de JS. No se puede llamar a estas API mediante el mecanismo de interoperabilidad de JS en las aplicaciones de Blazor Server porque el control de eventos de la interfaz de usuario se realiza de forma asincrónica y, por lo general, no en el contexto del gesto del usuario. La aplicación debe controlar completamente el evento de interfaz de usuario en JavaScript, por lo que debe usar (documentación de MDN) en lugar del atributo de directiva @onclick de Blazor.

Invocación de funciones de JavaScript sin leer un valor devuelto (InvokeVoidAsync)

Utilice InvokeVoidAsync si:

  • .NET no es necesario para leer el resultado de una llamada de JS.
  • Las funciones de JS devuelven JS o undefined.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server), proporciona una función displayTickerAlert1 de JS. Se llama a la función con InvokeVoidAsync y no se devuelve un valor:

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

Ejemplo (InvokeVoidAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged1 en el componente CallJsExample2 siguiente.

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

Ejemplo (InvokeVoidAsync) de clase (.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 llama al método handleTickerChanged1 en el componente CallJsExample3 siguiente.

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

Invocación de funciones de JavaScript y lectura de un valor devuelto (InvokeAsync)

Use InvokeAsync cuando .NET debe leer el resultado de una llamada de JS.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server), proporciona una función displayTickerAlert2 de JS. En el ejemplo siguiente se devuelve una cadena para que la muestre el llamador:

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

Ejemplo (InvokeAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample4 siguiente.

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

Ejemplo (InvokeAsync) de clase (.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 llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample5 siguiente.

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

Escenarios de generación de contenido dinámico

Para generar contenido dinámico con BuildRenderTree, use el atributo :

[Inject]
IJSRuntime JS { get; set; }

Detección de cuándo se está obteniendo una representación previa de la aplicación Blazor Server

Esta sección se aplica a Blazor Server y a las aplicaciones Blazor WebAssembly hospedadas que representan previamente los componentes de Razor. La representación previa se describe en Integración y representación previa de componentes de ASP.NET Core.

Durante la representación previa de una aplicación, no es posible realizar ciertas acciones, como llamar a JavaScript. Es posible que los componentes tengan que representarse de forma diferente cuando se representen previamente.

En el ejemplo siguiente, la función setElementText1 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con JSRuntimeExtensions.InvokeVoidAsync y no se devuelve un valor:

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Para retrasar las llamadas de interoperabilidad de JavaScript hasta un punto en el que se garantice que estas llamadas funcionan, invalide el evento de ciclo de vida . Solo se llama a este evento después de que la aplicación se represente por completo.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

En el componente siguiente se muestra cómo usar la interoperabilidad de JavaScript como parte de la lógica de inicialización de un componente de una manera compatible con la representación previa. El componente muestra que es posible desencadenar una actualización de representación desde dentro de OnAfterRenderAsync. El desarrollador debe tener cuidado y evitar la creación de un bucle infinito en este escenario.

En el ejemplo siguiente, la función setElementText2 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con IJSRuntime.InvokeAsync y se devuelve un valor.

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Cuando se llama a JSRuntime.InvokeAsync, ElementReference solo se utiliza en OnAfterRenderAsync y no en ningún otro método de ciclo de vida anterior porque no existe ningún elemento de JavaScript hasta después de representar el componente.

Se llama a StateHasChanged para volver a representar el componente con el nuevo estado obtenido de la llamada de interoperabilidad de JavaScript (para más información, vea StateHasChanged). El código no crea un bucle infinito porque solo se llama a StateHasChanged cuando null es data.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

Interoperabilidad JS sincrónica en aplicaciones Blazor WebAssembly

Esta sección solo se aplica a las aplicaciones .

De forma predeterminada, las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asincrónicas de forma predeterminada para asegurarse de que los componentes son compatibles en ambos modelos Blazor de hospedaje, Blazor Server y Blazor WebAssembly. En Blazor Server, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabe con certeza que la aplicación solo se ejecuta en Blazor WebAssembly, puede optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para hacer una llamada sincrónica de .NET a JavaScript en una aplicación Blazor WebAssembly, convierta IJSRuntime en IJSInProcessRuntime para hacer la llamada de interoperabilidad de JS:

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

Al trabajar con IJSObjectReference en las aplicaciones ASP.NET Core 5.0 o posterioresBlazor WebAssembly, puede usar IJSInProcessObjectReference sincrónicamente en su lugar:

private IJSInProcessObjectReference module;

...

module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", "./scripts.js");

Ubicación de JavaScript

Cargue código de JavaScript (JS) mediante cualquiera de los enfoques descritos en el JS:

Para obtener información sobre cómo aislar scripts en los módulos , consulte la sección Aislamiento de JavaScript en módulos de JavaScript.

Advertencia

No coloque una etiqueta <script> en un archivo de componente (.razor), porque la etiqueta <script> no se puede actualizar dinámicamente.

Aislamiento de JavaScript en módulos de JavaScript

Blazor permite el aislamiento de JavaScript (JS) en Blazor estándar (JS).

El aislamiento de JS proporciona las siguientes ventajas:

  • El JS importado no contamina el espacio de nombres global.
  • No es necesario que los consumidores de una biblioteca y los componentes importen el código de JS relacionado.

Por ejemplo, el siguiente módulo de JS exporta una función de JS para mostrar un JS. Coloque el código de JS siguiente en un archivo de JS externo.

wwwroot/scripts.js:

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

Agregue el módulo de JS anterior a una aplicación o biblioteca de clases como un recurso web estático en la carpeta wwwroot y, después, importe dicho módulo al código .NET llamando a InvokeAsync en la instancia IJSRuntime.

IJSRuntime importa el módulo como un elemento IJSObjectReference, que es una referencia a un objeto de JS hecha desde código .NET. Use IJSObjectReference para invocar funciones de JS exportadas desde el módulo.

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

En el ejemplo anterior:

  • Por convención, el identificador de import es un identificador especial que se usa específicamente para importar un módulo de JS.
  • Especifique el archivo JS externo del módulo mediante su ruta de acceso de recurso web estático estable: ./{SCRIPT PATH AND FILENAME (.js)}, donde:
    • El segmento de trazado del directorio actual (./) es necesario para crear la ruta de recurso estático correcta para el archivo JS.
    • El marcador de posición {SCRIPT PATH AND FILENAME (.js)} es la ruta de acceso y el nombre de archivo en wwwroot.

La importación dinámica de un módulo requiere una solicitud de red, por lo que solo se puede lograr de forma asincrónica llamando a InvokeAsync.

IJSInProcessObjectReference representa una referencia a un objeto JS cuyas funciones se pueden invocar de forma sincrónica en las aplicaciones de Blazor WebAssembly. Para más información, consulte la sección Interoperabilidad sincrónica de JS en aplicaciones Blazor WebAssembly.

Nota

Cuando una JS proporciona el archivo externo JS, especifique el archivo JS del módulo mediante su ruta de acceso estática estable del recurso web: ./_content/{PACKAGE ID}/{SCRIPT PATH AND FILENAME (.js)}:

  • El segmento de trazado del directorio actual (./) es necesario para crear la ruta de recurso estático correcta para el archivo JS.
  • El marcador de posición {PACKAGE ID} es el id. de paquete de la biblioteca. El id. de paquete tiene como valor predeterminado el nombre de ensamblado del proyecto si <PackageId> no se especifica en el archivo del proyecto. En el ejemplo siguiente, el nombre del ensamblado de la biblioteca es ComponentLibrary y el archivo de proyecto de la biblioteca no especifica <PackageId>.
  • El marcador de posición {SCRIPT PATH AND FILENAME (.js)} es la ruta de acceso y el nombre de archivo en wwwroot. En el ejemplo siguiente, el archivo JS externo (script.js) se coloca en la carpeta wwwroot de la biblioteca de clases.
var module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

Para obtener más información, vea Consumo de componentes de ASP.NET Core a partir de una biblioteca de clases de Razor (RCL).

Captura de referencias a elementos

Algunos escenarios de interoperabilidad de JavaScript (JS) requieren referencias a elementos HTML. Por ejemplo, una biblioteca de interfaz de usuario puede requerir una referencia de elemento para inicializarse, o puede que necesite llamar a API de tipo comando en un elemento, como click o play.

Use el siguiente método para capturar referencias a elementos HTML en un componente:

  • Agregue un atributo @ref al elemento HTML.
  • Defina un campo de tipo ElementReference cuyo nombre coincida con el valor del atributo @ref.

En el siguiente ejemplo se muestra la captura de una referencia al elemento username<input>:

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

@code {
    private ElementReference username;
}

Advertencia

Use únicamente una referencia de elemento para mutar el contenido de un elemento vacío que no interactúa con Blazor. Este escenario es útil cuando una API de terceros proporciona contenido al elemento. Dado que Blazor no interactúa con el elemento, no existe ninguna posibilidad de que se produzca un conflicto entre la representación de Blazor del elemento y el Document Object Model (DOM).

En el siguiente ejemplo, es peligroso mutar el contenido de la lista sin ordenar () porque Blazor interactúa con el DOM para rellenar los elementos de lista de este elemento (<li>) del objeto Todos:

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

Si la interoperabilidad de JS muta el contenido del elemento MyList y Blazor intenta realizar comparaciones con ese elemento, las comparaciones no coincidirán con el DOM.

Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

ElementReference se pasa al código de JS por medio de la interoperabilidad de JS. El código de JS recibe una instancia de HTMLElement que puede usar con las API de DOM habituales. Por ejemplo, el siguiente código define un método de extensión de .NET (TriggerClickEvent) que permite enviar un clic del mouse a un elemento.

La función de JSclickElement crea un evento click en el elemento HTML pasado (element):

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

Para llamar a una función de JS que no devuelve un valor, use JSRuntimeExtensions.InvokeVoidAsync. El siguiente código desencadena un evento click de cliente al llamar a la función de JS anterior con el elemento ElementReference capturado:

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

Para usar un método de extensión, cree un método de extensión estático que reciba la instancia de IJSRuntime:

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

Se llama al método clickElement directamente en el objeto. En el siguiente ejemplo se da por hecho que el método TriggerClickEvent está disponible en el espacio de nombres 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);
    }
}

Importante

La variable exampleButton solo se rellena después de que el componente se represente. Si se pasa un elemento ElementReference sin rellenar al código de JS, el código de JS recibe un valor null. Para manipular referencias de elemento una vez finalizada la representación del componente, use los métodos de ciclo de vida del componente o OnAfterRender.

Cuando se trabaje con tipos genéricos y se devuelva un valor, use ValueTask<TResult>:

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

El marcador de posición {JAVASCRIPT FUNCTION} es el identificador de la función JS.

Se llama al método GenericMethod directamente en el objeto con un tipo. En el siguiente ejemplo se da por hecho que el método GenericMethod está disponible en el espacio de nombres 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);
    }
}

Referencia a elementos en componentes

ElementReference no se puede pasar entre componentes porque:

Para que un componente primario haga que una referencia de elemento esté disponible para otros componentes, el componente primario puede:

  • Permitir que los componentes secundarios registren devoluciones de llamada.
  • Invoca las devoluciones de llamada registradas durante el evento OnAfterRender con la referencia de elemento que se ha pasado. Este método permite de forma indirecta que los componentes secundarios interactúen con la referencia del elemento primario.

Agregue el estilo siguiente a la etiqueta <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server):

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

Agregue el siguiente script dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server):

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

Pages/CallJsExample7.razor (componente primario):

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes en la carpeta Pages. Si prueba el código de forma local, actualice el espacio de nombres.

Shared/SurveyPrompt.razor (componente secundario):

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes compartidos en la carpeta Shared. Si prueba el código de forma local, actualice el espacio de nombres.

Protección de las llamadas de interoperabilidad de JavaScript

Esta sección se aplica principalmente a las aplicaciones , pero las aplicaciones Blazor WebAssembly también pueden establecer tiempos de espera de interoperabilidad de JS si las condiciones lo permiten.

En aplicaciones Blazor Server, la interoperabilidad de JavaScript (JS) puede no funcionar debido a errores de red y debe tratarse como no confiable. De forma predeterminada, las aplicaciones Blazor Server usan un tiempo de espera de un minuto para las llamadas de interoperabilidad de JS. Si una aplicación puede tolerar un tiempo de espera más restrictivo, establézcalo recurriendo a uno de los siguientes métodos.

Establezca un tiempo de espera global en el método Startup.ConfigureServices de Startup.cs con CircuitOptions.JSInteropDefaultCallTimeout:

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

El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).

Establezca un tiempo de espera por cada invocación en el código de componente. El tiempo de espera especificado invalida el tiempo de espera global establecido por JSInteropDefaultCallTimeout:

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

En el ejemplo anterior:

  • El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).
  • El marcador de posición {ID} es el identificador de la función que se va a invocar. Por ejemplo, el valor someScope.someFunction invoca la función window.someScope.someFunction.

Aunque una causa común de los errores de interoperabilidad de JS son los errores de red en las aplicaciones Blazor Server, se pueden establecer tiempos de espera por cada invocación para las llamadas de interoperabilidad de JS en las aplicaciones Blazor WebAssembly. Aunque no existe ningún circuito SignalR en una aplicación Blazor WebAssembly, las llamadas de interoperabilidad de JS pueden presentar errores por otros motivos que se aplican a las aplicaciones Blazor WebAssembly.

Para más información sobre el agotamiento de recursos, vea Guía de mitigación de amenazas para ASP.NET Core .

Evitar referencias de objetos circulares

Los objetos que contienen referencias circulares no se pueden serializar en el cliente para:

  • Llamadas de método .NET.
  • Llamadas de método JavaScript desde C# cuando el tipo de valor devuelto tiene referencias circulares.

Bibliotecas de JavaScript que representan la interfaz de usuario

En ocasiones, es posible que quiera usar bibliotecas de JavaScript (JS) que generen elementos de la interfaz de usuario visibles en el Document Object Model (DOM) del explorador. A primera vista, esto podría parecer difícil porque el sistema de comparación de Blazor se basa en tener control sobre el árbol de elementos DOM y encuentra errores si algún código externo muta el árbol DOM e invalida su mecanismo para aplicar las comparaciones. No se trata de una limitación específica de Blazor. El mismo desafío se produce con cualquier marco de interfaz de usuario basado en comparaciones.

Afortunadamente, es sencillo insertar una interfaz de usuario generada externamente dentro de una interfaz de usuario de componentes de Razor de forma confiable. La técnica recomendada consiste en hacer que el código del componente (archivo .razor) produzca un elemento vacío. En lo que se refiere al sistema de comparación de Blazor, el elemento siempre está vacío, por lo que el representador no recorre en el elemento y no interferirá con sus contenidos. Esto hace que sea seguro rellenar el elemento con contenido arbitrario administrado externamente.

En el siguiente ejemplo se muestra el concepto. Dentro de la instrucción if cuando firstRender es true, interactúe con unmanagedElement fuera de Blazor mediante la interoperabilidad de JS. Por ejemplo, llame a una biblioteca de JS externa para rellenar el elemento. Blazor no hará nada con el contenido del elemento hasta que se quite este componente. Cuando se quite el componente, también se quitará el subárbol DOM completo del componente.

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

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

@code {
    private ElementReference unmanagedElement;

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

Considere el ejemplo siguiente que representa un mapa interactivo mediante las API de código abierto de Mapbox.

El módulo de JS siguiente se coloca en la aplicación o está disponible en una biblioteca de clases de Razor.

Nota

Para crear el mapa de Mapbox, obtenga un token de acceso de Inicio de sesión de Mapbox y proporciónelo donde aparece en el código siguiente.

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

Para generar el estilo correcto, agregue la siguiente etiqueta de hoja de estilos a la página HTML del host.

En wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server), agregue el elemento <link> siguiente al marcado del elemento <head>:

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

Pages/CallJsExample8.razor:

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

<h1>Call JS Example 8</h1>

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

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

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

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

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

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

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

En el ejemplo anterior se genera una interfaz de usuario de mapa interactiva. El usuario:

  • Puede arrastrar para desplazarse o hacer zoom.
  • Puede hacer clic en los botones para saltar a ubicaciones predefinidas.

Mapbox street map of Tokyo, Japan with buttons to select Bristol, United Kingdom and Tokyo, Japan

En el ejemplo anterior:

  • En cuanto a Blazor, <div> con @ref="mapElement" se deja vacío. El script mapbox-gl.js puede rellenar de forma segura el elemento y modificar su contenido. Utilice esta técnica con cualquier biblioteca de JS que represente la interfaz de usuario. Puede insertar componentes de un marco de JS SPA de terceros dentro de los componentes de Razor, siempre y cuando no intenten modificar otras partes de la página. No es seguro que el código externo modifique los elementos que Blazor no considere vacíos.
  • Al utilizar este enfoque, tenga en cuenta las reglas sobre cómo Blazor conserva o destruye los elementos DOM. El componente controla de forma segura los eventos de clic de botón y actualiza la instancia del mapa existente porque, de forma predeterminada, los elementos DOM se conservan siempre que sea posible. Si estuviera representando una lista de elementos de mapa desde dentro de un bucle @foreach, querría usar @key para garantizar la preservación de las instancias del componente. De lo contrario, los cambios en los datos de la lista podrían hacer que las instancias del componente conservaran el estado de las instancias anteriores de forma no deseada. Para obtener más información, vea using @key to preserve elements and components.
  • El ejemplo encapsula la lógica de JS y las dependencias dentro de un módulo ES6 y carga el módulo dinámicamente mediante el identificador import. Para más información, consulte Aislamiento de JavaScript en módulos de JavaScript.

Límites de tamaño en las llamadas de interoperabilidad de JavaScript

Esta sección solo se aplica a las aplicaciones Blazor Server. En Blazor WebAssembly, el marco no impone límites en cuanto al tamaño de las entradas y salidas de las llamadas de interoperabilidad de JavaScript (JS).

En Blazor Server, las llamadas de interoperabilidad de JS presentan un tamaño limitado por el tamaño máximo de los mensajes SignalR entrantes que se permite para los métodos del concentrador. Esto se aplica por medio de HubOptions.MaximumReceiveMessageSize (valor predeterminado: 32 KB). Los mensajes de JS a .NET SignalR mayores que MaximumReceiveMessageSize producen un error. El marco no impone ningún límite de tamaño para un mensaje SignalR desde el concentrador a un cliente.

Cuando el registro de SignalR no está establecido en SignalR o Seguimiento, solo aparece un error relativo al tamaño del mensaje en la consola de herramientas de desarrollo del explorador:

Error: Conexión desconectada con el error "Error: El servidor ha devuelto un error al cerrarse: Conexión cerrada con un error.".

Cuando el registro del lado servidor de se establece en Depurar o Seguimiento, el registro del lado servidor inicia una excepción relativa a un error del tamaño del mensaje.

appsettings.Development.json:

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

Error:

System.IO.InvalidDataException: Se ha superado el tamaño máximo del mensaje de 32768B. El tamaño del mensaje se puede configurar en AddHubOptions.

Para aumentar el límite, establezca MaximumReceiveMessageSize en Startup.ConfigureServices:

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

El aumento del límite de tamaño del mensaje entrante SignalR implica que se necesitan más recursos del servidor y lo expone a más riesgos por parte de un usuario malintencionado. Además, la lectura de una gran cantidad de contenido en la memoria, como cadenas o matrices de bytes, también puede dar lugar a que las asignaciones funcionen de forma deficiente con el recolector de elementos no utilizados, lo que puede reducir significativamente el rendimiento.

Una opción para leer cargas grandes consiste en enviar el contenido en fragmentos más pequeños y procesar la carga como Stream. Se puede usar al leer cargas grandes de JSON o si los datos están disponibles en JS como bytes sin formato. Para obtener un ejemplo en el que se muestra el envío de cargas binarias de gran tamaño en Blazor Server que usa técnicas similares a las del Blazor Server, vea la InputFile.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual de la unidad de producto para la próxima versión de .NET. Para seleccionar la rama de una versión diferente, use la lista desplegable Switch branches or tags. El repositorio de ASP.NET Core usa un formato "release/{VERSION}" para los nombres de rama de versión, donde el marcador de posición {VERSION} es la versión de lanzamiento. Por ejemplo, seleccione la rama release/6.0 para la versión 6.0 de ASP.NET Core. Otros repositorios de origen de referencia pueden usar otros esquemas de nomenclatura de etiquetas de versión.

Tenga en cuenta las instrucciones siguientes al desarrollar código que transfiera un gran volumen de datos entre JS y Blazor en aplicaciones Blazor Server:

  • Segmente los datos en partes más pequeñas y envíe los segmentos de datos secuencialmente hasta que el servidor reciba todos los datos.
  • No asigne objetos grandes en código JS y C#.
  • No bloquee el subproceso de interfaz de usuario principal durante períodos largos al enviar o recibir datos.
  • Libere la memoria consumida al completar o cancelar el proceso.
  • Aplique los requisitos adicionales siguientes por motivos de seguridad:
    • Declare el tamaño máximo del archivo o los datos que se pueden pasar.
    • Declare la tasa mínima de carga desde el cliente al servidor.
  • Después de que el servidor reciba los datos, los datos se pueden:
    • Almacenar temporalmente en un búfer de memoria hasta que se recopilen todos los segmentos.
    • Consumir inmediatamente. Por ejemplo, los datos se pueden almacenar inmediatamente en una base de datos o escribir en el disco a medida que se reciba cada segmento.

Interoperabilidad de JavaScript deserializada

Los componentes de Blazor WebAssembly pueden experimentar un rendimiento deficiente cuando se serializan objetos .NET para la interoperabilidad de JavaScript (JS) y se cumple cualquiera de las siguientes condiciones:

  • Un gran volumen de objetos .NET se serializan rápidamente. Por ejemplo, se puede producir un rendimiento deficiente cuando las llamadas de interoperabilidad de JS se realizan en función del movimiento de un dispositivo de entrada, como girar una rueda del mouse.
  • Los objetos .NET grandes o muchos objetos .NET deben serializarse para la interoperabilidad de JS. Por ejemplo, se puede producir un rendimiento deficiente cuando las llamadas de interoperabilidad de JS requieren la serialización de docenas de archivos.

IJSUnmarshalledObjectReference representa una referencia a un objeto de JS cuyas funciones se pueden invocar sin la sobrecarga de serializar los datos de .NET.

En el ejemplo siguiente:

  • Un struct que contiene una cadena y un entero se pasa sin serializar a .
  • Las funciones de JS procesan los datos y devuelven un valor booleano o una cadena al autor de la llamada.
  • Una cadena de JS no se puede convertir directamente en un objeto string de .NET. La función unmarshalledFunctionReturnString llama a BINDING.js_string_to_mono_string para administrar la conversión de una cadena de JS.

Nota

Los ejemplos siguientes no son casos de uso típicos para este escenario porque el struct pasado a no da lugar a un rendimiento deficiente del componente. En el ejemplo se usa un objeto pequeño simplemente para mostrar los conceptos para pasar datos de .NET no serializados.

Coloque el bloque <script> siguiente en wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server). Como alternativa, puede colocar el script JS en un archivo externo de JS al que se hace referencia dentro de la etiqueta de cierre </body> con <script src="{SCRIPT PATH AND FILE NAME (.js)}></script>, donde el marcador de posición {SCRIPT PATH AND FILE NAME (.js)} es la ruta de acceso y el nombre de archivo del script.

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

Advertencia

El nombre, el comportamiento y la existencia de la función js_string_to_mono_string están sujetos a cambios en una versión futura de .NET. Por ejemplo:

  • Es probable que se cambie el nombre de la función.
  • La propia función podría quitarse en favor de la conversión automática de cadenas por el marco.

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

Si una instancia de IJSUnmarshalledObjectReference no se elimina en el código de C#, se puede eliminar en JS. La siguiente función de dispose elimina la referencia de objeto cuando se llama desde JS:

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

    ...
  };
}

Los tipos de matriz se pueden convertir a partir de objetos JS en objetos .NET mediante js_typed_array_to_array, pero la matriz de JS debe ser una matriz con tipo. Las matrices de JS se pueden leer en código de C# como una matriz de objetos .NET (object[]).

Otros tipos de datos, como las matrices de cadenas, se pueden convertir pero requieren la creación de un nuevo objeto de matriz Mono (mono_obj_array_new) y el establecimiento de su valor (mono_obj_array_set).

Advertencia

Las funciones de JS proporcionadas por el marco de Blazor, como js_typed_array_to_array, mono_obj_array_new, y mono_obj_array_set, están sujetas a cambios de nombre, cambios de comportamiento o eliminación en versiones futuras de .NET.

Detección de excepciones de JavaScript

Para detectar excepciones JS, encapsule la interoperabilidad de JS en un JS y detecte una excepción JSException.

En el siguiente ejemplo, la función nonFunctionJS no existe. Cuando no se encuentra la función, el objeto JSExceptionqueda atrapado con una excepción Message que indica el siguiente error:

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

Recursos adicionales

Para obtener información sobre cómo llamar a métodos de .NET desde JS, vea JS.

Para llamar a JS desde .NET, inserte la abstracción IJSRuntime y llame a uno de los métodos siguientes:

Para los métodos de .NET anteriores que invocan funciones de JS:

  • El identificador de función (String) es relativo al ámbito global (window). Para llamar a window.someScope.someFunction, el identificador es someScope.someFunction. No es necesario registrar la función para poder llamarla.
  • Pase cualquier número de argumentos serializables de JSON en Object[] a una función de JS.
  • El token de cancelación (CancellationToken) propaga una notificación de que se deben cancelar las operaciones.
  • TimeSpan representa un límite de tiempo para una operación de JS.
  • El tipo de TValue devuelto también debe ser serializable con JSON. TValue debe coincidir con el tipo de .NET que mejor asignación tenga con el tipo de JSON devuelto.
  • Se devuelve un objeto JS Promise para los métodos InvokeAsync. InvokeAsync desencapsula el objeto Promise y devuelve el valor que el objeto Promise espera.

En el caso de las aplicaciones de Blazor Server que tienen habilitada la representación previa, no se puede llamar a JS durante la representación previa inicial. Las llamadas de interoperabilidad de JS se deben aplazar hasta que se establezca la conexión con el explorador. Para obtener más información, vea la sección Detección de cuándo se está obteniendo una representación previa de una aplicación .

El siguiente ejemplo se basa en TextDecoder, un descodificador basado en JS. En el ejemplo se muestra cómo invocar una función de JS desde un método de C# que descarga un requisito del código de desarrollador a una API de JS existente. La función de JS acepta una matriz de bytes procedente de un método de C#, la descodifica y devuelve el texto al componente para que lo muestre.

Agregue el código de JS siguiente dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o 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>

El componente CallJsExample1 siguiente:

  • Invoca la función de convertArrayJS con InvokeAsync al seleccionar un botón (Convert Array).
  • Después de llamar a la función de JS, la matriz que se ha pasado se convierte en una cadena. Dicha cadena se devuelve al componente para que la muestre (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-2005">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));
    }
}

API de JavaScript restringida a los gestos del usuario

Esta sección solo se aplica a las aplicaciones .

Algunas API de JavaScript (JS) del explorador solo se pueden ejecutar en el contexto de un gesto del usuario, como el uso de JS. No se puede llamar a estas API mediante el mecanismo de interoperabilidad de JS en las aplicaciones de Blazor Server porque el control de eventos de la interfaz de usuario se realiza de forma asincrónica y, por lo general, no en el contexto del gesto del usuario. La aplicación debe controlar completamente el evento de interfaz de usuario en JavaScript, por lo que debe usar (documentación de MDN) en lugar del atributo de directiva @onclick de Blazor.

Invocación de funciones de JavaScript sin leer un valor devuelto (InvokeVoidAsync)

Utilice InvokeVoidAsync si:

  • .NET no es necesario para leer el resultado de una llamada de JS.
  • Las funciones de JS devuelven JS o undefined.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server), proporciona una función displayTickerAlert1 de JS. Se llama a la función con InvokeVoidAsync y no se devuelve un valor:

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

Ejemplo (InvokeVoidAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged1 en el componente CallJsExample2 siguiente.

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

Ejemplo (InvokeVoidAsync) de clase (.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 llama al método handleTickerChanged1 en el componente CallJsExample3 siguiente.

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

Invocación de funciones de JavaScript y lectura de un valor devuelto (InvokeAsync)

Use InvokeAsync cuando .NET debe leer el resultado de una llamada de JS.

Dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server), proporciona una función displayTickerAlert2 de JS. En el ejemplo siguiente se devuelve una cadena para que la muestre el llamador:

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

Ejemplo (InvokeAsync) de componente (.razor)

TickerChanged llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample4 siguiente.

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

Ejemplo (InvokeAsync) de clase (.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 llama al método handleTickerChanged2 y muestra la cadena devuelta en el componente CallJsExample5 siguiente.

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

Escenarios de generación de contenido dinámico

Para generar contenido dinámico con BuildRenderTree, use el atributo :

[Inject]
IJSRuntime JS { get; set; }

Detección de cuándo se está obteniendo una representación previa de la aplicación Blazor Server

Esta sección se aplica a Blazor Server y a las aplicaciones Blazor WebAssembly hospedadas que representan previamente los componentes de Razor. La representación previa se describe en Integración y representación previa de componentes de ASP.NET Core.

Durante la representación previa de una aplicación, no es posible realizar ciertas acciones, como llamar a JavaScript. Es posible que los componentes tengan que representarse de forma diferente cuando se representen previamente.

En el ejemplo siguiente, la función setElementText1 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con JSRuntimeExtensions.InvokeVoidAsync y no se devuelve un valor:

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Para retrasar las llamadas de interoperabilidad de JavaScript hasta un punto en el que se garantice que estas llamadas funcionan, invalide el evento de ciclo de vida . Solo se llama a este evento después de que la aplicación se represente por completo.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

En el componente siguiente se muestra cómo usar la interoperabilidad de JavaScript como parte de la lógica de inicialización de un componente de una manera compatible con la representación previa. El componente muestra que es posible desencadenar una actualización de representación desde dentro de OnAfterRenderAsync. El desarrollador debe tener cuidado y evitar la creación de un bucle infinito en este escenario.

En el ejemplo siguiente, la función setElementText2 se coloca dentro del elemento <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Layout.cshtml (Blazor Server). Se llama a la función con IJSRuntime.InvokeAsync y se devuelve un valor.

Nota

Para obtener instrucciones sobre la ubicación de JS y nuestras recomendaciones para aplicaciones de producción, consulte Interoperabilidad de ASP.NET Core Blazor JavaScript (Interoperabilidad JS).

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

Advertencia

En el ejemplo anterior se modifica el Document Object Model (DOM) directamente solo con fines de demostración. No se recomienda modificar directamente el DOM con JavaScript en la mayoría de los escenarios, ya que JavaScript puede interferir con el seguimiento de cambios de Blazor. Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

Cuando se llama a JSRuntime.InvokeAsync, ElementReference solo se utiliza en OnAfterRenderAsync y no en ningún otro método de ciclo de vida anterior porque no existe ningún elemento de JavaScript hasta después de representar el componente.

Se llama a StateHasChanged para volver a representar el componente con el nuevo estado obtenido de la llamada de interoperabilidad de JavaScript (para más información, vea StateHasChanged). El código no crea un bucle infinito porque solo se llama a StateHasChanged cuando null es data.

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

Nota:

El ejemplo anterior contamina el cliente con métodos globales. Para obtener un mejor enfoque en las aplicaciones de producción, consulte Aislamiento de JavaScript en módulos de JavaScript.

Ejemplo:

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

Interoperabilidad JS sincrónica en aplicaciones Blazor WebAssembly

Esta sección solo se aplica a las aplicaciones .

De forma predeterminada, las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asincrónicas de forma predeterminada para asegurarse de que los componentes son compatibles en ambos modelos Blazor de hospedaje, Blazor Server y Blazor WebAssembly. En Blazor Server, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabe con certeza que la aplicación solo se ejecuta en Blazor WebAssembly, puede optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para hacer una llamada sincrónica de .NET a JavaScript en una aplicación Blazor WebAssembly, convierta IJSRuntime en IJSInProcessRuntime para hacer la llamada de interoperabilidad de JS:

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

Al trabajar con IJSObjectReference en las aplicaciones ASP.NET Core 5.0 o posterioresBlazor WebAssembly, puede usar IJSInProcessObjectReference sincrónicamente en su lugar:

private IJSInProcessObjectReference module;

...

module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", "./scripts.js");

Ubicación de JavaScript

Cargue código de JavaScript (JS) mediante cualquiera de los enfoques descritos en el JS:

Advertencia

No coloque una etiqueta <script> en un archivo de componente (.razor), porque la etiqueta <script> no se puede actualizar dinámicamente.

Captura de referencias a elementos

Algunos escenarios de interoperabilidad de JavaScript (JS) requieren referencias a elementos HTML. Por ejemplo, una biblioteca de interfaz de usuario puede requerir una referencia de elemento para inicializarse, o puede que necesite llamar a API de tipo comando en un elemento, como click o play.

Use el siguiente método para capturar referencias a elementos HTML en un componente:

  • Agregue un atributo @ref al elemento HTML.
  • Defina un campo de tipo ElementReference cuyo nombre coincida con el valor del atributo @ref.

En el siguiente ejemplo se muestra la captura de una referencia al elemento username<input>:

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

@code {
    private ElementReference username;
}

Advertencia

Use únicamente una referencia de elemento para mutar el contenido de un elemento vacío que no interactúa con Blazor. Este escenario es útil cuando una API de terceros proporciona contenido al elemento. Dado que Blazor no interactúa con el elemento, no existe ninguna posibilidad de que se produzca un conflicto entre la representación de Blazor del elemento y el Document Object Model (DOM).

En el siguiente ejemplo, es peligroso mutar el contenido de la lista sin ordenar () porque Blazor interactúa con el DOM para rellenar los elementos de lista de este elemento (<li>) del objeto Todos:

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

Si la interoperabilidad de JS muta el contenido del elemento MyList y Blazor intenta realizar comparaciones con ese elemento, las comparaciones no coincidirán con el DOM.

Para más información, vea Interoperabilidad de JavaScript en de ASP.NET Core (interoperabilidad de JS).

ElementReference se pasa al código de JS por medio de la interoperabilidad de JS. El código de JS recibe una instancia de HTMLElement que puede usar con las API de DOM habituales. Por ejemplo, el siguiente código define un método de extensión de .NET (TriggerClickEvent) que permite enviar un clic del mouse a un elemento.

La función de JSclickElement crea un evento click en el elemento HTML pasado (element):

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

Para llamar a una función de JS que no devuelve un valor, use JSRuntimeExtensions.InvokeVoidAsync. El siguiente código desencadena un evento click de cliente al llamar a la función de JS anterior con el elemento ElementReference capturado:

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

Para usar un método de extensión, cree un método de extensión estático que reciba la instancia de IJSRuntime:

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

Se llama al método clickElement directamente en el objeto. En el siguiente ejemplo se da por hecho que el método TriggerClickEvent está disponible en el espacio de nombres 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);
    }
}

Importante

La variable exampleButton solo se rellena después de que el componente se represente. Si se pasa un elemento ElementReference sin rellenar al código de JS, el código de JS recibe un valor null. Para manipular referencias de elemento una vez finalizada la representación del componente, use los métodos de ciclo de vida del componente o OnAfterRender.

Cuando se trabaje con tipos genéricos y se devuelva un valor, use ValueTask<TResult>:

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

El marcador de posición {JAVASCRIPT FUNCTION} es el identificador de la función JS.

Se llama al método GenericMethod directamente en el objeto con un tipo. En el siguiente ejemplo se da por hecho que el método GenericMethod está disponible en el espacio de nombres 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);
    }
}

Referencia a elementos en componentes

ElementReference no se puede pasar entre componentes porque:

Para que un componente primario haga que una referencia de elemento esté disponible para otros componentes, el componente primario puede:

  • Permitir que los componentes secundarios registren devoluciones de llamada.
  • Invoca las devoluciones de llamada registradas durante el evento OnAfterRender con la referencia de elemento que se ha pasado. Este método permite de forma indirecta que los componentes secundarios interactúen con la referencia del elemento primario.

Agregue el estilo siguiente a la etiqueta <head> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server):

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

Agregue el siguiente script dentro de la etiqueta de cierre </body> de wwwroot/index.html (Blazor WebAssembly) o Pages/_Host.cshtml (Blazor Server):

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

Pages/CallJsExample7.razor (componente primario):

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes en la carpeta Pages. Si prueba el código de forma local, actualice el espacio de nombres.

Shared/SurveyPrompt.razor (componente secundario):

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

En el ejemplo anterior, el espacio de nombres de la aplicación es BlazorSample con componentes compartidos en la carpeta Shared. Si prueba el código de forma local, actualice el espacio de nombres.

Protección de las llamadas de interoperabilidad de JavaScript

Esta sección se aplica principalmente a las aplicaciones , pero las aplicaciones Blazor WebAssembly también pueden establecer tiempos de espera de interoperabilidad de JS si las condiciones lo permiten.

En aplicaciones Blazor Server, la interoperabilidad de JavaScript (JS) puede no funcionar debido a errores de red y debe tratarse como no confiable. De forma predeterminada, las aplicaciones Blazor Server usan un tiempo de espera de un minuto para las llamadas de interoperabilidad de JS. Si una aplicación puede tolerar un tiempo de espera más restrictivo, establézcalo recurriendo a uno de los siguientes métodos.

Establezca un tiempo de espera global en el método Startup.ConfigureServices de Startup.cs con CircuitOptions.JSInteropDefaultCallTimeout:

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

El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).

Establezca un tiempo de espera por cada invocación en el código de componente. El tiempo de espera especificado invalida el tiempo de espera global establecido por JSInteropDefaultCallTimeout:

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

En el ejemplo anterior:

  • El marcador de posición {TIMEOUT} es un TimeSpan (por ejemplo, TimeSpan.FromSeconds(80)).
  • El marcador de posición {ID} es el identificador de la función que se va a invocar. Por ejemplo, el valor someScope.someFunction invoca la función window.someScope.someFunction.

Aunque una causa común de los errores de interoperabilidad de JS son los errores de red en las aplicaciones Blazor Server, se pueden establecer tiempos de espera por cada invocación para las llamadas de interoperabilidad de JS en las aplicaciones Blazor WebAssembly. Aunque no existe ningún circuito SignalR en una aplicación Blazor WebAssembly, las llamadas de interoperabilidad de JS pueden presentar errores por otros motivos que se aplican a las aplicaciones Blazor WebAssembly.

Para más información sobre el agotamiento de recursos, vea Guía de mitigación de amenazas para ASP.NET Core .

Evitar referencias de objetos circulares

Los objetos que contienen referencias circulares no se pueden serializar en el cliente para:

  • Llamadas de método .NET.
  • Llamadas de método JavaScript desde C# cuando el tipo de valor devuelto tiene referencias circulares.

Límites de tamaño en las llamadas de interoperabilidad de JavaScript

Esta sección solo se aplica a las aplicaciones Blazor Server. En Blazor WebAssembly, el marco no impone límites en cuanto al tamaño de las entradas y salidas de las llamadas de interoperabilidad de JavaScript (JS).

En Blazor Server, las llamadas de interoperabilidad de JS presentan un tamaño limitado por el tamaño máximo de los mensajes SignalR entrantes que se permite para los métodos del concentrador. Esto se aplica por medio de HubOptions.MaximumReceiveMessageSize (valor predeterminado: 32 KB). Los mensajes de JS a .NET SignalR mayores que MaximumReceiveMessageSize producen un error. El marco no impone ningún límite de tamaño para un mensaje SignalR desde el concentrador a un cliente.

Cuando el registro de SignalR no está establecido en SignalR o Seguimiento, solo aparece un error relativo al tamaño del mensaje en la consola de herramientas de desarrollo del explorador:

Error: Conexión desconectada con el error "Error: El servidor ha devuelto un error al cerrarse: Conexión cerrada con un error.".

Cuando el registro del lado servidor de se establece en Depurar o Seguimiento, el registro del lado servidor inicia una excepción relativa a un error del tamaño del mensaje.

appsettings.Development.json:

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

Error:

System.IO.InvalidDataException: Se ha superado el tamaño máximo del mensaje de 32768B. El tamaño del mensaje se puede configurar en AddHubOptions.

Para aumentar el límite, establezca MaximumReceiveMessageSize en Startup.ConfigureServices:

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

El aumento del límite de tamaño del mensaje entrante SignalR implica que se necesitan más recursos del servidor y lo expone a más riesgos por parte de un usuario malintencionado. Además, la lectura de una gran cantidad de contenido en la memoria, como cadenas o matrices de bytes, también puede dar lugar a que las asignaciones funcionen de forma deficiente con el recolector de elementos no utilizados, lo que puede reducir significativamente el rendimiento.

Una opción para leer cargas grandes consiste en enviar el contenido en fragmentos más pequeños y procesar la carga como Stream. Se puede usar al leer cargas grandes de JSON o si los datos están disponibles en JS como bytes sin formato. Para obtener un ejemplo en el que se muestra el envío de cargas binarias de gran tamaño en Blazor Server que usa técnicas similares a las del Blazor Server, vea la InputFile.

Nota:

Los vínculos de la documentación al origen de referencia de .NET cargan normalmente la rama predeterminada del repositorio, que representa el desarrollo actual de la unidad de producto para la próxima versión de .NET. Para seleccionar la rama de una versión diferente, use la lista desplegable Switch branches or tags. El repositorio de ASP.NET Core usa un formato "release/{VERSION}" para los nombres de rama de versión, donde el marcador de posición {VERSION} es la versión de lanzamiento. Por ejemplo, seleccione la rama release/6.0 para la versión 6.0 de ASP.NET Core. Otros repositorios de origen de referencia pueden usar otros esquemas de nomenclatura de etiquetas de versión.

Tenga en cuenta las instrucciones siguientes al desarrollar código que transfiera un gran volumen de datos entre JS y Blazor en aplicaciones Blazor Server:

  • Segmente los datos en partes más pequeñas y envíe los segmentos de datos secuencialmente hasta que el servidor reciba todos los datos.
  • No asigne objetos grandes en código JS y C#.
  • No bloquee el subproceso de interfaz de usuario principal durante períodos largos al enviar o recibir datos.
  • Libere la memoria consumida al completar o cancelar el proceso.
  • Aplique los requisitos adicionales siguientes por motivos de seguridad:
    • Declare el tamaño máximo del archivo o los datos que se pueden pasar.
    • Declare la tasa mínima de carga desde el cliente al servidor.
  • Después de que el servidor reciba los datos, los datos se pueden:
    • Almacenar temporalmente en un búfer de memoria hasta que se recopilen todos los segmentos.
    • Consumir inmediatamente. Por ejemplo, los datos se pueden almacenar inmediatamente en una base de datos o escribir en el disco a medida que se reciba cada segmento.

Detección de excepciones de JavaScript

Para detectar excepciones JS, encapsule la interoperabilidad de JS en un JS y detecte una excepción JSException.

En el siguiente ejemplo, la función nonFunctionJS no existe. Cuando no se encuentra la función, el objeto JSExceptionqueda atrapado con una excepción Message que indica el siguiente error:

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

Recursos adicionales