.NET

9 mai, 2016

ASP .NET MVC 5 – a misteriosa classe ModelState

Publicidade

Neste artigo vou desmistificar a misteriosa classe ModelState, mostrando o seu significado, recursos e como devemos tratá-la em aplicações ASP .NET MVC.

O que é o ModelState?

O ModelSate é uma propriedade do Controller e pode ser acessado a partir das classes que herdam de System.Web.Mvc.Controller. Ele é um dicionário disponível na classe base do controlador que armazena as informações adicionais e de estado sobre o modelo.

O ModeState representa uma coleção de pares de nome e valor que são submetidos ao servidor durante o POST. Ele também contém uma coleção de mensagens de erros para cada valor submetido.

Apesar de usar o nome ModelState, ele não conhece nada sobre qualquer classe de modelo. Ele apenas contém nomes, valores e erros.

Então, para que serve o ModelState? Ele tem dois propósitos:

  1. Armazenar o valor submetido ao servidor;
  2. Armazenar os erros de validação associados com esses valores.

Assim, quando um post acontece, você pode realizar suas verificações; e se algo der errado, você pode adicionar um item ao dicionário ModelState e esta informação estará disponível para ser utilizada pela View para exibir um resumo das incoerências.

O ModelStateDictionary tem vários métodos para adicionar entradas:

void Add (KeyValuePair <string, ModelState> item);
void Add (string chave, valor ModelState);
AddModelError void (string chave, exceção Exception);
AddModelError void (chave string, string errorMessage);

Simples assim.

Vamos, então, mostrar isso na prática.

Recursos usados: Visual Studio 2013 Express for Web

Desmistificando o ModelState

Abra o VS 2013 Express for web e clique em New Project. A seguir, selecione Visual C# -> ASP .NET Web Application e informe o nome Mvc_ModelState. Clique no botão OK.

mvc_mdlst11

A seguir, selecione o template Empty, marque MVC  e clique no botão OK.

mvc_mdlst12

Será criado um projeto contendo toda a estrutura de pastas criadas pelo framework ASP .NET MVC.

Definindo o Model

Vamos definir um view model na pasta Models para representar um usuário. Não sabe que é um View Model? Então, dê uma olhada nos modelos de domínio do seu projeto, e veja se há códigos que são utilizados exclusivamente pelas Views, não tendo nenhuma relação com o domínio do negócio em questão.

Se isto estiver ocorrendo, então, seu modelo de domínio está assumindo muitas responsabilidades. O padrão View Model veio justamente para resolver isso.

O ViewModel deve conter a lógica da interface do usuário e permitir modelar entidades a partir de um ou mais modelos em um único objeto, representando um conjunto de um ou mais Models e outros dados que serão representados em uma View.

Assim, um View Model tem as seguintes características:

  • Contém toda lógica de interface e a referência ao modelo e assim atua como modelo para a View;
  • Separar as responsabilidades do Model usando Informações que somente serão exibidas nas Views;
  • A View direciona a construção da ViewModel;
  • O ViewModel contém somente dados e comportamento relacionados com a view;
  • Cada ViewModel possui uma View Tipada.

Clique com o botão direito do mouse na pasta Models e a seguir Add Class. Depois informe o nome UsuarioViewModel e defina o código abaixo para esta classe:

public class UsuarioViewModel
{
        public string Nome { get; set; }
        public string Sobrenome { get; set; }
        public string Email { get; set; }
}

Definindo o Controller

Vamos agora definir o controlador do nosso projeto: clique com o botão do mouse sobre a pasta Controllers e, a seguir, clique em Add -> Controller. Selecione o Scaffold – MVC 5 Controller Empty – e clique em Add. Depois informe o nome HomeController e, a seguir, defina dois métodos Actions (o get e o post) neste controlador para incluir um usuário usando o nomeIncluirUsuario():

using System.Web.Mvc;
using Mvc_ModelState.Models;
namespace Mvc_ModelState.Controllers
{
    public class HomeController : Controller
    {
        [HttpGet]
        public ActionResult IncluirUsuario()
        {
            UsuarioViewModel model = new UsuarioViewModel();
            return View(model);
        }
        [HttpPost]
        public ActionResult IncluirUsuario(UsuarioViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }
            return RedirectToAction("Index");
        }
        public ActionResult Index()
        {
            return View();
        }     
    }
}

O código referencia o namespace  Mvc_ModelState.Models para ter acesso ao modelo definido na pasta Models.

Temos o método IncluirUsuario(), que apresenta o formulário ao usuário com base no modelo UsuarioViewModel e, a seguir, trata o POST deste formulário no mesmo método onde recebe os dados enviados da view para o controlador via ModelBinding.

O método Action Index() irá definir uma view Index bem simples.

Criando as Views

Vamos criar a view Index(). Para isso, clique com o botão direito do mouse sobre o método Index() e a seguir em Add View. Depois, defina o template como Empty(without model) e clique no botão Add.

Informe apenas um texto para identificar a view, pois talvez ela nem será usada.

Vamos agora criar a view IncluirUsuario: clique com o botão direito do mouse sobre o método IncluirUsuario() e, a seguir, em Add View. Defina o template como Empty e o Model Class igual a UsuarioViewModel e clique no botão Add.

mvc_mdlst13

A seguir, vamos definir código da view onde temos um formulário para o usuário informar o nome, o sobrenome e o e-mail:

@model Mvc_ModelState.Models.UsuarioViewModel
<h2>Incluir Usuário</h2>
@using (Html.BeginForm())
{
    <div>
        <div>
           Nome : @Html.TextBoxFor(x => x.Nome)
        </div>
        <div>
           Sobrenome: @Html.TextBoxFor(x => x.Sobrenome)
        </div>
        <div>
          Email:  @Html.TextBoxFor(x => x.Email)
        </div>
        <div>
            <input type="submit" value="Salvar Usuário" />
        </div>
    </div>
}

Executando o projeto e preenchendo o formulário, ao clicar no botão – Salvar Usuário – veremos que todos os valores entrados serão exibidos na instância de UsuarioViewModel (model) no controlador.

Nota: Marque um breakpoint no método Action IncluirUsuario(Post) e verifique os valores de model.

Como esses valores foram parar aí?

mvc_mdlst14

Vamos dar uma espiada no código HTML da view IncluirUsuario.cshtml que foi renderizada:

mvc_mdlst15

No momento do POST, todos os valores nas tags <input> são submetidos para o servidor como pares chave-valor.

Quando o MVC recebe o POST, ele recebe todos os parâmetros do POST e os inclui em uma instância ModelStateDictionary.

Quando debugamos o método Action POST do Controlador, podemos usar a janela Local para investigar os valores neste dicionário:

mvc_mdlst16

Espiando o ModelState no Debug, vemos o seguinte:

mvc_mdlst17

Cada uma das propriedade tem uma instância de ValueProviderResult que contém os valores atuais submetidos ao servidor.

O MVC cria todas essas instâncias automaticamente quando submetemos os dados, e o método Action POST tem os inputs que mapeia os valores submetidos.

Essencialmente, o MVC está encapsulando os inputs do usuário em classes no servidor (ModelState).

E os erros, onde estão?

Vamos aplicar atributos Data Annotations em nosso UsuarioViewModel para realizar a validação e espiar os erros. Altere a classe UsuarioViewModel incluindo os atributos abaixo no código da classe:

using System.ComponentModel.DataAnnotations;
namespace Mvc_ModelState.Models
{
    public class UsuarioViewModel
    {
        [Required(ErrorMessage = "Informe o nome do usuário.")]
        [StringLength(20, ErrorMessage = "O nome deve ser menor que {1} caracteres.")]
        [Display(Name = "Nome do Usuário:")]
        public string Nome { get; set; }
        [Required(ErrorMessage = "Informe o sobrenome do usuário.")]
        [StringLength(20, ErrorMessage = "O sobrenome não pode ter mais que {1} caracteres.")]
        [Display(Name = "Sobrenome do Usuário:")]
        public string Sobrenome { get; set; }
        [EmailAddress(ErrorMessage = "Email inválido")]
        [Required(ErrorMessage = "Informe o endereço de Email.")]
        [Display(Name = "Endereço de Email:")]
        public string Email { get; set; }
    }
}

Feito isso, vamos alterar a nossa view IncluirUsuario.cshtml incluindo o ValidationSummary() e o ValidationMessageFor() para exibir mensagens de erros se eles ocorrerem.

  • ValidationSummary() – realiza a validação do modelo e lê os erros do estado do modelo e exibe em uma lista;
  • ValidationMessageFor() – Exibe os erros para a propriedade específica.

Veja como deve ficar o código da view:

@model Mvc_ModelState.Models.UsuarioViewModel
<h2>Incluir Usuário</h2>
@using (Html.BeginForm())
{
    <div>
        @Html.ValidationSummary()
        <div>
            Nome : @Html.TextBoxFor(x => x.Nome)
            @Html.ValidationMessageFor(x => x.Nome)
        </div>
        <div>
            Sobrenome: @Html.TextBoxFor(x => x.Sobrenome)
            @Html.ValidationMessageFor(x => x.Sobrenome)
        </div>
        <div>
            Email:  @Html.TextBoxFor(x => x.Email)
            @Html.ValidationMessageFor(x => x.Email)
        </div>
        <div>
            <input type="submit" value="Salvar Usuário" />
        </div>
    </div>
}

Agora, vamos executar novamente e simular um erro (não informar o nome do usuário) e fazer o Debug espiando o ModelState:

mvc_mdlst18

Conforme era esperado, a instância de ModelState para o nome agora possui um erro na coleção Errors.

Quando o MVC cria o model state para as propriedade submetidas, ele vai até cada propriedade no ViewModel e valida a propriedade usando os atributos associados a ela.

Se houver algum erro, ele é adicionado na coleção Errors na propriedade do ModelState. Note também que IsValid agora é false, indicando que existe um erro.

Dessa forma, definindo a validação como fizemos, permitimos ao MVC trabalhar da forma esperada: o ModelState armazena os valores submetidos e permite que eles sejam mapeados para propriedades de classe, mantendo uma coleção de erros para cada propriedade.

É tudo que precisamos e tudo isso ocorre de forma transparente e não precisa de nenhuma configuração extra.

Pegue o projeto completo aqui: Mvc_ModelState.zip