(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:
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:
Agora utilizando a exibição com summary (notem os asteriscos também):
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.