Março de 2019

Volume 34 – Número 3

[Desenvolvimento para a Web]

Pilha completa em C# com Blazor

Por Jonathan C. Miller | Março de 2019 | Obtenha o código

Blazor, a estrutura experimental da Microsoft que traz C# para o navegador, é a peça que faltava no quebra-cabeça do C#. Atualmente, um programador de C# cria aplicativos para computador, servidor Web, nuvem, smartphone, tablet, relógio, TV e IoT. O Blazor completa o quebra-cabeça, e permite ao desenvolvedor de C# compartilhar código e lógica de negócios no navegador do usuário. Esta é uma funcionalidade poderosa e uma melhoria de produtividade gigantesca para desenvolvedores de C#.

Neste artigo, vou demonstrar um caso de uso comum para compartilhamento de código. Demonstrarei como compartilhar a lógica de validação entre um cliente de Blazor e um aplicativo de servidor de API Web. Hoje em dia é esperado que você valide a entrada não apenas no servidor, mas também no navegador do cliente. Os usuários de aplicativos Web modernos esperam comentários quase em tempo real. Os dias de preenchimento de um formulário longo e clicar em Enviar somente para ver um erro em vermelho ser retornado estão basicamente no fim.

Um aplicativo Web de Blazor executado dentro do navegador pode compartilhar código com um servidor de back-end de C#. Você pode colocar sua lógica em uma biblioteca compartilhada e utilizá-la tanto no front-end como no back-end. Isso traz muitos benefícios. Você pode colocar todas as regras em um lugar e saber que elas só precisam ser atualizadas em um único local. Você sabe que elas realmente funcionarão da mesma forma, porque são o mesmo código. Economiza-se bastante tempo para testes e solução de problemas em que a lógica do cliente e do servidor nem sempre é exatamente a mesma.

Talvez o mais importante, é que você pode usar uma biblioteca para a validação no cliente e no servidor. Tradicionalmente, um front-end de JavaScript força os desenvolvedores a escrever duas versões de regras de validação; uma no JavaScript para o front-end e outra na linguagem usada no back-end. Tentativas de resolver essa incompatibilidade envolvem estruturas de regras complicadas e camadas adicionais de abstração. Com Blazor, a mesma biblioteca de .NET Core é executada no cliente e servidor.

O Blazor ainda é uma estrutura experimental, mas está avançando rapidamente. Antes de compilar este exemplo, verifique se você tem a versão correta do Visual Studio, o SDK do .NET Core e os serviços de linguagem do Blazor instalados. Examine as etapas de Introdução no blazor.net.

Criação de um novo aplicativo Blazor

Primeiro, vamos criar um novo aplicativo Blazor. Na caixa de diálogo Novo projeto, clique em Aplicativo Web ASP.NET Core, clique em OK, em seguida selecione o ícone do Blazor na caixa de diálogo mostrada na Figura 1. Clique em OK. Isso criará o aplicativo de exemplo padrão do Blazor. Caso já tenha experimentado o Blazor, este aplicativo padrão será familiar para você.

Escolha de um aplicativo Blazor
Figura 1 Escolha de um aplicativo Blazor

A lógica compartilhada que valida as regras de negócio será demonstrada em um novo formulário de registro. A Figura 2 mostra um formulário simples com campos de Nome, Sobrenome, Email e Telefone. Neste exemplo, ela validará que todos os campos são obrigatórios, que os campos de Nome e Sobrenome possuem comprimento máximo e se os campos de Email e Telefone estão no formato correto. Ela exibirá uma mensagem de erro para cada campo, e essas mensagens serão atualizadas conforme o usuário digita. Por fim, o botão Registrar só será habilitado se não houver nenhum erro.

Formulário de registro
Figura 2 Formulário de registro

Biblioteca compartilhada

Todo o código que precisa ser compartilhado entre o servidor e o cliente Blazor será colocado em um projeto de biblioteca compartilhada separado. A biblioteca compartilhada conterá a classe de modelo e um mecanismo muito simples de validação. A classe de modelo conterá os campos de dados do formulário de registro. É semelhante ao seguinte:

public class RegistrationData : ModelBase
{
  [RequiredRule]
  [MaxLengthRule(50)]
  public String FirstName { get; set; }
 
  [RequiredRule]
  [MaxLengthRule(50)]
  public String LastName { get; set; }
 
  [EmailRule]
  public String Email { get; set; }
 
  [PhoneRule]
  public String Phone { get; set; }
 
}

A classe RegistrationData herda de uma classe ModelBase, que contém toda a lógica que pode ser usada para validar as regras e retornar mensagens de erro que estão associadas à página Blazor. Cada campo é decorado com atributos que mapeiam para as regras de validação. Optei por criar um modelo muito simples que seja semelhante ao modelo de anotação de dados do Entity Framework (EF). Toda a lógica para este modelo está contida na biblioteca compartilhada.

A classe ModelBase contém métodos que podem ser usados pelo aplicativo cliente ou servidor do Blazor para determinar se há erros de validação. Ela também acionará um evento quando o modelo for alterado, para que o cliente possa atualizar a interface do usuário. Qualquer classe de modelo pode herdar a partir dela e obter toda a lógica do mecanismo de validação automaticamente.

Vou começar criando primeiro uma nova classe ModelBase dentro do projeto SharedLibrary, da seguinte forma:

public class ModelBase
{
}

Erros e regras

Agora, adicionarei um dicionário particular à classe ModelBase que contém uma lista de erros de validação. O dicionário _errors é ligado ao nome do campo e ao nome da regra. O valor é a mensagem de erro real a ser exibida. Essa configuração facilita determinar se há erros de validação para um campo específico e recuperar as mensagens de erro rapidamente. Eis o código:

private Dictionary<String, Dictionary<String, String>> _errors =
  new Dictionary<string, Dictionary<string, string>>();

Agora vou adicionar o método AddError para inserir erros no dicionário de erros internos. AddError tem parâmetros para fieldName, ruleName e errorText. Ele pesquisa o dicionário de erros internos e remove as entradas se estas já existirem e, em seguida, adiciona a nova entrada de erro, conforme mostrado neste código:

private void AddError(String fieldName, String ruleName, String errorText)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  if (_errors[fieldName].ContainsKey(ruleName))
     { _errors[fieldName].Remove(ruleName); }
  _errors[fieldName].Add(ruleName, errorText);
  OnModelChanged();
}

Por fim, vou adicionar o método RemoveError, que aceita os parâmetros fieldName e ruleName e pesquisa no dicionário de erros internos um erro correspondente para removê-lo. Eis o código:

private void RemoveError(String fieldName, String ruleName)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  if (_errors[fieldName].ContainsKey(ruleName))
     { _errors[fieldName].Remove(ruleName);
    OnModelChanged();
  }
}

A próxima etapa é adicionar as funções de CheckRules que são responsáveis por localizar as regras de validação anexadas ao modelo e executá-las. Há duas funções diferentes de CheckRules: Uma que não tem um parâmetro e verifica todas as regras em todos os campos e uma segunda, que tem um parâmetro fieldName e valida apenas um campo específico. Essa segunda função é usada quando um campo é atualizado e as regras para esse campo são validadas imediatamente.

A função CheckRules usa reflexão para encontrar a lista de atributos anexados a um campo. Em seguida, ela testa cada atributo para ver se é um tipo de IModelRule. Quando um IModelRule é encontrado, ela chama o método Validate e retorna o resultado, conforme mostrado na Figura 3.

Figura 3 A função CheckRules

public void CheckRules(String fieldName)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  var attrInfos = propertyInfo.GetCustomAttributes(true);
  foreach (var attrInfo in attrInfos)
  {
    if (attrInfo is IModelRule modelrule)
    {
      var value = propertyInfo.GetValue(this);
      var result = modelrule.Validate(fieldName, value);
      if (result.IsValid)
      {
        RemoveError(fieldName, attrInfo.GetType().Name);
      }
      else
      {
        AddError(fieldName, attrInfo.GetType().Name, result.Message);
      }
    }
  }
}
 
public bool CheckRules()
{
  foreach (var propInfo in this.GetType().GetProperties(
    System.Reflection.BindingFlags.Public |
      System.Reflection.BindingFlags.Instance))
    CheckRules(propInfo.Name);
 
  return HasErrors();
}

Em seguida, adiciono a função Errors. Essa função usa um nome de campo como um parâmetro e retorna uma cadeia de caracteres que contém a lista de erros para esse campo. Ela usa o dicionário interno _errors para determinar se há erros para esse campo, conforme mostrado aqui:

public String Errors(String fieldName)
{
  if (!_errors.ContainsKey(fieldName)) { _errors.Add(fieldName,
    new Dictionary<string, string>()); }
  System.Text.StringBuilder sb = new System.Text.StringBuilder();
  foreach (var value in _errors[fieldName].Values)
    sb.AppendLine(value);
 
  return sb.ToString();
}

Agora, preciso adicionar a função HasErrors, que retornará true se houver erros em qualquer campo do modelo. Esse método é usado pelo cliente para determinar se o botão Registrar deve ser habilitado. Ele também é usado pelo servidor de API Web para determinar se os dados de entrada do modelo têm erros. Abaixo está o código de função:

public bool HasErrors()
{
  foreach (var key in _errors.Keys)
    if (_errors[key].Keys.Count > 0) { return true; }
  return false;
}

Valores e eventos

É hora de adicionar o método GetValue, que leva um parâmetro fieldname e utiliza a reflexão para localizar o campo no modelo e retornar seu valor. Ele é usado pelo cliente Blazor para recuperar o valor atual e exibi-lo na caixa de entrada, conforme mostrado aqui:

public String GetValue(String fieldName)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  var value = propertyInfo.GetValue(this);
 
  if (value != null) { return value.ToString(); }
  return String.Empty;           
}

Agora, adicione o método SetValue. Ele utiliza a reflexão para localizar o campo no modelo e atualizar seu valor. Em seguida, ele dispara o método CheckRules que valida todas as regras no campo. Ele é usado no cliente Blazor para atualizar o valor conforme o usuário digita na caixa de texto de entrada. Eis o código:

public void SetValue(String fieldName, object value)
{
  var propertyInfo = this.GetType().GetProperty(fieldName);
  propertyInfo.SetValue(this, value);
  CheckRules(fieldName);
}

Finalmente, adiciono o evento para ModelChanged, que é gerado quando um valor no modelo foi alterado ou uma regra de validação foi adicionada ou removida do dicionário interno de erros. O cliente Blazor escuta esse evento e atualiza a interface do usuário quando ele é acionado. Isso é o que faz com que os erros exibidos sejam atualizados, conforme mostrado neste código:

public event EventHandler<EventArgs> ModelChanged;
 
protected void OnModelChanged()
{
  ModelChanged?.Invoke(this, new EventArgs());
}

Esse mecanismo de validação é reconhecidamente um design muito simples com muitas oportunidades de aprimoramento. Em um aplicativo de negócios de produção, seria útil ter níveis de severidade para os erros, como Informação, Aviso e Erro. Em determinados cenários, seria útil se as regras pudessem ser carregadas dinamicamente de um arquivo de configuração sem a necessidade de modificar o código. Não estou defendendo que você crie seu próprio mecanismo de validação; há muitas opções por aí. Este foi projetado para ser bom o suficiente para demonstrar um exemplo do mundo real, mas simples o suficiente para caber neste artigo e ser fácil de entender.

Criação das regras

Neste ponto, há uma classe RegistrationData que contém os campos de formulário. Os campos na classe são decorados com atributos como RequiredRule e EmailRule. A classe RegistrationData herda de uma classe ModelBase que contém toda a lógica para validar as regras e para notificar o cliente de alterações. A última parte do mecanismo de validação é a lógica da regra em si. Vou explorar isso a seguir.

Eu começo criando uma nova classe na SharedLibrary chamada IModelRule. Essa regra consiste em um único método Validate que retorna um ValidationResult. Cada regra deve implementar a interface IModelRule, conforme mostrado aqui:

public interface IModelRule
{
  ValidationResult Validate(String fieldName, object fieldValue);
}

Em seguida, crio uma nova classe na SharedLibrary chamada ValidationResult, que consiste em dois campos. O campo IsValid informa se a regra é válida ou não, enquanto o campo Message contém a mensagem de erro a ser exibida quando a regra é inválida. Este é o código:

public class ValidationResult
{
  public bool IsValid { get; set; }
  public String Message { get; set; }
}

O aplicativo de exemplo usa quatro regras diferentes, que são classes públicas que herdam da classe Attribute e implementam a interface IModelRule.

Agora é hora de criar as regras. Tenha em mente que todas as regras de validação são simplesmente as classes que herdam da classe Attribute e implementam o método Validate da interface de IModelRule. A regra de comprimento máximo na Figura 4 retornará um erro se o texto inserido for maior que o comprimento máximo especificado. As outras regras para Obrigatório, Telefone e Email, funcionam da mesma forma, mas com uma lógica diferente para o tipo de dados que elas validam.

Figura 4 A classe MaxLengthRule

public class MaxLengthRule : Attribute, IModelRule
{
  private int _maxLength = 0;
  public MaxLengthRule(int maxLength) { _maxLength = maxLength; }
 
  public ValidationResult Validate(string fieldName, object fieldValue)
  {
    var message = $"Cannot be longer than {_maxLength} characters";
    if (fieldValue == null) { return new ValidationResult() { IsValid = true }; }
 
    var stringvalue = fieldValue.ToString();
    if (stringvalue.Length > _maxLength )
    {
      return new ValidationResult() { IsValid = false, Message = message };
    }
    else
    {
      return new ValidationResult() { IsValid = true };
    }
  }
}

Criação do formulário de registro do Blazor

Agora que o mecanismo de validação está completo na biblioteca compartilhada, ele pode ser aplicado a um novo formulário de registro no aplicativo Blazor. Eu começo adicionando primeiro uma referência ao projeto shared-library a partir do aplicativo Blazor. Você faz isso na janela Solução da caixa de diálogo Gerenciador de referências, conforme mostrado na Figura 5.

Adição de uma referência à biblioteca compartilhada
Figura 5 Adição de uma referência à biblioteca compartilhada

Em seguida, adiciono um novo link de navegação ao NavMenu do aplicativo. Abro o arquivo Shared\NavMenu.cshtml e adiciono um novo link de formulário de registro à lista, conforme mostrado na Figura 6.

Figura 6 Adição de um link de formulário de registro

<div class=@(collapseNavMenu ? "collapse" : null) onclick=@ToggleNavMenu>
  <ul class="nav flex-column">
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="" Match=NavLinkMatch.All>
        <span class="oi oi-home" aria-hidden="true"></span> Home
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="counter">
        <span class="oi oi-plus" aria-hidden="true"></span> Counter
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="fetchdata">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
      </NavLink>
    </li>
    <li class="nav-item px-3">
      <NavLink class="nav-link" href="registrationform">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Registration Form
      </NavLink>
    </li>
  </ul>
</div>

Por último, adiciono o novo arquivo RegistrationForm.cshtml na pasta Pages. Você pode fazer isso com o código mostrado na Figura 7.

O código cshtml na Figura 7 inclui quatro campos <TextInput > dentro da marca <form>. A marca <TextInput> é um componente personalizado do Blazor que manipula a associação de dados e a lógica de exibição de erro para o campo. O componente precisa apenas de três parâmetros para funcionar:

  • Campo Model: identifica a classe à qual ele é associado.
  • FieldName: identifica o membro de dados para associar dados.
  • Campo DisplayName: permite ao componente exibir mensagens amigáveis ao usuário.

Figura 7 Adição do arquivo RegistrationForm.cshtml

@page "/registrationform"
@inject HttpClient Http
@using SharedLibrary
 
<h1>Registration Form</h1>
 
@if (!registrationComplete)
{
  <form>
    <div class="form-group">
      <TextInput Model="model" FieldName="FirstName" DisplayName="First Name" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="LastName" DisplayName="Last Name" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="Email" DisplayName="Email" />
    </div>
    <div class="form-group">
      <TextInput Model="model" FieldName="Phone" DisplayName="Phone" />
    </div>
 
    <button type="button" class="btn btn-primary" onclick="@Register"
      disabled="@model.HasErrors()">Register</button>
  </form>
}
else
{
  <h2>Registration Complete!</h2>
}
 
@functions {
  bool registrationComplete = false;
  RegistrationData model { get; set; }
 
  protected override void OnInit()
  {
    base.OnInit();
    model = new RegistrationData() { FirstName =
      "test", LastName = "test", Email = "test@test.com", Phone = "1234567890" };
    model.ModelChanged += ModelChanged;
    model.CheckRules();
  }
 
  private void ModelChanged(object sender, EventArgs e)
  {
    base.StateHasChanged();
  }
 
  async Task Register()
  {
    await Http.PostJsonAsync<RegistrationData>(
      "https://localhost:44332/api/Registration", model);
    registrationComplete = true;
  }
}

Dentro do bloco @functions da página, o código é mínimo. O método OnInit inicializa a classe de modelo com alguns dados de teste dentro dela. Ele é associado ao evento ModelChanged e chama o método CheckRules para validar as regras. O manipulador de ModelChanged chama o método base.StateHasChanged para forçar uma atualização da interface do usuário. O método Register é chamado quando o botão Registrar é clicado, e envia os dados de registro para um serviço de API Web de back-end.

O componente TextInput contém o rótulo de entrada, a caixa de texto de entrada, a mensagem de erro de validação e a lógica para atualizar o modelo conforme o usuário digita. Os componentes de Blazor são muito simples de escrever e fornecem uma maneira eficiente de decompor uma interface em partes reutilizáveis. Os membros de parâmetro são decorados com o atributo Paramater, informando ao Blazor que eles são parâmetros do componente.

O evento oninput da caixa de texto de entrada é ligado ao manipulador OnFieldChanged. Ele é acionado sempre que a entrada é alterada. O manipulador OnFieldChanged então chama o método SetValue, que faz com que as regras para esse campo sejam executadas, e a mensagem de erro seja atualizada em tempo real conforme o usuário digita. A Figura 8 mostra o código.

Figura 8 Atualização da mensagem de erro

@using SharedLibrary
 
<label>@DisplayName</label>
<input type="text" class="form-control" placeholder="@DisplayName"
  oninput="@(e => OnFieldChanged(e.Value))"
  value="@Model.GetValue(FieldName)" />
<small class="form-text" style="color:darkred;">@Model.Errors(FieldName)
  </small>
 
@functions {
 
  [Parameter]
  ModelBase Model { get; set; }
 
  [Parameter]
  String FieldName { get; set; }
 
  [Parameter]
  String DisplayName { get; set; }
 
  public void OnFieldChanged(object value)
  {
    Model.SetValue(FieldName, value);
  }
}

Validação no servidor

O mecanismo de validação agora está configurado e funcionando no cliente. A próxima etapa é usar a biblioteca compartilhada e o mecanismo de validação no servidor. Para fazer isso, começo adicionando outro projeto de Aplicativo Web do ASP.NET Core à solução. Desta vez escolho API em vez de Blazor na caixa de diálogo Novo aplicativo Web do ASP.NET Core, mostrada na Figura 1.

Depois de criar o novo projeto API, adiciono uma referência ao projeto compartilhado, assim como fiz no aplicativo cliente Blazor (ver Figura 5). Em seguida, adiciono um novo controlador ao projeto API. O novo controlador aceitará a chamada RegistrationData do cliente Blazor, conforme mostrado na Figura 9. O controlador de registro é executado no servidor e é típico de um servidor de API de back-end. A diferença aqui é que ele agora executa as mesmas regras de validação que são executadas no cliente.

Figura 9 O controlador de registro

[Route("api/Registration")]
[ApiController]
public class RegistrationController : ControllerBase
{
  [HttpPost]
  public IActionResult Post([FromBody] RegistrationData value)
  {
    if (value.HasErrors())
    {
      return BadRequest();
    }
    // TODO: Save data to database
    return Created("api/registration", value);
  }
}

O controlador de registro tem um único método POST que aceita RegistrationData como seu valor. Ele chama o método HasErrors, que valida todas as regras e retorna um valor booliano. Se houver erros, o controlador retorna uma resposta BadRequest; caso contrário, ele retornará uma resposta de êxito. Deixei de fora, intencionalmente, o código que salvaria os dados de registro em um banco de dados para que eu pudesse me concentrar no cenário de validação. A lógica de validação compartilhada agora é executada no cliente e no servidor.

O quadro geral

Esse exemplo simples de compartilhamento da lógica de validação no navegador e no back-end são apenas a ponta do iceberg do que é possível fazer com um ambiente de C# de pilha completa. A mágica do Blazor é que ele permite que o exército de desenvolvedores de C# existente crie aplicativos de página única avançados, modernos e responsivos com um período mínimo de crescimento. Ele permite que as empresas reutilizem e remontem o código existente para que ele possa ser executado direito no navegador. A capacidade de compartilhar código C# entre o navegador, a área de trabalho, o servidor, a nuvem e as plataformas móveis pode aumentar bastante a produtividade do desenvolvedor. Ele também permite aos desenvolvedores oferecer mais recursos e mais valor comercial para os clientes com maior rapidez.

 


Jonathan Milleré arquiteto sênior. Ele tem desenvolvido produtos na pilha da Microsoft há dez anos e programação no .NET desde seu início. Miller é um desenvolvedor de produto de pilha completa com experiência em tecnologias de front-end (Windows Forms, Windows Presentation Foundation, Silverlight, ASP.NET, Angular/Bootstrap), middleware (Windows services, API da Web) e back-ends (SQL server, Azure).

Agradecemos ao especialista técnico, Dino Esposito, pela revisão deste artigo