Blazor -  Usando JavaScript - I


   Neste artigo veremos como usar JavaScript em aplicações Blazor WebAssembly.

Com o Blazor WebAssembly, podemos criar aplicativos robustos sem escrever nenhum código JavaScript.

No entanto, existem alguns cenários que requerem o uso de JavaScript. Por exemplo, podemos ter uma biblioteca JavaScript favorita que queremos continuar a usar. Além disso, sem JavaScript, não podemos manipular o DOM ou chamar qualquer uma das APIs JavaScript.

A seguir temos uma lista parcial de coisas às quais não temos acesso diretamente via Blazor WebAssembly:

- A manipulação do DOM;
- A API de captura de mídia e streams;
- A API WebGL (gráficos 2D e 3D para a web);
- A API de armazenamento da Web (localStorage e sessionStorage);
- A API de geolocalização;
- Caixas pop-up JavaScript (caixa de alerta, caixa de confirmação, caixa de prompt);
- O status online do navegador;
- Histórico do navegador
- A biblioteca Chart.js;
- Outras bibliotecas JavaScript de terceiros;

A lista anterior não é abrangente, pois existem centenas de bibliotecas JavaScript atualmente disponíveis. No entanto, o ponto principal a ser lembrado é que não podemos manipular o DOM sem usar JavaScript.

Portanto, provavelmente sempre precisaremos usar algum JavaScript em nossos aplicativos da web, e,  felizmente, podemos usar a interoperabilidade javascript ou JS nas aplicações Blazor WebAssembly para ter acesso a estes recursos.

Explorando a interoperabilidade JS

Para invocar uma função JavaScript a partir da plataforma .NET, usamos a abstração IJSRuntime.

Essa abstração representa uma instância de um tempo de execução JavaScript que o framework pode chamar. Para usar o IJSRuntime, devemos primeiro injetá-lo em nosso componente usando injeção de dependência.

A diretiva @inject é usada para injetar uma dependência em um componente. O código a seguir injeta o IJSRuntime no componente atual:

@inject IJSRuntime js

A abstração IJSRuntime tem dois métodos que podemos usar para invocar funções JavaScript:

  1. InvokeAsync
  2. InvokeVoidAsync

Ambos os métodos são assíncronos. A diferença entre esses dois métodos é que um deles retorna um valor e o outro não.

Podemos fazer downcast de uma instância de IJSRuntime para uma instância de IJSInProcessRuntime para executar o método de forma síncrona. Finalmente, podemos invocar um método da plataforma .NET a partir de código JavaScript decorando o método com JsInvokable. Veremos exemplos de cada um desses métodos mais adiante.

No entanto, antes de podermos invocar um método JavaScript, precisamos carregar o JavaScript em nosso aplicativo.

Carregando código JavaScript

Existem algumas maneiras de carregar o código JavaScript em um aplicativo Blazor WebAssembly. Uma maneira é inserir o código JavaScript diretamente em um elemento de script no elemento body do arquivo wwwroot/index.html.

No entanto, em vez de inserir o código JavaScript diretamente no arquivo html, recomendamos o uso de um arquivo JavaScript externo para suas funções JavaScript.

Podemos adicionar um arquivo externo referenciando-o no arquivo wwwroot./index.html. O código a seguir faz referência a um arquivo chamado btwInterop.js que está na pasta wwwroot/scripts.

<script src="scripts/bweInterop.js"></script>

Uma maneira melhor de organizar scripts é colocar um arquivo JavaScript externo com um componente específico. Para adicionar um arquivo JavaScript colocado com um componente específico, crie um arquivo JavaScript na mesma pasta do componente com o mesmo nome do componente, mas com uma extensão de arquivo JS.

Por exemplo, o componente MeuComponente definido no arquivo MeuComponente.razor usaria MeuComponente.razor.js como seu arquivo JavaScript colocado.

Para que o componente faça referência ao código no arquivo JavaScript, o arquivo deve ser importado para o componente durante o método OnAfterRenderAsync do componente.

No exemplo a seguir, o identificador de importação é usado para importar um arquivo JavaScript.

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

Neste código o arquivo JavaScript que está sendo importado está na pasta Pages e é chamado MeuComponente.razor.js.

Nota: Os arquivos JavaScript colocados serão movidos automaticamente para a pasta wwwroot quando o aplicativo for publicado.

O código anterior usa o método InvokeAsync da biblioteca IJSRuntime para invocar a função de importação de JavaScript do .NET.

Chamando código JavaScript a partir do .NET

Existem dois métodos diferentes na biblioteca IJSRutime que podemos usar para invocar JavaScript de .NET de forma assíncrona.

  1. InvokeAsync
  2. InvokeVoidAsync

Para invocar uma função JavaScript do .NET de forma síncrona, a biblioteca IJSRutime deve ser reduzida(downcast) para IJSInProcessRuntime.

Usando InvokeAsync

O método InvokeAsync é um método assíncrono usado para invocar uma função JavaScript que retorna um valor.

Este é o método InvokeAsync da biblioteca IJSRuntime:


  ValueTask<TValue> InvokeAsync<TValue>(string identifier, params object[] args);   
 

Neste código, o primeiro argumento é o identificador da função JavaScript e o segundo argumento é um array de argumentos serializáveis em JSON. O segundo argumento é opcional.

O método InvokeAsync retorna um ValueTask do tipo TValue onde TValue é uma instância desserializada via JSON do valor de retorno do JavaScript.

Em JavaScript, o objeto Window representa a janela do navegador. Para determinar a largura e a altura da janela atual, usamos as propriedades innerWidth e innerHeight do objeto Window.

O código JavaScript a seguir inclui um método chamado getWindowSize que retorna a largura e a altura do objeto Window em um arquivo bweinterop.js :

var bweInterop = {};

bweInterop.getWindowSize = function () {
    var size = {
        width: window.innerWidth,
        height: window.innerHeight
    }
    return size;
}

Esta é a definição da classe WindowSize que é usada para armazenar o tamanho da janela no .NET:

public class WindowSize
{
   public int? Width { get; set; }
   public int? Height { get; set; }
}

A seguir temos o componente Index.razor chamando o método GetWindowSize do arquivo bweInterop.js:

@page "/"
@inject IJSRuntime js

<PageTitle>Home</PageTitle>

@if (windowSize.Width != null)
{
    <h2>
        Tamanho da Janela: @windowSize.Width x @windowSize.Height
    </h2>
}
<button @onclick="GetWindowSize">Tamanho da Janela</button>

@code {

    private WindowSize windowSize = new WindowSize();

    private async ValueTask GetWindowSize()
    {
        windowSize = await js.InvokeAsync<WindowSize>(
            "bweInterop.getWindowSize");
    }
}

Neste código temos:

A injeção da biblioteca IJSRuntime no componente. Quando o botão T'tamanho da janela' for clicado, o método GetWindowSize usa o método InvokeAsync de IJSRuntime para invocar a função JavaScript getWindowSize.

A função JavaScript GetWindowSize retorna a largura e a altura da janela para a propriedade windowSize. Por fim, o componente regenera sua árvore de renderização e aplica quaisquer alterações ao DOM do navegador.

Esta é uma captura de tela da página após clicar no botão 'Tamanho da janela':

O método InvokeSync de IJSRuntime é usado para chamar funções JavaScript que retornam um valor. Se não precisarmos retornar um valor, podemos usar o método InvokeAsync.

Usando o InvokeVoidAsync

O método InvokeVoidAsync é um método assíncrono usado para invocar uma função JavaScript que não retorna um valor.

Este é o método InvokeVoidAsync da bibliotea IJSRuntime:

 
   InvokeVoidAsync(string identifier, params object[] args);      

 

Assim como o método InvokeAsync, o primeiro argumento é o identificador da função JavaScript que está sendo chamada e o segundo argumento é um array de argumentos serializáveis em JSON. O segundo argumento é opcional.

Em JavaScript, o objeto Document representa o nó raiz do documento HTML. A propriedade title do objeto Document é usada para especificar o texto que aparece na barra de título do navegador.

Suponha que queremos atualizar o título do navegador enquanto navegamos entre os componentes em nosso aplicativo Blazor WebAssembly. Para fazer isso, precisamos usar JavaScript para atualizar a propriedade title.

O código JavaScript a seguir exporta uma função chamada setDocumentTitle, que define a propriedade title do objeto Document com o valor fornecido pelo argumento title:

Shared/Document.razor.js

export function setDocumentTitle(title) {
    document.title = title;
}

Neste código usamos uma instrução export para exportar a função setDocumentTitle.

O seguinte componente Document.razor usa a função JavaScript setDocumentTitle para atualizar a barra de título do navegador:

@inject IJSRuntime js

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

    protected override async ValueTask OnAfterRenderAsync
        (bool firstRender)
    {
        if (firstRender)
        {
            IJSObjectReference module =
                await js.InvokeAsync<IJSObjectReference>
                    ("import", "./Shared/Document.razor.js");

            await module.InvokeVoidAsync
                ("setDocumentTitle", Title);
        }
    }
}

O código de marcação a seguir usa o componente Document para atualizar a barra de título do navegador para Home – Meu Aplicativo:

<Document Title="Home - Meu aplicativo" />

Por padrão, as chamadas de interoperabilidade JS são assíncronas. Para fazer chamadas de interoperabilidade JS síncronas, precisamos usar IJSInProcessRuntime.

Usando IJSInProcessRuntime

Até agora examinamos apenas a invocação de funções JavaScript de forma assíncrona. Mas também podemos invocar funções JavaScript de forma síncrona.

Fazemos isso fazendo um downcast de IJSRuntime para IJSInProcessRuntime.

A biblioteca IJSInProcessRuntime permite que nosso código .NET invoque chamadas de interoperabilidade JS de forma síncrona. Isso pode ser vantajoso porque essas chamadas têm menos sobrecarga do que suas contrapartes assíncronas.

Estes são os métodos síncronos de IJsInProcessRuntime:

O código a seguir usa IJSInProcessRuntime para invocar uma função JavaScript de forma síncrona:

@inject IJSRuntime js
@code {
    private string GetGuid()
    {
        string guid = 
            ((IJSInProcessRuntime)js).Invoke<string>("getGuid");
        return guid;
    }
}

Neste código a instância IJsRuntime foi reduzida para uma instância IJSInProcessRuntime e o método Invoke da instância IJSInProcessRuntime foi usado para chamar o método JavaScript getGuid.

A abstração IJSRuntime fornece métodos para invocar funções JavaScript diretamente de métodos .NET. Eles podem ser invocados de forma assíncrona ou síncrona. Chamar um método .NET diretamente de uma função JavaScript requer um atributo especial.

Invocando código .NET a partir de JavaScript

Podemos invocar um método .NET público a partir do JavaScript decorando o método com o atributo JSInvokable.

O método .NET a seguir é decorado com o atributo JSInvokable para permitir que seja invocado do JavaScript:

private WindowSize windowSize = new WindowSize();

[JSInvokable]
public void GetWindowSize(WindowSize newWindowSize)
{
    windowSize = newWindowSize;
    StateHasChanged();
}

Neste código a propriedade windowSize é atualizada sempre que o método GetWindowSize é invocado do JavaScript. Depois que a propriedade windowSize é atualizada, o método StateHasChanged do componente é chamado para notificar o componente de que seu estado foi alterado e, portanto, o componente deve ser renderizado novamente.

Para chamar um método .NET de JavaScript, devemos criar uma classe DotNetObjectReferenece para o JavaScript usar para localizar o método .NET. A classe DotNetObjectReferenece envolve um argumento de interoperabilidade JS, indicando que o valor não deve ser serializado como JSON, mas deve ser passado como uma referência.

Nota: Para evitar vazamentos de memória e permitir a coleta de lixo em um componente que cria uma classe DotNetObjectReference, você deve descartar cuidadosamente cada instância de DotNetObjectReference.

O código a seguir cria uma instância DotNetObjectReference que envolve o componente Resize. A referência é então passada para o método JavaScript:

private DotNetObjectReference<Resize> objRef;

protected async override ValueTask OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        objRef = DotNetObjectReference.Create(this);
        await js.InvokeVoidAsync(
            "bweInterop.registerResizeHandler",
             objRef);
    }
}

Podemos invocar um método em um componente .NET de JavaScript usando uma referência ao componente criado com DotNetObjectReference.

No código JavaScript do arquivo bweInterop.js a seguir, a função registerResizeHandler cria a função resizeHandler que é chamada na inicialização e sempre que a janela for redimensionada.

bweInterop.registerResizeHandler = function (dotNetObjectRef) {
    function resizeHandler() {
        dotNetObjectRef.invokeMethodAsync('GetWindowSize',
            {
                width: window.innerWidth,
                height: window.innerHeight
            });
    };
    resizeHandler();
    window.addEventListener("resize", resizeHandler);
}

 

Aqui a função invokeMethodAsync é usada para invocar o método GetWindowSize da plataforma .NET que foi decorado com o atributo JSInvokable.

Nota : Podemos usar a função invokeMethod ou a função invokeMethodAsync para invocar métodos de instância .NET a partir do JavaScript.

Este é o código .NET completo para o componente Resize:

@page "/resize"
@inject IJSRuntime js
@implements IDisposable

<PageTitle>Resize</PageTitle>

@if (windowSize.Width != null)
{
        <h2>
            Window Size: @windowSize.Width x @windowSize.Height
        </h2>
}
@code {
    private DotNetObjectReference<Resize> objRef;

    private WindowSize windowSize = new WindowSize();

    protected async override Task OnAfterRenderAsync(
        bool firstRender)
    {
        if (firstRender)
        {
            objRef = DotNetObjectReference.Create(this);
            await js.InvokeVoidAsync(
                "bweInterop.registerResizeHandler",
                    objRef);
        }
    }

    [JSInvokable]
    public void GetWindowSize(WindowSize newWindowSize)
    {
        windowSize = newWindowSize;
        StateHasChanged();
    }
    public void Dispose()
    {
        objRef?.Dispose();
    }
}

Neste código o componente Resize exibe a largura e a altura atuais do navegador. Conforme você redimensiona o navegador, os valores exibidos são atualizados automaticamente.

Além disso, o objeto DotNetObjectReference é descartado quando o componente é descartado. Para testar o componente Resize , pressione Ctrl+F5 para iniciar o aplicativo sem depuração. Depois que o aplicativo iniciar, navegue até a página /resize e redimensione a janela.

A abstração IJSRuntime nos fornece uma maneira de invocar funções JavaScript do .NET e invocar métodos .NET do JavaScript.

Para mostrar um exemplo prático de uso do JavaScript vamos usar a API Web Storage do JavaScript na próxima parte do artigo....

"Também não oprimirás o estrangeiro; pois vós conheceis o coração do estrangeiro, pois fostes estrangeiros na terra do Egito."
Êxodo 23:9

Referências:


José Carlos Macoratti