(Este aqui é o terceiro post sobre as novidades do Preview 5 (P5) do ASP.Net MVC. Mais informações aqui e aqui.)

Calma, meus amigos… como tudo no ASP.Net Mvc, a idéia é a mesma, mas a implementação é mais “unplugged”, mais hardcore! Eu sei que todos estão esperando os validadores, já estamos no Preview 5 (!), então vamos lá.

O conceito aqui é de validação, mas não teremos controles validadores, em que você indica que quer validação de datas e ele faz, ou que um campo é obrigatório e o javascript já é gerado para você. A validação, pelo menos até agora, acontece toda no servidor, e o cliente só exibe os erros de validação e as mensagens. Mas acompanhem comigo a implementação. Vou comentado ao longo do post.

O conceito é o seguinte: você tem um campo “x” de algum objeto de negócio. O servidor valida este campo contra alguma regra física (tipo, nulabilidade, etc) e/ou de negócio (deve ser maior o campo “y”, só pode ser nulo se o campo “w” também for, etc). Se houver um problema o cliente recebe a página de volta, corretamente preenchida, e com indicação dos campos incorretos, com mensagens, inclusive. Essa parte ficou parecida ao Webforms, há o conceito do validador que gera o * ao lado do campo com problema, e do summary de validação, mas eles são diferentes. Vamos aos screenshots:

Entrada de dados:

Input simples

Campo com erro após o submit. Notem que os valores voltam conforme foram digitados, mesmo os incorretos. Neste caso, que é um cadastro de categorias, é obrigatório a categoria tenha descrição e nome:

Primeiro erro de validação sem summary

Agora utilizando a exibição com summary (notem os asteriscos também):

Segundo erro de validação com summary

Tudo isso é meio automático. Meio, não muito. Vejam o código do formulário de edição:

    1 <%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
    2 AutoEventWireup="true" CodeBehind="Edit.aspx.cs"
    3 Inherits="MvcApplication1Preview5.Views.Categories.Edit" %>
    4 <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server">
    5 <% using (Html.Form())
    6    { %>
    7    <% =Html.ValidationSummary() %>
    8    <table>
    9     <tr>
   10         <td>Category Name:</td>
   11         <td>
   12             <% =Html.TextBox("CategoryName") %>
   13             <%--<% =Html.ValidationMessage("CategoryName") %>--%>
   14             <% =Html.ValidationMessage("CategoryName","*") %>
   15         </td>
   16     </tr>
   17     <tr>
   18         <td>Description:</td>
   19         <td>
   20             <% =Html.TextBox("Description") %>
   21             <%--<% =Html.ValidationMessage("Description")%>--%>
   22             <% =Html.ValidationMessage("Description", "*")%>
   23         </td>
   24     </tr>
   25    </table>
   26 <% =Html.SubmitButton("Enviar","Enviar") %>
   27 <% } %>
   28 </asp:Content>

(Notem o novo método “Html.Form”, com overload sem parâmetros. Ele vai postar de volta para o mesmo controlador e mesma ação, algo desejável depois da possibilidade de postar para ações com o mesmo nome.)

É bastante simples. Esse é o código com o summary incluido. Continuando no mesmo caminho, a utilização dos validadores é feita com métodos de auxílio (helper methods), a partir da classe HtmlHelper. São dois os métodos: ValidationMessage, para as mensagens, e ValidationSummary, para o summary. O método ValidationMessage tem um overload em você passa o nome do campo com problema (está comentado no código acima), e tem outro que você passa o nome do campo e o texto a ser exibido. Se você passar este segundo parâmetro, como estou fazendo quando passo só um asterisco “*”, a mensagem de erro não é exibida, só o que você digitou neste segundo campo. E se você tiver uma chamada ao método ValidationSummary, é lá que os erros que você ocultou são exibidos. Não é obrigatório ter o summary e o message juntos, você pode ter só o ValidationMessage, ou só o ValidationSummary, mas os dois juntos ficam melhores.

Nesse momento você, assim como eu também pensei quando vi, deve estar pensando: e de onde vem esta informação de erros de validação? Não há no MVC nenhum lugar em que se armazena isso. Não havia. Agora há. Chama-se ModelState, e é uma classe nova no MVC. Lá você coloca erros o valor que foi tentado. Há também um dicionário (chamado ModelStateDictionary), onde você indica o campo como chave em string, e o ModelState deste campo como valor. Fica então fácil fazer coisas desse tipo:

modelState.AddModelError("CategoryName", cat.CategoryName,
"O valor de CategoryName é obrigatório.");

Essa classe é acessível a partir do ViewData. Há uma propriedade chamada ModelState (que na verdade é um ModelStateDictionary), que você pode manipular. E se está na View o Controller enxerga e pode manipular. E é a partir dele mesmo que rola toda a coordenação. Afinal, é trabalho do controlador dizer o que a view deve exibir.

Existe um motivo para a complexidade adicional que é colocar os erros de validação no modelo: É PORQUE É LÁ QUE ELES DEVEM FICAR. Eu sei que quando usamos webforms quem controla a exibição dos erros de negócio são os validadores diretamente, ou seja, a interface final com o usuário (ou UI, user interface). Pois é, isso traz altíssimo acomplamento, dependência da camada de negócio da camada de interface gráfica (o correto é o contrário), e uma separação de responsabilidades pobre. Funciona? Funciona. Mas, se você não validar no modelo de novo, pode ter problemas. E se validar, está indo contra o famoso princípio DRY (Don’t Repeat Yourself, ou, não se repita). E a manutenção passa a ficar um inferno, porque as regras de negócio estão espalhadas por toda a aplicação. É por isso que sempre digo que essa nova maneira de trabalhar enfatiza as boas práticas. ASP.Net MVC é o que há!

Discursos a parte, o que eu fiz para controlar os erros foi simples, foi mais para simplificar o exemplo. O ScottGu tem um exemplo um pouco diferente, mais complexo, com uso de interfaces, sugiro dar uma olhada depois. Continuo utilizando o Entity Framework – EF (funciona bem, é rápido de montar, e está à mão). Ele gerou para mim uma classe Category, e eu criei uma parcial da mesma. Adicionei métodos parciais de validação, que já vêm criados quando a classe é gerada pelo EF, no caso os métodos OnCategoryNameChanging, e OnDescriptionChanging. Os dois acontecem antes da classe de categoria ser atualizada. Minha regra é simples: descrição e nomes são campos obrigatórios. Se vierem em branco ou nulos é um erro, e eu lanço uma exceção. Assim:

    1 [ModelBinder(typeof(Binders.CategoryBinder))]
    2 public partial class Category
    3 {
    4     partial void OnCategoryNameChanging(string value)
    5     {
    6         if (string.IsNullOrEmpty(value))
    7         {
    8             LancaErro("O valor de CategoryName é obrigatório.");
    9         }
   10     }
   11     partial void OnDescriptionChanging(string value)
   12     {
   13         if (string.IsNullOrEmpty(value))
   14         {
   15             LancaErro("O valor de Description é obrigatório.");
   16         }
   17     }
   18
   19     private void LancaErro(string textoErro)
   20     {
   21         Erros.Add(textoErro);
   22         throw new ApplicationException(textoErro);
   23     }
   24
   25     public IList<string> Erros = new List<string>();
   26
   27 }

Eu não morri de amores com esse negócio de lançar exceção à toa, mas é o modelo recomendado. Isso tudo porque foi criado um novo método no ASP.Net MVC chamado TryUpdateModel, em que você passa a classe, ele já pega os erros, e cadastra no ModelStateDictionary, tudo sozinho, deixando a implemantação mais leve. Se der tudo certo ele retorna true. Se der algum erro ele retorna falso.(Há também o método UpdateModel, que retorna void, mas se não conseguir atualizar o modelo joga uma exceção.)

    1 [ActionName("Edit")]
    2 [AcceptVerbs("POST")]
    3 public ActionResult SaveEdit(int categoryID)
    4 {
    5     bool atualizou;
    6     Models.Category catFromDB;
    7     using (var db = new Models.NorthwindEntities())
    8     {
    9         catFromDB = (from cats in db.Categories
   10                         where cats.CategoryID == categoryID
   11                         select cats).First();
   12         atualizou = TryUpdateModel(catFromDB,
               new[] { "CategoryName", "Description" });
   13         if (atualizou)
   14             db.SaveChanges();
   15     }
   16
   17     if (atualizou)
   18         return RedirectToAction("Edit", new { CategoryID = categoryID });
   19     else
   20         return View(catFromDB);
   21 }

Viram a chamada do método na linha 12? Se não der erro eu atualizo o banco e volto para a view de edição, passando a própria categoria que recebi (que não foi alterada). Se der certo, eu redireciono para a edição de novo, para montar a tela de edição à partir de um GET, não de um POST.

Se estamos passando o objeto de categorias não modificado, como pode ser que o campo exibido para o usuário contém o valor digitado anteriormente, e não o valor do banco de dados? Isso é culpa do ModelState, que carrega a tentativa, lembram? Agora, todos os métodos do HtmlHelper estão passando a checar o ModelState. Se tiver um valor lá para um campo determinado ele é utilizado. Vejam o código retirado do método InputHelper, utilizado pelos outros métodos, como o Textbox(), para compor o html:

tagBuilder.MergeAttribute("value", attemptedValue ??
           ((useViewData) ? EvalString(name) : value));

Ou seja, se tem valor de tentativa, utilize.

E o fundo e bordas vermelhos? Mesma coisa, ModelState. Vejam outro trecho de código do mesmo método do HtmlHelper:

if (ViewData.ModelState.TryGetValue(name, out modelState)) {
    if (modelState.Errors.Count > 0) {
        tagBuilder.AddCssClass(ValidationInputCssClassName);
    }
}

Ou seja, se tiver um erro que seja, utilize uma classe de CSS. É isso que deixa o fundo e a borda vermelhos. Simples, não? A partir da inclusão do código do controlador, qualquer adição será trabalhada na View e na camada de negócios.

Nesse ponto fica devendo ainda algo que utilize Javascript, para facilitar a vida do usuário, sem postback. Aumenta a segurança, melhora a usabilidade, e diminui o uso da banda. Sinceramente, não sei se vamos ter esse presente, pode ser que não tenha. O foco em deixar tudo na camada de modelo, e ficar bem DRY pode impedí-los de caminhar nessa direção. Vamos ver.

Gostaria de ouvir a opinião de vocês. Gostaram do que viram? Dá muito trabalho? O ScottGu, no post que comentei, relembra que o webforms não vai morrer, e o MVC e o webforms vão continuar evoluindo em paralelo, e lembra: se você não quiser, não precisa usar o MVC. O que você acha? Vale a pena? Dá para abandonar o webforms?

O projeto que usei está disponível aqui.

Giovanni Bassi

Arquiteto e desenvolvedor, agilista, escalador, provocador. É fundador e CSA da Lambda3. Programa porque gosta. Acredita que pessoas autogerenciadas funcionam melhor e por acreditar que heterarquia é mais eficiente que hierarquia. Foi reconhecido Microsoft MVP há mais de dez anos, dos mais de vinte que atua no mercado. Já palestrou sobre .NET, Rust, microsserviços, JavaScript, TypeScript, Ruby, Node.js, Frontend e Backend, Agile, etc, no Brasil, e no exterior. Liderou grupos de usuários em assuntos como arquitetura de software, Docker, e .NET.