Março de 2018

Volume 33 – Número 3

ASP.NET - Usar o Razor para gerar HTML para modelos em um aplicativo de página única

Por Nick Harrison

Os aplicativos de página única (SPA) são muito populares por uma boa razão. Os usuários esperam que os aplicativos Web sejam rápidos, envolventes e trabalhem em todos os dispositivos, de smartphones a computadores com as telas mais amplas. Além disso, eles precisam ser seguros, visualmente envolventes e ter uma funcionalidade útil. Isso é pedir muito de um aplicativo Web, mas na verdade é apenas o ponto de partida.

Conforme a expectativa dos usuários em relação a aplicativos Web foi aumentando, houve uma explosão de estruturas de usuário para “ajudar” a lhes fornecer uma implementação do padrão Model-View-ViewModel (MVVM). A impressão que dá é que todas as semanas há uma nova estrutura sendo lançada. Algumas provaram ser úteis, outras nem tanto. Mas todas elas implementam o padrão de design de forma diferente, adicionando novos recursos ou solucionando problemas recorrentes de forma única. Cada estrutura adota uma abordagem diferente face ao padrão de design, com uma sintaxe única para modelos e introduzindo conceitos personalizados como fábricas, módulos e observáveis. O resultado é uma curva de aprendizagem acentuada, que pode frustrar os desenvolvedores que estão tentando permanecer atualizados em meio à entrada de saída de moda de novas estruturas. Qualquer coisa que possamos fazer para facilitar essa curva de aprendizagem é bom.

Fazer com que as estruturas do lado do cliente implementem o padrão MVVM e com que o ASP.NET implemente o padrão MVC no servidor gerou um pouco de confusão. Como combinar MVC de servidor com MVVM do lado do cliente? Para muitos, a resposta é simples: eles não se combinam. A abordagem comum é compilar páginas HTML estáticas ou fragmentos e fornecê-los ao cliente com um mínimo de processamento do lado do servidor. Neste cenário, os controladores da API Web podem frequentemente substituir o processamento que teria sido manipulado pelo Controlador MVC no passado.

Como mostra a Figura 1, os requisitos para o servidor Web e um servidor totalmente desenvolvido para a API Web são mínimos, sendo que os dois servidores precisam ser acessados de fora da firewall.

Layout típico de aplicativo
Figura 1 Layout típico de aplicativo

Esta disposição consegue separar bem os problemas, e muitos aplicativos foram escritos seguindo este padrão, mas como desenvolvedor, muita coisa é deixada sobre a mesa. O ASP.NET tem muito a oferecer e sua implementação do MVC oferece muitos recursos que ainda são relevantes, mesmo quando uma das estruturas do lado do cliente faz uma boa parte do trabalho pesado. Neste artigo, focarei em um desses recursos, o Mecanismo de Exibição do Razor.

Razor no aplicativo de página única

O Mecanismo de Exibição do Razor é uma maravilha quando o assunto é simplificar a criação de marcações HTML. Com apenas alguns ajustes, a marcação gerada é facilmente personalizada para de adaptar às expectativas do modelo para qualquer que seja a estrutura usada no cliente. Destacarei alguns exemplos com o Angular e o Knockout, mas essas técnicas funcionarão independentemente da estrutura que você implementar. Se você fizer chamadas de volta ao servidor para fornecer modelos ao seu aplicativo, agora é possível usar o Razor para o trabalho pesado que gera o HTML.

Há muito o que gostar aqui. O EditorTemplates e o DisplayTemplates ainda são seus amigos, assim como os modelos de scaffolding. Você ainda pode injetar exibições parciais, e o fluxo fluido da sintaxe Razor ainda está lá para você tirar vantagem dela. Além da exibições, você também pode usar o controlador MVC, que pode adicionar a potência de processamento para agilizar as coisas e adicionar uma camada extra de segurança. Se isso não for necessário, os controladores poderão ser uma passagem simples pela exibição.

Para demonstrar como isso pode ser usado a seu favor, vou percorrer a criação de algumas telas de entrada de dados para um aplicativo de rastreio de tempo, mostrando como o Razor pode ajudar a agilizar a criação de um HTML adequado para ser usado pelo Angular ou o Knockout.

Como mostra a Figura 2, não há muitas mudanças quanto ao layout típico. Eu incluí uma opção mostrando que o aplicativo MVC poderia interagir com esse banco de dados. Nesta fase, isso não e necessário, mas está disponível se você quiser simplificar seu processamento. Em geral, o fluxo tem o seguinte aspecto:

  • Recupere uma página completa do aplicativo MVC, incluindo todas as referências e conteúdo minimalista da planilha de estilo e do JavaScript.
  • Assim que a página estiver totalmente carregada no navegador e a estrutura inicializada, esta pode chamar o servidor MVC para solicitar um modelo como Exibição Parcial.
  • Este modelo é gerado com o Razor, tipicamente sem quaisquer dados associados.
  • Ao mesmo tempo, a estrutura faz uma chamada à API para obter os dados que serão associados ao modelo recebido do site MVC.
  • Quando o usuário fizer qualquer edição necessária, a estrutura faz chamadas ao servidor da API Web para atualizar o banco de dados do back-end.

Adição de fluxo simples no Razor
Figura 2 Adição de fluxo simples no Razor

Quando um novo modelo é solicitado, este fluxo de trabalho é repetido tanto quanto necessário. Se a estrutura permitir que você especifique uma URL para um modelo, o MVC pode fornecer o modelo. Enquanto eu mostro os exemplos que geram exibições adequadas de associação no Angular e Knockout, lembre-se de que eles com certeza não são a única opção.

Configuração de uma solução

Do ponto de vista da configuração, você precisa pelo menos de três projetos. Um para a API Web, outra para o aplicativo MVC e, por fim, um projeto comum para hospedar códigos em comum entre esses dois projetos. No exemplo usado neste artigo, o projeto comum hospedará os ViewModels para que eles possam ser usados tanto na Web quanto na API Web. O projeto inicial será estruturado conforme mostrado na Figura 3.

Estrutura de projeto inicial
Figura 3 Estrutura de projeto inicial

No mundo real, você só dará suporte a uma estrutura do lado do cliente. Como exemplo para este artigo, é mais simples termos dois projetos MVC, um para cada estrutura a ser demonstrada. Você poderá encontrar algo parecido se precisar dar suporte a um aplicativo Web, um aplicativo móvel personalizado, um aplicativo do SharePoint, um aplicativo de área de trabalho ou qualquer outro cenário que não seja fácil de renderizar a partir de um projeto de interface de usuário comum. De qualquer forma, somente a lógica inserida no projeto de interface do usuário terá que ser repetida quando você oferece suporta a vários front-ends.

Importando dados

Na prática, seus dados serão armazenados em um banco de dados, provavelmente usando um mapeador relacional de objetos (ORM) como o Entity Framework. Aqui, colocarei lado a lado problemas de persistência de dados para focar no front-end. Vou confiar nos controladores da API Web para retornar valores codificados nas ações Get e executarei esses exemplos em uma situação perfeita em que todos os retornos da chamada API tenham êxito. Adicionar o tratamento de erro mais indicado será deixado como um exercício para você, leitor corajoso.

Para este exemplo, usarei um único Modelo de Exibição, decorado com atributos do namespace System.ComponentModel.DataAnnotations, conforme mostrado na Figura 4.

Figura 4 TimeEntryViewModel simples

namespace TimeTracking.Common.ViewModels
{
  public class TimeEntryViewModel
  {
    [Key]
    public int Id { get; set; }
    [Required (ErrorMessage ="All time entries must always be entered")]
    [DateRangeValidator (ErrorMessage ="Date is outside of expected range",
      MinimalDateOffset =-5, MaximumDateOffset =0)]
    public DateTime StartTime { get; set; }
    [DateRangeValidator(ErrorMessage = "Date is outside of expected range",
      MinimalDateOffset = -5, MaximumDateOffset = 0)]
    public DateTime EndTime { get; set; }
    [StringLength (128, ErrorMessage =
      "Task name should be less than 128 characters")]
    [Required (ErrorMessage ="All time entries should be associated with a task")]
    public string Task { get; set; }
    public string Comment { get; set; }
  }
}

O atributo DateRangeValidator não vem com um namespace DataAnnotations. Ele não é um atributo de validação padrão, mas a Figura 5 mostra como pode ser fácil criar um novo controle de servidor de validação. Quando aplicado, ele se comporta tal como os controles de servidor de validação padrão.

Figura 5 Controle de servidor de validação personalizado

public class DateRangeValidator : ValidationAttribute
{
  public int MinimalDateOffset { get; set; }
  public int MaximumDateOffset { get; set; }
  protected override ValidationResult IsValid(object value,
    ValidationContext validationContext)
  {
    if (!(value is DateTime))
      return new ValidationResult("Inputted value is not a date");
    var date = (DateTime)value;
    if ((date >= DateTime.Today.AddDays(MinimalDateOffset)) &&
      date <=  DateTime.Today.AddDays(MaximumDateOffset))
      return ValidationResult.Success;
    return new ValidationResult(ErrorMessage);
  }
}

Sempre que o modelo for validado, todos os controles de servidor de validação serão executados, incluindo todos os personalizados. Os modos de exibição criados com o Razor podem facilmente incorporar essas validações do lado do cliente, e esses controles de servidor de validação são automaticamente avaliados no servidor pelo Associador de Modelos. Validar a entrada do usuário é fundamental para ter mais de um sistema seguro.

Controladores da API

Agota que eu tenho o Modelo de Exibição, estou pronto para gerar um controlador. Usarei um scaffolding integrado para apagar o controlador. Isso criará métodos para as ações baseadas em verbo padrão (Get, Post, Put, Delete). Para este artigo, não estou preocupado com os detalhes dessas ações. Estou interessado somente em verificar que tenho pontos de extremidade que serão chamados na estrutura do lado do cliente.

Criando uma exibição

Em seguida, vou focar minha atenção no Modo de Exibição que o scaffolding integrado produz no Modelo de Exibição.

No projeto TimeTracking.Web, adicionarei um novo Controlador, que darei o nome de TimeEntryController e o criarei como um Controlador Vazio. Neste Controlador, vou criar a ação Editar, adicionando este código:

public ActionResult Edit()
{
  return PartialView(new TimeEntryViewModel());
}

De dentro deste método, clicarei e selecionarei “Adicionar Exibição”. No pop-up, especificarei o que eu quero um modelo de Edição, selecionando o modelo TimeEntryViewModel.

Além de especificar o modelo, eu me certifico de especificar a criação de uma exibição parcial. Eu quero que a exibição gerada inclua somente a marcação definida nela. Este fragmento de HTML será injetado na página existente no cliente. Uma amostragem da marcação gerada pelo scaffolding é mostrada na Figura 6.

Figura 6 Marcação Razor para a opção Editar Modo de Exibição

@model TimeTracking.Common.ViewModels.TimeEntryViewModel
@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()
  <div class="form-horizontal">
    <h4>TimeEntryViewModel</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    @Html.HiddenFor(model => model.Id)
    <div class="form-group">
      @Html.LabelFor(model => model.StartTime, htmlAttributes:
        new { @class = "control-label col-md-2" })
      <div class="col-md-10">
        @Html.EditorFor(model => model.StartTime,
          new { htmlAttributes = new { @class = "form-control" } })
        @Html.ValidationMessageFor(model => model.StartTime, "",
          new { @class = "text-danger" })
      </div>
    </div>
    ...
  </div>
}
<div>
  @Html.ActionLink("Back to List", "Index")
</div>

Existem alguns elementos-chave a serem observados com este modo de exibição gerado, que se for gerado fora da caixa, cria uma interface do usuário responsiva baseada no Bootstrap. Estão incluídos:

  • O grupo de formulário é repetido para cada propriedade no Modelo de Exibição.
  • A propriedade da ID é ocultada porque é marcada com o atributo Key, que a identifica como uma chave que não deve ser modificada.
  • Cada campo de entrada tem espaços reservados de validação associados que podem ser usados se uma validação discreta for habilitada.
  • Todos os rótulos são adicionados usando o auxiliar LabelFor, que interpreta os metadados na propriedade para determinar o rótulo apropriado. O DisplayAttribute pode ser usado para dar um nome melhor, assim como para manipular a localização.
  • Cada controle de entrada é adicionado usando o auxiliar EditorFor, que interpreta os metadados na propriedade para determinar o editor apropriado.

Os metadados são avaliados no tempo de execução. Isso significa que você pode adicionar atributos ao seu modelo depois de gerar a exibição e esses atributos serão usados para determinar os controles de servidor de validação, rótulos e editores, conforme apropriado.

Como o scaffolding é uma geração única de código, não há problema em editar a marcação gerada, pois isso geralmente está previsto.

Associação ao Knockout

Para fazer a marcação gerada funcionar com o Knockout, preciso adicionar dois atributos à marcação gerada. Eu não mergulharei profundamente nos trabalhos internos do Knockout, exceto para observar que a associação usa um atributo data-bind. A declaração de associação especifica o tipo de associação e, em seguida, a propriedade a ser usada. Precisarei adicionar um atributo data-bind aos controles de entrada. Se olhar novamente para a marcação gerada, você verá como o atributo class foi adicionado. Seguindo o mesmo processo, posso modificar a função EditorFor, conforme mostrado no código aqui:

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class = "form-control",
         data_bind = "text: StartTime" } })

Usando a marcação gerada fora da caixa no scaffolding, esta é a única alteração necessária para adicionar a associação Knockout.

Associação ao Angular

A associação de dados com o Angular é parecida. Posso adicionar a ela um atributo ng-model ou data-ng-model. O atributo data-ng-model manterá a marcação HTML5 em conformidade, porém, o atributo ng-bind ainda é comumente usado. De qualquer forma, o valor do atributo é simplesmente o nome da propriedade a ser associada. Para dar suporte à associação a um controlador Angular, modificarei a função EditorFor, usando este código:

@Html.EditorFor(model => model.StartTime,
  new { htmlAttributes = new { @class     = "form-control",
                                  data_ng-model = "StartTime" } })

Existem ainda dois pequenos ajustes que entram em cena para definir o aplicativo e o controlador. Consulte o código de exemplo para ver essas alterações em contexto no exemplo de trabalho completo.

É possível adotar uma técnica semelhante para fazer a marcação funcionar com qualquer estrutura MVVM que você estiver usando.

Alterando o scaffolding

Como o scaffolding usa T4 para gerar a saída, eu posso alterar o que é gerado para evitar ter que editar todas as exibições geradas. Os modelos usados são armazenados na instalação do Visual Studio. No Visual Studio 2017, você os encontrará aqui:

C:\Program Files (x86)\Microsoft Visual Studio 14.0\
  Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates\MvcView

Você pode editar esses modelos diretamente, mas isso afetaria todos os projetos nos quais está trabalhando no seu computador, e ninguém mais no projeto teria acesso às edições feitas nos modelos. Em vez disso, você adicione os modelos T4 ao projeto e substitua a implementação padrão usando a cópia local.

Basta copiar os modelos desejados na pasta “Code Templates” na pasta raiz do seu projeto. Você encontrará modelos para C# e Visual Basic. A linguagem escolhida será identificada no nome do arquivo. Você só precisa dos modelos para uma linguagem. Ao invocar o scaffolding no Visual Studio, ele vai primeiro procurar por um modelo na pasta CodeTemplates. Se um exemplo não for encontrado aqui, o mecanismo do scaffolding vai, então, procurar na instalação do Visual Studio.

De modo geral, T4 é uma ferramenta avançada para gerar texto, não apenas código. Aprender sobre esta ferramenta é por si só um tema importante, mas não se preocupe se ainda não conhece bem o T4. Tratam-se de pequenos ajustes os modelos. Você não precisará se aprofundar nos mecanismos internos para entender como o T4 funciona, mas precisará baixar uma extensão para adicionar suporte para edição de modelos T4 no Visual Studio. O Tangible T4 Editor (bit.ly/2Flqiqt) e o Devart T4 Editor (bit.ly/2qTO4GU) oferecem versões de comunidade excelentes de seus Editores T4 para edição de modelos T4 que fornecem realce de sintaxe, o que facilita a separação do código que orienta o modelo do código sendo criado pelo modelo.

Ao abrir o arquivo Edit.cs.t4, você encontrará blocos de código para controlar o modelo e blocos de código que são a marcação. A maior parte do código orientando o modelo faz o processamento condicional para lidar com casos especiais como páginas parciais de suporte e tipos especiais de propriedades como chaves de referência, enumerações e boolianos. A Figura 7 mostra um exemplo do modelo no Visual Studio quando é usada uma extensão apropriada. O código orientando o modelo é ocultado nas seções recolhidas, o que facilita ver qual será o resultado.

O modelo T4 de edição do Visual Studio
Figura 7 O modelo T4 de edição do Visual Studio

Felizmente, você não tem que rastrear por essas condicionais. Em vez disso, procure marcações geradas que você deseja alterar. Neste caso, são importantes para mim seis instruções diferentes. Elas foram extraídas para sua referência na Figura 8.

Figura 8 Código original para geração de editores

@Html.DropDownList("<#= property.PropertyName #>", null,
  htmlAttributes: new { @class = "form-control" })
@Html.DropDownList("<#= property.PropertyName #>", String.Empty)
@Html.EditorFor(model => model.<#= property.PropertyName #>)
@Html.EnumDropDownListFor(model => model.<#= property.PropertyName #>,
  htmlAttributes: new { @class = "form-control" })
@Html.EditorFor(model => model.<#= property.PropertyName #>,
  new { htmlAttributes = new { @class = "form-control" } })
@Html.EditorFor(model => model.<#= property.PropertyName #>)

Ao fazer a atualizações para dar suporte à sia estrutura especifica do lado do cliente, todas as novas exibições geradas com o modelo darão suporte à sua estrutura automaticamente.

Validação do lado do cliente

Quando eu examinei a exibição que foi gerada, eu ignorei as chamadas da função ValidationMessageFor feitas para cada propriedade. Essas chamadas produzem espaços reservados para exibir qualquer mensagem de validação criada quando as validações do lado do cliente são avaliadas. Essas validações são baseadas nos atributos Validation adicionados ao modelo. Tudo o que é necessário para habilitar essas validações do clado do cliente é adicionar referências aos scripts de validação jquery:

@Scripts.Render("~/bundles/jqueryval")

O pacote jqueryval é definido na classe BundleConfig da pasta App_Start.

Se você tentar enviar o formulário sem inserir nenhum dado, os controladores de servidor de validação do campo obrigatório serão acionados para evitar o envio.

Se você preferir uma estratégia de validação diferente do lado do cliente (bit.ly/2CZmRqR), você pode facilmente modificar o modelo T4 para não dar o resultado das chamadas ValidationMessageFor. E se você não usar a abordagem de validação nativa, não precisará referencia o pacote jqueryval, pois ele não será mais necessário.

Modelos do Editor

Como eu especifiquei o controle de entrada chamando o auxiliar html EditorFor, não vou especificar explicitamente qual deveria ser o controle de entrada. Em vez disso, eu deixo para a estrutura MVC escolher o controle de entrada mais apropriado para a propriedade especificada, baseado no tipo de dados ou atributos como o UIHint. Eu também posso influenciar diretamente a seleção do editor criando explicitamente um EditorTemplate. Isso permite que eu controle, globalmente, como a entrada de tipos específicos será tratada.

O editor padrão para uma propriedade DateTime é um textbox. Não é o editor ideal, mas eu posso mudar isso.

Adicionarei uma exibição parcial chamada DateTime.cshtml à pasta \Views\Shared\EditorTemplates. A marcação adicionada a esse arquivo será usada como o editor para qualquer propriedade do tipo DateTime. Adiciono a marcação mostrada na Figura 9 à exibição parcial e adiciono o seguinte código à parte inferior do Layout.cshtml:

<script>
  $(document).ready(function () {
    $(".date").datetimepicker();
  });
</script>
Figure 9 Markup for a DateTime Editor
@model DateTime?
<div class="container">
  <div class="row">
    <div class='col-md-10'>
      <div class="form-group ">
        <div class='input-group date'>
         <span class="input-group-addon">
           <span class="glyphicon glyphicon-calendar"></span>
         </span>
         @Html.TextBox("", (Model.HasValue ?
           Model.Value.ToShortDateString() :
           string.Empty), new
           {
             @class = "form-control"
           })
        </div>
      </div>
    </div>
  </div>
</div>

Quando esses elementos de código são adicionados, eu fico com um editos de Data e Hora bem legal para me ajudar a editar uma propriedade DateTime. A Figura 10 mostra este novo editor em ação.

Um seletor de data ativado
Figura 10 Um seletor de data ativado

Conclusão

Como você viu, o Razor pode fazer muita coisa para simplificar e agilizar a criação de exibições em qualquer estrutura MVVM do lado do cliente que você desejar usar. Você tem liberdade para dar suporte a estilos e convenções de aplicativos, e recursos como scaffolding e EditorTemplates ajudam a garantir a consistência em todo o seu aplicativo. Eles também possibilitam o uso de suporte interno para validações baseadas em atributos adicionados ao seu modelo de exibição, tornando seu aplicativo mais seguro.

Observe novamente o MVC ASP.NET e você encontrará muitas áreas que ainda são úteis e relevantes, mesmo que o cenário dos aplicativos Web continue mudando.


Nick Harrison  é um consultor de software e vive em Columbia, S.C., com sua querida esposa Tracy e sua filha. Tem desenvolvido a todo o vapor usando .NET para criar soluções de negócios desde 2002. Entre em contato com ele no Twitter: @Neh123us, onde ele anuncia suas postagens no blog, trabalhos publicados e palestras.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Lide Winburn (Softdocks Inc.)
Lide Winburn é arquiteto de nuvem na Softdocs, Inc., onde está ajudando escolas a usar menos papel. Ele também é jogador de hóquei amador e um frequentador assíduo de cinema com sua família.


Discuta esse artigo no fórum do MSDN Magazine