Janeiro de 2019

Volume 34 – Número 1

[Moderno]

Componentes baseados em modelo no Blazor

Por Dino Esposito | janeiro de 2019

Dino EspositoFaz quase um ano desde que o primeiro build público do Blazor foi lançado no início de 2018. Concebida para ser uma estrutura da Web do lado do cliente capaz de executar os códigos C# e .NET de dentro do navegador do host, a plataforma evoluiu em várias direções.

O Blazor continua sendo uma estrutura simples do lado do cliente que processa dados baixados de quaisquer serviços de back-end acessíveis, mas também teve seus fundamentos modificados para ser executado inteiramente do servidor por meio de uma conexão SignalR. A introdução recente do serviço do Azure SignalR apenas confirma, na minha opinião, a intenção da Microsoft de apresentar o Blazor cada vez mais como uma plataforma de desenvolvimento moderna. Um serviço de nuvem escalonável entre o aplicativo do servidor e uma miríade de clientes garante que o código .NET Core possa ser efetivamente hospedado no servidor e ser executado de forma interativa no cliente por meio da intermediação do C# em vez de JavaScript.

Como uma estrutura da Web do lado do cliente, o Blazor (parte dele será fornecida com o lançamento do ASP.NET Core 3.0) não funciona bem sem componentes. Na versão 0.6.0 da estrutura, a equipe do Blazor introduziu um tipo específico de componentes: os baseados em modelo. Neste artigo, exp,icarei como eles funcionam atualizando o exemplo de preenchimento automático explicado nas minhas duas últimas colunas para um componente baseado em modelo.

Como adicionar modelos ao Blazor

Os componentes simples podem ser configurados por meio de propriedades, mas os componentes realistas geralmente precisam de mais flexibilidade de renderização, e os modelos são uma maneira canônica de se conseguir isso. Por exemplo, pense em um componente de grade de dados. Com o Razor, ou qualquer outra infraestrutura de associação de dados, você pode criar com facilidade uma tabela de dados vinculada a uma fonte de dados conhecida. Vejamos um exemplo de como criar uma tabela HTML com base em uma coleção de itens de dados:

<table>
@foreach(var item in Items)
{
  <tr>
    <td>@item.FirstName</td>
    <td>@item.LastName</td>
  </tr>
}
</table>

É rápido e fácil, mas não há muita coisa que pode ser reutilizada. Agora imagine que você torne a estrutura de grades mais rica adicionando um cabeçalho, um rodapé e talvez uma barra de pesquisa na parte superior e uma barra de pager na parte inferior. O código e o layout gráfico por trás da barra de pesquisa e da barra de pager não serão alterados com os dados reais sendo pesquisados e paginados. No entanto, você reescreverá a barra de pesquisa e de pager sempre que usar uma grade de dados para exibir um tipo de dados diferente.

Com esse nível mínimo de abstração, uma grade de países e uma grade de clientes são entidades completamente diferentes, embora o código principal por trás de várias partes internas seja quase o mesmo. Os componentes baseados em modelo abordam esse cenário específico e mostram uma maneira de ter um único componente DataGrid capaz de apresentar, pesquisar e paginar tanto os países quanto os clientes com uma única base de código.

Digamos que você queira ter um componente de grade avançado em suas exibições de front-end do Blazor. A Figura 1 mostra o código-fonte de um novo componente do Blazor de DataSource baseado em modelo.

Figura 1 O componente de modelo de DataSource

@typeparam TItem
@inject HttpClient HttpExecutor
<div style="border: solid 4px #111;">   
  <div class="table-responsive">
    <table class="table table-hover">
      <thead>
        <tr>
          @if (HeaderTemplate != null)
          {
            @HeaderTemplate
          }
        </tr>
      </thead>
      <tbody>
        @foreach (var item in Items)
        {
          <tr>
           @RowTemplate(item)
          </tr>
        }
      </tbody>
      <tfoot>
        <tr>
          @if (FooterTemplate != null)
          {
            @FooterTemplate(Items)
          }
        </tr>
      </tfoot>
    </table>
  </div>
</div>
@functions {
  [Parameter]
  RenderFragment HeaderTemplate { get; set; }
  [Parameter]
  RenderFragment<TItem> RowTemplate { get; set; }
  [Parameter]
  RenderFragment<IList<TItem>> FooterTemplate { get; set; }
  [Parameter]
  IList<TItem> Items { get; set; }
}

Como você pode ver, o componente DataSource é criado em torno do esqueleto de uma tabela HTML na qual o corpo é construído pela iteração de linhas da tabela na parte superior de registros de dados na coleção associada. O cabeçalho e o rodapé são definidos como linhas da tabela, com os detalhes do conteúdo real deixados para as páginas do cliente. As páginas do cliente podem interagir e personalizar ainda mais o componente através da interface pública definida pela seção @functions. Nela, você definiu três modelos — HeaderTemplate, FooterTemplate e RowTemplate — e uma propriedade, chamada Items, que atua como a fonte de dados real e o provedor de dados para o componente.

Você pode ter componentes baseados em modelos vinculados a um tipo de dados fixo ou pode ter componentes vinculados a um tipo de dados genérico especificado de forma declarativa. O mesmo componente de grade pode ser usado realisticamente para apresentar, pesquisar e paginar diferentes coleções de dados. Para dar ao componente essa capacidade, você usa a diretiva @typeparam no Blazor, da seguinte forma:

@typeparam TItem

Qualquer referência ao moniker de TItem encontrada no código-fonte do Razor é tratada como uma referência ao tipo determinado dinamicamente de uma classe genérica de C#. Você especifica o tipo real que está sendo usado no componente através da propriedade TItem. A Figura 2 mostra como uma página do cliente declararia um componente de modelo de DataSource.

Figura 2 Declarando um componente do modelo de DataSource

<DataSource Items="@Countries" TItem="Country">
  <HeaderTemplate>
    <th>Name</th>
    <th>Capital</th>
  </HeaderTemplate>
  <RowTemplate>
    <td>@context.CountryName</td>
    <td>@context.Capital</td>
  </RowTemplate>
  <FooterTemplate>
    <td colspan="2">
      @context.Count countries found.
    </td>
  </FooterTemplate>
</DataSource>

O nome da propriedade de tipo genérico — TItem no trecho de código anterior — corresponderá ao nome do parâmetro de tipo conforme declarado por meio da diretiva @typeparam dentro do código-fonte do componente. Observe que, frequentemente, o parâmetro de tipo genérico é apenas inferido pela estrutura e pode não ser especificado.

Aspectos da programação dos modelos

Um modelo Blazor é uma instância do tipo RenderFragment. Dito de outra forma, é uma parte da marcação sendo renderizada pelo mecanismo de exibição do Razor que pode ser tratada como uma instância simples de um tipo .NET. A maioria dos modelos não tem parâmetros, mas você também pode torná-los genéricos. Um modelo genérico receberá uma instância do tipo especificado como argumento e poderá usar esse conteúdo para renderizar sua saída. No exemplo da Figura 2, o modelo de cabeçalho não tem parâmetros, mas os modelos de linha e de rodapé são genéricos.

Em particular, a propriedade RowTemplate usa uma instância TItem, enquanto a propriedade FooterTemplate recebe uma coleção de instâncias TItem. Se necessário, você também pode definir que um modelo receba uma instância de um tipo fixo. Por exemplo, o FooterTemplate poderia passar apenas um número inteiro indicando a quantidade de itens sendo renderizados na página. Isso é mostrado no código aqui:

RenderFragment<TItem> RowTemplate { get; set; }
RenderFragment<IList<TItem>> FooterTemplate { get; set; }

Encapsulando sua chamada com uma verificação simples para existência antes do uso, é possível fazer com que seja opcional a implementação de um modelo em uma página do cliente. Veja como um componente do Blazor pode tornar opcional uma das suas propriedades de modelo:

@if (FooterTemplate != null)
{
  @FooterTemplate(Items)
}

Ao renderizar um modelo paramétrico, você usa o nome implícito do “contexto” para fazer referência ao argumento do modelo. Por exemplo, ao renderizar uma linha da tabela através da propriedade RowTemplate, você usa o parâmetro de contexto para se referir ao item sendo renderizado, da seguinte forma:

<RowTemplate>
  <td>@context.CountryName</td>
  <td>@context.Capital</td>
</RowTemplate>

Observe que o nome do argumento de contexto pode ser alterado de forma declarativa por meio da propriedade Context do modelo, conforme mostrado aqui:

<RowTemplate Context="dataItem">
  <td>@dataItem.CountryName</td>
  <td>@dataItem.Capital</td>
</RowTemplate>

Como resultado, o componente genérico DataSource pode ser usado na mesma exibição do Blazor para preencher grades de dados de diferentes tipos de dados, conforme descrito na Figura 3. A estrutura do código que você grava é resumida assim:

<DataSource Items="@Countries" TItem="Country">
  ...
</DataSource>
<DataSource Items="@Forecasts" TItem="WeatherForecast">
  ...
</DataSource>

Componente genérico DataSource
Figura 3 Componente genérico DataSource

Todas as marcações e códigos que possam haver ao redor da grade exibida (por exemplo, barra de pager, barra de pesquisa, botões de classificação) são inteiramente reutilizados. Como uma observação pessoal, esse nível mais profundo de personalização de marcação me lembra dos velhos tempos do Web Forms do ASP.NET, em que os controles de servidor personalizados, por meio de modelos e propriedades personalizadas, definiam a própria linguagem específica de domínio. Isso tornou o processo de delinear a interface do usuário desejada bastante suave e fluente. O advento do MVC e a mudança subsequente para o desenvolvimento da Web simples e no lado do cliente nos aproximou do mecanismo de HTML e nos afastou da abstração. Os componentes da estrutura estão apenas tentando recuperar esse nível de expressividade.

Regravar o componente TypeAhead do Blazor

No mês passado (msdn.com/magazine/mt830376), apresentei um componente de preenchimento automático inteiramente escrito no Blazor, fornecendo a mesma funcionalidade que o popular — baseado em JavaScript — plug-in TypeAhead do Twitter. Nessa implementação, o ponto de extremidade do servidor encarregado de retornar as dicas foi forçado a retornar um objeto de transferência de dados compacto e de uso geral com três propriedades: a ID exclusiva do objeto identificado na consulta, um texto de exibição e uma terceira renderização da propriedade da marcação a ser exibida na caixa suspensa para cada dica.

Na demonstração do mês passado, usei o componente de preenchimento automático para encontrar o nome de um país. O ponto de extremidade do servidor retornou um objeto feito com o código ISO do país, o nome do país e um trecho de HTML com o nome do país, sua capital e continente. O trecho de HTML, no entanto, estava sob o controle total da implementação do servidor e não havia chance de o autor da página do cliente que usava o componente de preenchimento automático controlar o layout do trecho de HTML. Esse é o cenário perfeito para ver componentes baseados em modelos em ação fora do âmbito de uma demonstração básica, como fiz até agora.

A Figura 4 apresenta o layout em HTML do novo componente de preenchimento automático. É o mesmo código do mês passado, com uma exceção notável: o formato dos dados retornados pelo provedor de dicas. Na implementação original, o provedor de dicas (um exemplo de controlador) retornava um objeto de transferência de dados e, portanto, era o único ponto de controle dos dados reais que estavam sendo selecionados pelo usuário. Pedir para o provedor de dicas retornar uma lista de países em vez de uma lista de itens de preenchimento automático personalizados permite que o componente do Blazor exponha uma propriedade ItemTemplate para que os chamadores decidam o layout de cada item de menu suspenso. Receber uma lista de países permite que o componente acione seu evento OnSelection, passando diretamente a instância do item de dados selecionado para qualquer ouvinte interessado.

Figura 4 Um componente TypeAhead baseado em modelo

<div>
  <div class="input-group">
    <input type="text" class="@Class"
           oninput="this.blur(); this.focus();"
           bind="@SelectedText"
           onblur="@(ev => TryAutoComplete(ev))" />
    <div class="input-group-append">
      <button class="btn dropdown-toggle"
              type="button"
              data-toggle="dropdown"
              style="display: none;">
        <i class="fa fa-chevron-down"></i>
      </button>
      <div class="dropdown-menu @(_isOpen ? "show" : "")"
           style="width: 100%;">
        <h6 class="dropdown-header">
          @Items.Count item(s)
        </h6>
        @foreach (var item in Items)
        {
          <a class="dropdown-item"
           onclick="@(() => TrySelect(item))">
            @ItemTemplate(item)
          </a>
        }
      </div>
    </div>
  </div>
</div>

Para ser preciso, um campo oculto para coletar, via código, a ID exclusiva do item de dados selecionado ainda pode ser necessário para garantir que, uma vez usado em um formulário HTML, o componente de preenchimento automático possa postar seu conteúdo com êxito através do canal normal do navegador. No entanto, agora o campo oculto pode ser colocado mais naturalmente fora dos limites do componente de preenchimento automático e estar sob o controle total do desenvolvedor do cliente.

O componente de preenchimento automático define uma propriedade de modelo denominada ItemTemplate, que é definida da seguinte maneira:

[Parameter]
RenderFragment<TItem> ItemTemplate { get; set; }

O parâmetro TItem é definido pelo chamador. Em resumo, vejamos a marcação que configura um componente de preenchimento automático com modelos de item:

<Typeahead TItem="Country"
           url="/hint/countries"
           name="country"
           onSelectionMade="@ShowSelection">
  <ItemTemplate>
    <span>@context.CountryName</span>&nbsp;
    <b>(@context.ContinentName)</b>
    <small class="pull-right">@context.Capital</small>
  </ItemTemplate>
</Typeahead>

O parâmetro TItem informa ao componente que ele manipulará objetos do tipo País e receberá dicas da URL especificada. Sempre que forem recebidas dicas baseadas no texto digitado, elas serão renderizadas em uma lista suspensa dinâmica usando a marcação na seção ItemTemplate. Por design, o modelo de item recebe uma instância do item de dados atual e cria uma linha HTML. Não é preciso nem dizer que a forma da lista suspensa está agora inteiramente sob o controle do autor da página. E isso é um grande avanço (veja a Figura 5).

A lista suspensa de países
Figura 5 A lista suspensa de países

Conclusão

O componente TypeAhead é um exemplo interessante de programação de componentes no Blazor. Ele incorpora a lógica para recuperar dados através do protocolo HTTP, bem como modelos e alguma interação interna entre os elementos (o campo de entrada e a lista suspensa). Em seguida, ele se comunica com o mundo exterior por meio de eventos.

Nascido como um experimento cativante, o Blazor está crescendo significativamente, embora o caminho à frente não esteja completamente claro e possa mudar nos próximos meses. No momento, o principal objetivo da equipe é fornecer suporte para executar o Blazor do lado do cliente no navegador, via WebAssembly.

Ao mesmo tempo, a incorporação do Blazor ao ASP.NET Core traz muitos benefícios; um deles é o tempo de carregamento de aplicativos muito mais rápido. Os componentes do Blazor que serão integrados ao ASP.NET Core 3.0 serão renomeados para Componentes do Razor. É apenas um nome diferente escolhido para deixar as coisas claras e evitar possíveis confusões entre o que é executado no navegador e o que é executado no servidor que produz saída para o cliente. De qualquer forma, espera-se que o modelo de componente permaneça o mesmo, independentemente de você estar executando no servidor ou no cliente.


Dino Esposito é autor de mais de 20 livros e de mais de mil artigos em seus 25 anos de carreira. Autor de “The Sabbatical Break”, um show de estilo cênico, Esposito se ocupa escrevendo software para um mundo mais ecológico, como estrategista digital da BaxEnergy. Siga-o no Twitter: @despos.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Daniel Roth