blazor-js-interop-call-js-from-csharp

JS interop: llamar a JavaScript desde C#

  • 4 min

La interoperabilidad con JavaScript es el mecanismo con el que el código .NET y el entorno JavaScript intercambian llamadas y datos.

Blazor permite escribir gran parte de la interfaz con C#, pero seguimos necesitando JavaScript para algunas API del navegador y para integrar bibliotecas como Chart.js.

Blazor no aísla tu aplicación en una burbuja; al contrario, te permite interactuar con el ecosistema web mediante el mecanismo conocido como JS Interop.

El servicio IJSRuntime

La puerta de entrada a JavaScript es una abstracción: la interfaz IJSRuntime. Como cualquier otro servicio en Blazor, debemos inyectarlo en nuestro componente.

@inject IJSRuntime JS
Copied!

Este servicio se encarga de serializar tus datos en JSON, enviarlos al entorno de JavaScript (navegador), ejecutar la función que pidas y devolverte el resultado deserializado.

Definiendo la función JavaScript

En este primer ejemplo, la función será accesible globalmente desde window. Más adelante la moveremos a un módulo para no contaminar el ámbito global.

Para una demostración puedes cargar este script desde el documento raíz. En aplicaciones reales se recomiendan archivos externos y módulos JS, no etiquetas <script> dentro de componentes interactivos.

Vamos a añadir un script simple en nuestro HTML base:

<script>
    window.funcionesUtiles = {
        mostrarAlerta: function (mensaje) {
            alert(mensaje);
        },
        pedirConfirmacion: function (mensaje) {
            return confirm(mensaje); // Devuelve true o false
        },
        sumarNumeros: function (a, b) {
            return a + b;
        }
    };
</script>
Copied!

Llamando desde C#

El servicio IJSRuntime nos ofrece dos métodos principales, ambos asíncronos.

InvokeVoidAsync para llamadas sin resultado

Usamos este método cuando queremos ejecutar una acción en JS y no esperamos ningún valor de retorno (o no nos importa). Es ideal para efectos secundarios como logs, alertas o manipulación del DOM.

<button @onclick="LanzarAlerta">Saludar</button>

@code {
    private async Task LanzarAlerta()
    {
        // 1er parámetro: Nombre de la función JS
        // 2do parámetro: Argumento(s) para la función
        await JS.InvokeVoidAsync("funcionesUtiles.mostrarAlerta", "¡Hola desde C#!");
    }
}
Copied!

Aunque la función JS no devuelva nada, en Blazor Server es importante usar await. La llamada viaja por la red (SignalR) hasta el navegador, se ejecuta y vuelve la confirmación. Si no esperas (await), podrías tener condiciones de carrera.

InvokeAsync<T> para obtener un resultado

Usamos este método cuando la función JS devuelve un valor que necesitamos en C#. Blazor deserializará automáticamente el resultado al tipo T que especifiquemos.

<button @onclick="ConfirmarBorrado">Borrar Archivo</button>
<p>Resultado: @mensaje</p>

@code {
    private string mensaje = string.Empty;

    private async Task ConfirmarBorrado()
    {
        // Esperamos un booleano de vuelta
        bool confirmado = await JS.InvokeAsync<bool>(
            "funcionesUtiles.pedirConfirmacion",
            "¿Seguro que quieres borrar esto?");

        if (confirmado)
        {
            mensaje = "Archivo borrado ✅";
            // Lógica de borrado...
        }
        else
        {
            mensaje = "Operación cancelada ❌";
        }
    }
}
Copied!

Blazor maneja tipos primitivos (int, string, bool) y objetos complejos (DTOs) sin problemas, siempre que sean serializables a JSON.

El momento adecuado para acceder al DOM

Durante el prerenderizado no se puede llamar a JavaScript. Además, una referencia a un elemento todavía no existe en OnInitialized, porque el navegador aún no ha renderizado ese elemento.

Para inicializar una biblioteca sobre el DOM, espera a OnAfterRenderAsync. Las llamadas que no dependen del DOM también pueden hacerse desde un manejador de eventos interactivo.

@inject IJSRuntime JS

<input @ref="miInput" placeholder="Foco automático..." />

@code {
    private ElementReference miInput;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        // Solo queremos poner el foco la primera vez que se carga la página
        if (firstRender)
        {
            await JS.InvokeVoidAsync("funcionesUtiles.enfocar", miInput);
        }
    }
}
Copied!

La función JavaScript recibe la referencia al elemento:

window.funcionesUtiles.enfocar = element => element.focus();
Copied!

Encapsular las llamadas

Llenar tus componentes de llamadas JS.InvokeAsync es una mala práctica. Ensucia el código y lo hace difícil de testear.

La recomendación “Luis Llamas” es encapsular estas llamadas en un servicio C#.

Mala práctica:

// En el componente
await JS.InvokeVoidAsync("localStorage.setItem", "clave", "valor");
Copied!

Buena práctica: Crear un servicio ILocalStorageService.

public class LocalStorageService
{
    private readonly IJSRuntime _js;

    public LocalStorageService(IJSRuntime js) => _js = js;

    public async Task SetItemAsync(string key, string value)
    {
        await _js.InvokeVoidAsync("localStorage.setItem", key, value);
    }
}
Copied!

Así, tu componente solo inyecta ILocalStorageService y no sabe nada de JavaScript. Además, si mañana cambias LocalStorage por IndexedDB, el componente ni se entera.