Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

18 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 16 de junio de 2020
Blazor Aunque uno de los objetivos de partida de Blazor es permitir que toda la lógica de presentación de una aplicación pueda ser implementada sin tocar una línea de Javascript, sería absurdo ignorar que este lenguaje dispone de uno de los ecosistemas más amplios y ricos del mundo del software. Sin duda, sería una pena (y un error) intentar ignorar la ingente cantidad de bibliotecas y componentes que existen para él.

Tampoco podemos olvidar que puede que nosotros mismos tengamos bibliotecas Javascript que nos podría interesar reutilizar en nuevas interfaces de usuario. De nuevo, sería un error que el hecho de saltar a Blazor nos obligara a reescribir todo desde cero.

Por estas razones Blazor dispone de mecanismos para interoperar con Javascript bidireccionalmente, pues:
  • desde nuestras aplicaciones Blazor podemos invocar funciones Javascript,
  • y desde Javascript podemos invocar métodos escritos en C# con Blazor.
En este post vamos a centrarnos en la primera posibilidad, dejando la segunda para otras publicaciones de la serie que llegarán más adelante.

¿Cómo invocar métodos Javascript desde Blazor (Server o WebAssembly)?

Durante el arranque de una aplicación Blazor se registra en el inyector de dependencias un objeto IJSRuntime, una abstracción que nos dará acceso a los objetos y funciones Javascript que estén disponibles en la página actual.

Como podemos ver, esta interfaz es bastante sencilla pues sólo nos facilita un método InvokeAsync() con un par de variantes:
public interface IJSRuntime
{
    ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args);
    ValueTask<TValue> InvokeAsync<TValue>(
        string identifier, CancellationToken cancellationToken, object[] args);
}
Un detalle interesante es que, por detrás, cada "sabor" de Blazor deberá implementar esta interfaz expresamente, pues la forma de acceder al Javascript del navegador cambia radicalmente según si estamos ejecutando nuestra aplicación con Blazor Server o Blazor WebAssembly.
Por tanto, ya de forma intuitiva, podemos ver que para invocar código script desde Blazor, lo único que tenemos que hacer es solicitar al inyector una instancia de IJSRuntime y utilizar el método InvokeAsync(), como en el siguiente ejemplo, que en este caso retornará el valor obtenido tras hacer un prompt al usuario:
@inject IJSRuntime JsRuntime

<h1>Blazor To JS Demo</h1>
<button class="btn btn-primary" @onclick="PromptNameAsync">Click!</button>
@if (!string.IsNullOrEmpty(name))
{
    <p>Hello, @name</p>
}

@code {
    string name;

    async Task PromptNameAsync()
    {
        name = await JsRuntime.InvokeAsync<string>("prompt", "What is your name?");
    }
}
El parámetro genérico de InvokeAsync() indica el tipo de dato retornado por la llamada a la función Javascript; el primer parámetro es el identificador de la función y a continuación van los argumentos que queramos enviarle.

Además de los métodos impuestos por la interfaz IJSRuntime, podemos utilizar métodos extensores que nos simplificarán algunos escenarios. Por ejemplo, si estamos llamando a una función que no retorna ningún valor, podremos utilizar InvokeVoidAsync() de una forma muy similar a como hemos visto arriba:
await JsRuntime.InvokeVoidAsync("console.log", "Hello world!");
await JsRuntime.InvokeVoidAsync("alert", "Warning!");
Por supuesto, la función a invocar puede ser de las existentes "de serie" en Javascript, o bien una función personalizada que hayamos implementado sobre la página o en un .js vinculado a ella, por ejemplo:
await JsRuntime.InvokeVoidAsync("location.reload");
Observad que en todos los casos la llamada es asíncrona; esto, además de por razones estructurales (en Blazor Server el método a invocar está físicamente separado del punto desde el cual realizamos la llamada), es para permitir la llamada a funciones asíncronas que retornan un objeto Promise de Javascript:
function executeSomething() {
    return new Promise((resolve, reject) => {
        console.log("Waiting 3 seconds...")
        setTimeout(() => resolve(), 3000);
    });
}
La función anterior podría ser ejecutada de la siguente manera:
async Task PromptNameAsync()
{
    await JsRuntime.InvokeVoidAsync("executeSomething");
}
Otras variantes del método nos permiten indicar un token que podemos utilizar para cancelar la llamada cuando nos interese. Por ejemplo, el siguiente código muestra cómo utilizar un timeout, lo cual puede venir bien si no queremos que una llamada se quede "enganchada" más tiempo de la cuenta:
try
{
    var source = new CancellationTokenSource(TimeSpan.FromSeconds(1));
    await JsRuntime.InvokeVoidAsync("executeSomething", source.Token);
    await JsRuntime.InvokeVoidAsync("alert", "Done!");
}
catch (TaskCanceledException ex)
{
    await JsRuntime.InvokeVoidAsync("alert", "The task was cancelled!");
}
Como parámetros en las llamadas podemos utilizar cualquier tipo de objeto, que serán serializados automáticamente y enviados a la función. Por ejemplo, imaginad que tenemos una clase de datos como la siguiente:
// Friend.cs
public class Friend
{
    public string Name { get; set; }
    public int Age { get; set; }
}
Esta clase podríamos utilizarla directamente en una invocación a través del interop con Javascript:
@page "/"
@inject IJSRuntime JsRuntime

<h1>Complex object demo</h1>
<button class="btn btn-primary" @onclick="ShowFriendData">Click!</button>

@code {
    async Task ShowFriendData()
    {
         var friend = new Friend {Name = "John", Age = 32};
         await JsRuntime.InvokeVoidAsync("showFriendData", friend);
    }
}
Y la función Javascript podría ser la siguiente; observad que al serializar se han convertido las propiedades a camel casing, que es lo que se esperaría desde el lado Javascript:
function showFriendData(friend) {
    alert(`Hello, ${friend.name}, you are ${friend.age} years old`);
}
De la misma forma, podríamos utilizar tipos complejos como retorno de estas llamadas, es decir, hacer un InvokeAsync<Friend>() sobre una función Javascript que retornara un objeto de dicho tipo.

¿Y si no quiero ejecutar una función, sino leer una variable o propiedad?

Es importante tener en cuenta que, como su nombre sugiere, InvokeAsync() sólo puede ser utilizado para invocar funciones. Si quisiéramos, por ejemplo, obtener el valor de una variable o propiedad, deberíamos utilizar una función JS personalizada.

Por ejemplo, supongamos que queremos obtener el título de la página actual, disponible en la propiedad de la página document.title desde Blazor. Lo primero sería crear la función que actuará como puente:
function getTitle() {
    return document.title;
}
Y luego, ya desde Blazor, podríamos invocarla con total normalidad:
@page "/title"
@inject IJSRuntime JsRuntime

<h1>Show title Demo</h1>
<button class="btn btn-primary" @onclick="ShowTitle">Click!</button>

@code {
    async Task ShowTitle()
    {
        var title = await JsRuntime.InvokeAsync<string>("getTitle");
        await JsRuntime.InvokeVoidAsync("alert", "Title: " + title);
    }
}
¡Y esto es todo! Creo que a lo largo de este post hemos visto lo más destacado de este mecanismo que nos permite invocar funciones Javascript desde nuestro código C#/Blazor.

En entradas posteriores veremos cómo hacerlo en sentido inverso, es decir, permitir que desde Javascript invoquemos métodos escritos en C#.

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!