Skip to content

4. O modelo de uma ação

Voltemos à arquitetura de uma aplicação ASP.NET MVC:

No capítulo anterior, analisámos o processo que encaminha o pedido [1] para o controlador e a ação [2a] que o irá tratar, um mecanismo conhecido como encaminhamento. Apresentámos também as várias respostas que uma ação pode enviar de volta ao navegador. Até agora, apresentámos ações que não processaram a solicitação que lhes foi apresentada. Uma solicitação [1] transporta várias informações que o ASP.NET MVC apresenta [2a] à ação na forma de um modelo. Este termo não deve ser confundido com o modelo M de uma vista V [2c] que é produzida pela ação:

  • a solicitação HTTP do cliente chega a [1];
  • em [2], a informação contida na solicitação é transformada num modelo de ação [3], frequentemente, mas não necessariamente, uma classe, que serve como entrada para a ação [4];
  • em [4], a ação, com base neste modelo, gera uma resposta. Esta resposta tem dois componentes: uma vista V [6] e o modelo M desta vista [5];
  • a vista V [6] utilizará o seu modelo M [5] para gerar a resposta HTTP destinada ao cliente.

No modelo MVC, a ação [4] faz parte do C (controlador), o modelo de vista [5] é o M e a vista [6] é o V.

Este capítulo examina os mecanismos para ligar a informação transportada pela solicitação — que é inerentemente composta por cadeias de caracteres — ao modelo de ação, que pode ser uma classe com propriedades de vários tipos.

4.1. Inicialização dos parâmetros da ação

Adicionamos [1] um novo projeto básico ASP.NET MVC à solução existente:

  • em [2], o nome do novo projeto;
  • em [3, 4], selecionamos um projeto ASP.NET MVC básico;
  • em [5], o novo projeto.

Vamos definir o novo projeto como o projeto inicial da solução.

Tal como feito na Secção 3.1, criamos um controlador denominado [First] [1]:

Image

Neste controlador, criamos a seguinte ação [Action01]:


using System.Web.Mvc;
 
namespace Exemple_02.Controllers
{
  public class FirstController : Controller
  {
    // Action01
    public ContentResult Action01(string nom)
    {
      return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
    }
 
  }
}

A nova funcionalidade encontra-se na linha 8: o método [Action01] tem um parâmetro. Neste capítulo, exploraremos as diferentes formas de inicializar os parâmetros de uma ação. O parâmetro [name] acima é inicializado por ordem com os seguintes valores:

Request.Form["name"]
um parâmetro chamado [name] enviado por uma solicitação POST
RouteData.Values["name"]
um elemento de URL chamado [name]
Request.QueryString["name"]
um parâmetro chamado [name] enviado por uma solicitação GET
Request.Files["name"]
um ficheiro carregado chamado [name]

Vamos examinar estes diferentes casos. Vamos introduzir a URL [/First/Action01?name=someone] diretamente no navegador. Obtemos a seguinte resposta:

Image

A solicitação HTTP do navegador foi a seguinte:

1
2
3
GET /First/Action01?nom=someone HTTP/1.1
Host: localhost:55483
...
  • Linha 1: O pedido é um GET. O URL solicitado inclui o parâmetro [name]. No lado do servidor, o pedido é encaminhado para a ação [Action01], que tem a seguinte assinatura:

public ContentResult Action01(string nom)

Para atribuir um valor ao parâmetro name, o ASP.NET MVC verifica os seguintes valores por ordem: *Request.Form[&quot;name&quot;], RouteData.Values[&quot;name&quot;],* *<span style="color: #2323dc">Request.QueryString[&quot;name&quot;]</span>**, Request.Files[&quot;name&quot;]*. Ele pára assim que encontra um valor. O parâmetro [name] incorporado na URL GET foi colocado pela estrutura em Request.QueryString["name"]. É com este valor [someone] que o parâmetro [name] de [Action01] será inicializado. Em seguida, o código de [Action01] é executado:


return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom), "text/plain", Encoding.UTF8);

Este código fornece a resposta enviada ao cliente:

Image

Nota: O mecanismo de ligação de parâmetros não distingue maiúsculas de minúsculas. Assim, se a nossa ação estiver definida da seguinte forma:


public ContentResult Action01(string NOM)

e o parâmetro passado for [?Name=zébulon], a ligação ocorrerá na mesma. O parâmetro [Name] de [Action01] receberá o valor [zébulon].

Agora, vamos solicitar a mesma URL com um POST. Para isso, vamos utilizar a aplicação [Advanced Rest Client]:

  • em [1], a URL solicitada;
  • em [2], será utilizado o método POST;
  • em [3], os parâmetros POST.

Vamos enviar esta solicitação e analisar os registos HTTP. A solicitação HTTP é a seguinte:

  • em [1], o POST;
  • em [2], os parâmetros POST. Tecnicamente, foram enviados após os cabeçalhos HTTP, a seguir à linha em branco que indica o fim desses cabeçalhos;
  • em [3], a resposta recebida. Conseguimos recuperar com sucesso o parâmetro [name] do POST. Entre os valores testados para o parâmetro name Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — o primeiro funcionou.

Agora, vamos modificar a rota padrão em [App_Start/RouteConfig]. Atualmente, esta rota é a seguinte:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{id}",
          defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

Vamos alterá-lo para:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{nom}",
          defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);

  • Na linha 3, nomeámos o terceiro elemento de uma rota [name];
  • na linha 4, este elemento é declarado como opcional.

Agora, vamos recompilar a aplicação e solicitar a URL [/First/Action01/zébulon] diretamente no navegador. Obtemos a seguinte resposta:

Image

Entre os valores testados para o parâmetro «name» — Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — o segundo funcionou.

Vamos fazer a mesma solicitação utilizando uma solicitação POST e o [Advanced Rest Client]:

  • Em [1], atribuímos um valor ao elemento {name} da rota;
  • em [2], adicionamos um parâmetro [name] à solicitação enviada;
  • a resposta obtida está em [3].

Dos valores testados para o parâmetro [name] Request.Form["name"], RouteData.Values["name"], Request.QueryString["name"], Request.Files["name"] — dois eram válidos: os dois primeiros. Foi utilizado o primeiro.

4.2. Validação de parâmetros de ação

Se uma ação tiver um parâmetro denominado [p], o ASP.NET MVC tentará atribuir-lhe um dos seguintes valores: Request.Form["p"], RouteData.Values["p"], Request.QueryString["p"] ou Request.Files["p"]. Os três primeiros valores são cadeias de caracteres. Se o parâmetro [p] não for do tipo [string], poderão surgir problemas.

Vamos criar a seguinte nova ação:


    // Action02
    public ContentResult Action02(int age)
    {
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}", RouteData.Values["controller"], RouteData.Values["action"],age);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: A ação [Action02] aceita um parâmetro chamado [age] do tipo int. A string recuperada deve ser convertível para int.

Vamos solicitar a URL [http://localhost:55483/First/Action02?age=21]. Obtemos a seguinte página:

Image

Vamos solicitar a URL [http://localhost:55483/First/Action02?age=21x]. Obtemos a seguinte página:

Image

Desta vez, recebemos uma página de erro. É interessante analisar os cabeçalhos HTTP enviados pelo servidor neste caso:

1
2
3
4
5
HTTP/1.1 500 Internal Server Error
...
Content-Type: text/html; charset=utf-8
...
Content-Length: 12438

  • Linha 1: O servidor respondeu com um código [500 Internal Server Error] e enviou uma página HTML (linha 3) de 12 438 bytes (linha 5) para explicar as possíveis razões para este erro.

Agora vamos criar a seguinte ação [Action03]:


    // Action03
    public ContentResult Action03(int? age)
    {
      ...
}

[Action03] é idêntica a [Action02], exceto que alterámos o tipo do parâmetro [age] para int?, o que significa inteiro ou nulo.

Vamos solicitar a URL [http://localhost:55483/First/Action03?age=21x]. Obtemos a seguinte página:

Image

O ASP.NET MVC não conseguiu converter [21x] para um tipo int. Por isso, atribuiu o valor null ao parâmetro [age], conforme permitido pelo seu tipo int?. No entanto, é possível determinar se o parâmetro recebeu um valor da solicitação ou não.

Criamos a seguinte nova ação [Action04]:


    // Action04
    public ContentResult Action04(int? age)
    {
      bool valide = ModelState.IsValid;
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}", RouteData.Values["controller"], RouteData.Values["action"], age, valide);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: Mantivemos o tipo [int?]. Isto permite especificamente que a solicitação omita o parâmetro [age], que recebe então o valor nulo;
  • linha 4: verificamos se o modelo de ação é válido. O modelo de ação consiste em todos os seus parâmetros, neste caso [age]. O modelo é válido se todos os parâmetros conseguiram obter um valor da solicitação ou o valor nulo, caso o tipo do parâmetro o permita;
  • Linha 5: Adicionamos o valor da variável [valid] ao texto enviado ao cliente.

Vamos solicitar a URL [http://localhost:55483/First/Action04?age=21x]. Obtemos a seguinte página:

Image

O ASP.NET MVC não conseguiu converter [21x] para o tipo int. Por isso, atribuiu o valor nulo ao parâmetro [age], conforme permitido pelo seu tipo int?. No entanto, ocorreram erros de conversão, como demonstrado pelo valor de [valid].

É possível obter a mensagem de erro associada a uma conversão falhada. Vamos examinar a seguinte nova ação:


    // Action05
    public ContentResult Action05(int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, âge={2}, valide={3}, erreurs={4}", RouteData.Values["controller"], RouteData.Values["action"], age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

A nova funcionalidade encontra-se na linha 4. Aqui, chamamos um método privado [getErrorMessagesFor] e passamos-lhe o estado do modelo da ação. Este método devolve uma string que contém todas as mensagens de erro que ocorreram. Este método é o seguinte:


private string getErrorMessagesFor(ModelStateDictionary état)
    {
      List<String> erreurs = new List<String>();
      string messages = string.Empty;
      if (!état.IsValid)
      {
        foreach (ModelState modelState in état.Values)
        {
          foreach (ModelError error in modelState.Errors)
          {
            erreurs.Add(getErrorMessageFor(error));
          }
        }
        foreach (string message in erreurs)
        {
          messages += string.Format("[{0}]", message);
        }
      }
      return messages;
    }

  • linha 1: o parâmetro [ModelState] efetivamente passado para o método é do tipo [ModelStateDictionary];
  • linha 3: uma lista de mensagens de erro, inicialmente vazia;
  • linha 5: verificamos se o estado passado como parâmetro é válido ou não. Se não for, agregaremos todas as mensagens de erro numa única cadeia de caracteres;
  • linha 7: o tipo [ModelStateDictionary] tem uma propriedade [Values], que é uma coleção de tipos [ModelState]. Existe um [ModelState] por elemento do modelo. Por exemplo:

    • ModelState["age"]: o estado do modelo da ação para o parâmetro [age],
    • ModelState["age"].Errors: a coleção de erros para este parâmetro. Os erros são do tipo [ModelError],
    • ModelState["age"].Errors[i].ErrorMessage: a mensagem de erro (se houver) para o parâmetro [age] do modelo
    • ModelState["age"].Errors[i].Exception: a exceção para o erro #i na coleção de erros do parâmetro [age],
    • ModelState["age"].Errors[i].Exception.InnerException: a causa desta exceção,
    • ModelState["age"].Errors[i].Exception.InnerException.Message: a mensagem da causa da exceção;
  • linha 9: percorremos a coleção [Errors] de um [ModelState] específico;
  • linha 11: recuperamos a mensagem de erro de um [ModelError] específico e adicionamo-la à lista de mensagens de erro da linha 3;
  • linhas 14–17: os elementos da lista de mensagens de erro são concatenados numa única cadeia de caracteres.

O método [getErrorMessageFor] na linha 11 é o seguinte:


    private string getErrorMessageFor(ModelError error)
    {
      if (error.ErrorMessage != null && error.ErrorMessage.Trim() != string.Empty)
      {
        return error.ErrorMessage;
      }
      if (error.Exception != null && error.Exception.InnerException == null && error.Exception.Message != string.Empty)
      {
        return error.Exception.Message;
      }
      if (error.Exception != null && error.Exception.InnerException != null && error.Exception.InnerException.Message != string.Empty)
      {
        return error.Exception.InnerException.Message;
      }
      return string.Empty;
}

  • Linha 1: Recebemos um tipo [ModelError] que encapsula um erro num dos elementos do modelo da ação. Recuperamos a mensagem de erro de três locais diferentes:

    • em [ModelError].ErrorMessage, linhas 3–6;
    • em [ModelError].Exception.Message, linhas 7–10;
    • em [ModelError].Exception.InnerException.Message, linhas 11–14;

Durante os testes, verificamos que a mensagem de erro se encontra nestes três locais, dependendo da natureza do elemento do modelo. Deve existir uma regra que garanta que possamos obter a mensagem de erro associada a um elemento do modelo, mas eu desconheço-a. Por isso, procuro-a nos vários locais onde a posso encontrar, seguindo uma ordem específica. Assim que for encontrada uma mensagem não vazia, esta é devolvida.

Vamos aceder ao URL [http://localhost:55483/First/Action05?age=21x]. Aparece a seguinte página:

Image

4.3. Uma ação com vários parâmetros

Considere a seguinte nova ação:


    // Action06
    public ContentResult Action06(double? poids, int? age)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], poids, age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: Temos dois parâmetros [peso] e [idade].

As regras descritas acima aplicam-se agora a ambos os parâmetros. Aqui estão alguns exemplos de execução:

Image

Image

4.4. Usar uma classe como modelo de ação

Vamos definir uma classe que servirá de modelo para uma ação. Vamos colocá-la na pasta [Models] [1].

O seu código será o seguinte:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

A nossa classe tem como propriedades automáticas os dois parâmetros [Peso] e [Idade] discutidos anteriormente. Esta classe será o parâmetro de entrada para a ação [Ação07]:


    // Action07
    public ContentResult Action07(ActionModel01 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: O modelo de ação é uma instância do tipo [ActionModel01].

Vamos rever os mesmos dois exemplos de antes:

Image

Image

Note que a ligação de parâmetros não distingue maiúsculas de minúsculas. Os parâmetros da solicitação eram [age] e [weight]. Eles preencheram as propriedades [Age] e [Weight] da classe [ModelAction01].

Além disso, até agora utilizámos pedidos HTTP [GET]. Vamos demonstrar que os pedidos [POST] se comportam da mesma forma. Para tal, vamos utilizar novamente a aplicação [Advanced Rest Client]:

  • em [1], o URL solicitado;
  • em [2], será enviada através de uma solicitação POST;
  • em [3], os parâmetros POST.

Recebemos a mesma resposta que com a solicitação GET:

Image

4.5. Modelo de ação com restrições de validação - 1

Utilizando o modelo anterior:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double? Poids { get; set; }
    public int? Age { get; set; }
  }
}

Os parâmetros [weight] e [age] podem ser omitidos da solicitação. Neste caso, as propriedades [Weight] e [Age] são definidas como [null] e nenhum erro é relatado. Pode querer transformar o modelo da seguinte forma:


namespace Exemple_02.Models
{
  public class ActionModel01
  {
    public double Poids { get; set; }
    public int Age { get; set; }
  }
}

Linhas 5 e 6: as propriedades [Weight] e [Age] já não podem ter o valor [null]. Vamos ver o que acontece com este novo modelo quando os parâmetros [weight] e [age] não constam na solicitação.

Image

Não ocorreram erros, e as propriedades [Weight] e [Age] mantiveram o seu valor de inicialização: 0. ASP.NET MVC:

  • criou uma instância do modelo utilizando `new ActionModel01`. Foi aqui que as propriedades [Weight] e [Age] receberam o valor 0;
  • não atribuiu quaisquer valores a estas duas propriedades porque não existiam parâmetros com esses nomes.

O primeiro modelo permite-nos verificar a ausência de um parâmetro: a propriedade correspondente tem então o valor [null]. O segundo não permite isso. É possível adicionar restrições de validação para além do tipo simples dos parâmetros. Vamos agora apresentá-las.

Considere o seguinte novo modelo de ação:

Image


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double? Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int? Age { get; set; }
  }
}

  • linha 6: indica que o campo [Weight] é obrigatório;
  • linha 7: indica que o campo [Peso] deve estar no intervalo [1,200];
  • linha 9: indica que o campo [Idade] é obrigatório;
  • linha 7: indica que o campo [Idade] deve estar no intervalo [1,150];

A ação que utiliza este modelo será a seguinte [Ação08]:


    // Action08
    public ContentResult Action08(ActionModel02 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, valide={4}, erreurs={5}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: A ação recebe uma instância do modelo [ActionModel02];

Vamos executar alguns testes:

Image

Image

Image

Image

Os erros são detetados corretamente. Agora, vamos atualizar o modelo da seguinte forma:


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel02
  {
    [Required]
    [Range(1, 200)]
    public double Poids { get; set; }
    [Required]
    [Range(1, 150)]
    public int Age { get; set; }
  }
}

Linhas 8 e 11: as propriedades já não podem ter o valor [null]. Vamos compilar e executar o teste novamente sem parâmetros:

Image

A ausência de parâmetros fez com que as propriedades [Weight] e [Age] mantivessem o valor que adquiriram quando o modelo foi instanciado: 0. A validação ocorre então. O atributo [Required] é então satisfeito. Podemos ver que a mensagem de erro acima se refere ao atributo [Range]. Portanto, para verificar a presença de um parâmetro, a propriedade associada deve ser nula, ou seja, deve poder aceitar o valor nulo.

Voltemos ao modelo inicial [ActionModel02] e consideremos uma ação cujo modelo consiste numa instância [ActionModel02] e num tipo [DateTime] nulo:


    // Action09
    public ContentResult Action09(ActionModel02 modèle, DateTime? date)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("Contrôleur={0}, Action={1}, poids={2}, âge={3}, date={4}, valide={5}, erreurs={6}", RouteData.Values["controller"], RouteData.Values["action"], modèle.Poids, modèle.Age, date, ModelState.IsValid, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Vamos realizar alguns testes:

Image

Não passámos quaisquer parâmetros para a ação. Os atributos [Required] nas propriedades [Weight] e [Age] cumpriram a sua função. A data, no entanto, recebeu o valor nulo e não foram reportados erros.

Agora, vamos passar parâmetros inválidos:

Image

Agora vamos passar valores válidos:

Image

Vamos examinar outras restrições de validação. O novo modelo de ação é o seguinte:

Image


using System.ComponentModel.DataAnnotations;
namespace Exemple_02.Models
{
  public class ActionModel03
  {
    [Required(ErrorMessage = "Le paramètre email est requis")]
    [EmailAddress(ErrorMessage = "Le paramètre email n'a pas un format valide")]
    public string Email { get; set; }
 
    [Required(ErrorMessage = "Le paramètre jour est requis")]
    [RegularExpression(@"^\d{1,2}$", ErrorMessage = "Le paramètre jour doit avoir 1 ou 2 chiffres")]
    public string Jour { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    [MaxLength(4, ErrorMessage = "Le paramètre info1 ne peut avoir plus de 4 caractères")]
    public string Info1 { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [MinLength(2, ErrorMessage = "Le paramètre info2 ne peut avoir moins de 2 caractères")]
    public string Info2 { get; set; }
 
    [Required(ErrorMessage = "Le paramètre info3 est requis")]
    [MinLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    [MaxLength(4, ErrorMessage = "Le paramètre info3 doit avoir 4 caractères exactement")]
    public string Info3 { get; set; }
  }
}

  • linha 6: o atributo [Required], desta vez com uma mensagem de erro definida por nós;
  • linha 7: o atributo [EMailAddress] exige que o campo [Email] contenha um endereço de e-mail válido;
  • linha 11: o atributo [RegularExpression] exige que o campo [Day] contenha uma sequência de um ou dois dígitos. O primeiro parâmetro é a expressão regular contra a qual o campo deve ser validado;
  • linha 15: o atributo [MaxLength] exige que o campo [Info1] tenha no máximo 4 caracteres;
  • linha 19: o atributo [MinLength] exige que o campo [Info2] tenha pelo menos 2 caracteres;
  • linhas 23-24: os atributos [MaxLength] e [MinLength] combinados exigem que o campo [Info3] tenha exatamente 4 caracteres;

A ação [Action10] utilizará este modelo:


// Action10
    public ContentResult Action10(ActionModel03 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("email={0}, jour={1}, info1={2}, info2={3}, info3={4}, erreurs={5}",
        modèle.Email, modèle.Jour, modèle.Info1, modèle.Info2, modèle.Info3, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

Vamos realizar alguns testes com esta ação.

Primeiro, sem parâmetros:

Image

Depois, com parâmetros inválidos:

Image

Depois, com parâmetros válidos:

Image

4.6. Modelo de ação com restrições de validade - 2

Introduzimos restrições de integridade adicionais. O novo modelo de ação será a seguinte classe [ActionModel04]:


using System.ComponentModel.DataAnnotations;
 
namespace Exemple_02.Models
{
  public class ActionModel04
  {
    [Required(ErrorMessage="Le paramètre url est requis")]
    [Url(ErrorMessage="URL invalide")]
    public string Url { get; set; }
    [Required(ErrorMessage = "Le paramètre info1 est requis")]
    public string Info1 { get; set; }
    [Required(ErrorMessage = "Le paramètre info2 est requis")]
    [Compare("Info1",ErrorMessage="Les paramètres info1 et info2 doivent être identiques")]
    public string Info2 { get; set; }
    [Required(ErrorMessage = "Le paramètre cc est requis")]
    [CreditCard(ErrorMessage = "Le paramètre cc n'est pas un n° de carte de crédit valide")]
    public string Cc { get; set; }
  }
}
  • linha 8: exige que o campo anotado seja um URL válido;
  • linha 13: exige que as propriedades [Info1] e [Info2] tenham o mesmo valor;
  • linha 16: exige que o campo anotado seja um número de cartão de crédito válido.

A ação que utiliza este modelo será a seguinte:


    // Action11
    public ContentResult Action11(ActionModel04 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("URL={0}, Info1={1}, Info2={2}, CC={3},erreurs={4}",
        modèle.Url, modèle.Info1, modèle.Info2, modèle.Cc, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Para testar a ação [Action11], utilizamos a aplicação [Advanced Rest Client]:

  • em [1], a URL da ação [Action11];
  • em [2], esta URL será solicitada com um POST;
  • em [3], selecione o separador [Form];
  • em [4], os valores dos quatro parâmetros esperados. Esta inicialização é uma funcionalidade fornecida pelo [ARC]. Os parâmetros efetivamente enviados podem ser visualizados no separador [Raw] [5];
  • em [6], os parâmetros POST.

Para este pedido, recebemos a seguinte resposta:

Image

Vamos passar parâmetros inválidos:

Image

Recebemos então a seguinte resposta:

Image

4.7. Modelo de ação com restrições de validade - 3

Por vezes, as restrições de integridade disponíveis não são suficientes. Nesse caso, pode criar as suas próprias. Em particular, pode utilizar um modelo que implemente a interface [IValidatableObject]. Neste caso, adiciona as suas próprias validações de modelo ao método [Validate] desta interface. Vejamos um exemplo. O novo modelo de ação será a seguinte classe [ActionModel05]:


using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace Exemple_02.Models
{
  public class ActionModel05 : IValidatableObject
  {
    [Required(ErrorMessage = "Le paramètre taux est requis")]
    public double? Taux { get; set; }
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
      List<ValidationResult> résultats = new List<ValidationResult>();
      bool ok = Taux < 4.2 || Taux > 6.7;
      if (!ok)
      {
        résultats.Add(new ValidationResult("Le paramètre taux doit être < 4.2 ou > 6.7", new string[] { "Taux" }));
      }
      return résultats;
    }
  }
}

  • linha 6: o modelo implementa a interface [IValidatableObject];
  • linha 10: o método [Validate] desta interface. Retorna uma coleção de elementos do tipo [ValidationResult]. Este tipo encapsula os erros a serem reportados;
  • linha 9: uma taxa válida é uma taxa <4,2 ou > 6,7;
  • linha 12: criamos uma lista vazia de elementos do tipo [ValidationResult];
  • linha 13: verificamos a validade da propriedade [Rate];
  • linhas 14–17: se a propriedade [Rate] for inválida, então um elemento do tipo [ValidationResult] é adicionado à lista de resultados. O primeiro parâmetro é uma mensagem de erro. O segundo parâmetro, que é opcional, é uma coleção das propriedades afetadas por este erro.

A ação que utiliza este modelo será a seguinte:


    // Action12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Aqui está um exemplo de execução:

Image

4.8. Modelo de ação do tipo Table ou List

Considere a seguinte ação [Ação13]:


// Action13
    public ContentResult Action13(string[] data)
    {
      string strData = "";
      if (data != null && data.Length != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}]", strData);
      return Content(texte, "text/plain", Encoding.UTF8);
    }

  • Linha 2: O modelo de ação consiste numa matriz de [string]. Permite-nos recuperar um parâmetro denominado [data], que pode aparecer várias vezes nos parâmetros da solicitação, como em [?data=data1&data=data2&data=data3]. Os vários parâmetros [data] na solicitação irão preencher a matriz [data] no modelo de ação. Este cenário ocorre com listas suspensas. O navegador envia então os diferentes valores selecionados pelo utilizador, todos com o mesmo nome de parâmetro.

Eis um exemplo:

Image

O modelo também pode ser uma lista:


    // Action14
    public ContentResult Action14(List<int> data)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string strData = "";
      if (data != null && data.Count != 0)
      {
        strData = string.Join(",", data);
      }
      string texte = string.Format("data=[{0}], erreurs=[{1}]", strData, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

O modelo aqui é uma lista de inteiros (linha 2). Aqui está a primeira execução:

Image

e uma segunda:

Image

4.9. Filtrar um modelo de ação

Por vezes, temos um modelo, mas queremos que apenas determinados elementos do modelo sejam inicializados pela solicitação HTTP. Considere o seguinte modelo de ação [ActionModel06]:


using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace Exemple_02.Models
{
  [Bind(Exclude = "Info2")]
  public class ActionModel06
  {
    [Required(ErrorMessage = "Le paramètre [info1] est requis")]
    public string Info1 { get; set; }
 
    public string Info2 { get; set; }
  }
}
  • linhas 9-10: o parâmetro [info1] é obrigatório;
  • linha 6: o parâmetro [info2] na linha 12 é excluído da ligação do pedido HTTP ao seu modelo.

A ação será a seguinte [Action15]:


    // Action15
    public ContentResult Action15(ActionModel06 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("valide={0}, info1={1}, info2={2}, erreurs={3}", ModelState.IsValid, modèle.Info1, modèle.Info2, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Aqui está um exemplo de execução:

  • em [1]: passamos o parâmetro [info2] na URL;
  • em [2]: a propriedade [Info2] do modelo de ação permanece vazia.

4.10. Ampliando o modelo de ligação de dados

Vamos rever a arquitetura de execução de uma ação:

A classe de ação é instanciada no início da solicitação do cliente e destruída no final da mesma. Portanto, não pode ser usada para armazenar dados entre solicitações, mesmo que seja chamada repetidamente. Pode querer armazenar dois tipos de dados:

  • dados partilhados por todos os utilizadores da aplicação web. Geralmente, trata-se de dados de leitura apenas. São utilizados três ficheiros para implementar esta partilha de dados:
    • [Web.Config]: o ficheiro de configuração da aplicação
    • [Global.asax, Global.asax.cs]: permitem-lhe definir uma classe, denominada classe de aplicação global, cuja duração corresponde à da aplicação, bem como manipuladores para determinados eventos dessa mesma aplicação.

A classe de aplicação global permite-lhe definir dados que estarão disponíveis para todos os pedidos de todos os utilizadores.

  • dados partilhados entre pedidos do mesmo cliente. Estes dados são armazenados num objeto chamado Sessão. Referimo-nos a isto como a sessão do cliente para designar a memória do cliente. Todos os pedidos de um cliente têm acesso a esta sessão. Podem armazenar e ler informações nessa sessão.

Acima, mostramos os tipos de memória a que uma ação tem acesso:

  • a memória da aplicação, que contém principalmente dados de leitura única e é acessível a todos os utilizadores;
  • a memória de um utilizador específico, ou sessão, que contém dados de leitura/gravação e é acessível a pedidos sucessivos do mesmo utilizador;
  • não mostrado acima, existe uma memória de solicitação, ou contexto de solicitação. A solicitação de um utilizador pode ser processada por várias ações sucessivas. O contexto de solicitação permite que a Ação 1 passe informações para a Ação 2.

Vejamos um primeiro exemplo que ilustra estes diferentes tipos de memória:

Primeiro, modificamos o ficheiro [Web.config] do projeto [Example-02] da seguinte forma:


  <appSettings>
    <add key="webpages:Version" value="2.0.0.0" />
    ...
    <add key="infoAppli1" value="infoAppli1"/>
</appSettings>

Adicionamos a linha 4, que associa o valor [infoAppli1] à chave [infoAppli1]. Estes serão os nossos dados no âmbito [Application]: estarão acessíveis a todos os pedidos de todos os utilizadores.

Em seguida, modificamos o método [Application_Start] no ficheiro [Global.asax]. Este método é executado uma vez quando a aplicação é iniciada. É aqui que precisamos de utilizar o ficheiro [Web.config]:


    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
 
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // intialisation application
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

Adicionamos a linha 10. Ela faz duas coisas:

  • recupera o valor da chave [infoAppli1] do ficheiro [Web.config] utilizando a classe [System.Configuration.ConfigurationManager];
  • armazena-o no dicionário [HttpApplication.Application], associado à chave [infoAppli1]. Todas as ações têm acesso a este dicionário.

No mesmo ficheiro [Global.asax], adicionamos o seguinte método [Session_Start]:


    protected void Session_Start()
    {
      // initialisation compteur
      Session["compteur"] = 0;
}

O método [Session_Start] é executado para cada novo utilizador. O que é um novo utilizador? Um utilizador é «rastreado» por um token de sessão. Este token é:

  • criado pelo servidor web e enviado ao novo utilizador nos cabeçalhos HTTP da primeira resposta que lhe é enviada;
  • enviado de volta pelo navegador do utilizador com cada novo pedido que este efetua. Isto permite ao servidor reconhecer o utilizador e gerir um espaço de memória para ele, denominado sessão do utilizador.

O servidor web reconhece que está a lidar com um novo utilizador quando este não envia um token de sessão. O servidor cria então um para ele.

Na linha 4 acima, colocamos um contador na sessão do utilizador que será incrementado com cada pedido desse utilizador. Isto ilustra a memória associada a um utilizador. A classe [Session] é utilizada como um dicionário (linha 4).

Feito isso, escrevemos a seguinte ação [Action16]:


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • linha 5: recuperamos o contexto da solicitação HTTP atualmente em processamento. Este contexto nos dará acesso aos dados nos escopos [Application] e [Session];
  • linha 7: recuperamos as informações do âmbito [Application];
  • linha 9: recuperamos o contador da sessão;
  • linhas 10–11: este é incrementado e, em seguida, armazenado novamente na sessão;
  • Linhas 13–14: ambas as informações são enviadas para o cliente.

Aqui estão alguns exemplos de execução:

[Ação16] é solicitada uma vez [1], depois a página é atualizada [F5] duas vezes [2]:

Em [2], o cliente efetuou um total de três pedidos. Em cada ocasião, conseguiu recuperar o contador atualizado pelo pedido anterior.

Para simular um segundo utilizador, utilizamos um segundo navegador para solicitar o mesmo URL:

Em [3], o segundo utilizador recupera com sucesso a mesma informação do âmbito [Application], mas possui o seu próprio contador do âmbito [Session].

Voltemos ao código da ação [Action16]:


// Action16
    public ContentResult Action16()
    {
      // on récupère le contexte de la requête HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // on récupère les infos de portée Application
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // et celles de portée Session
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // la réponse au client
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Um dos objetivos da estrutura ASP.NET MVC é tornar os controladores e as ações testáveis de forma isolada, sem um servidor web. No entanto, como se vê na linha 5, o contexto da solicitação HTTP é necessário para recuperar dados dos âmbitos [Application] e [Session]. Propomos a criação de uma nova ação [Action17] que receberia dados dos âmbitos [Application] e [Session] como parâmetros:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      int compteur = sessionData.Compteur++;
      // customer response
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

O código já não tem quaisquer dependências da solicitação HTTP. Por isso, pode ser testado independentemente de um servidor web.

Vamos ver como fazer isto. Primeiro, precisamos de criar as classes [ApplicationModel] e [SessionModel], que irão encapsular os dados dos âmbitos [Application] e [Session], respetivamente. São as seguintes:


namespace Exemple_02.Models
{
  public class ApplicationModel
  {
    public string InfoAppli1 { get; set; }
  }
}


namespace Exemple_02.Models
{
  public class SessionModel
  {
    public int Compteur { get; set; }
    public SessionModel()
    {
      Compteur = 0;
    }
  }
}

Em seguida, precisamos de modificar os métodos [Application_Start] e [Session_Start] no ficheiro [Global.asax]:


public class MvcApplication : System.Web.HttpApplication
  {
    protected void Application_Start()
    {
      AreaRegistration.RegisterAllAreas();
 
      WebApiConfig.Register(GlobalConfiguration.Configuration);
      FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
      RouteConfig.RegisterRoutes(RouteTable.Routes);
      BundleConfig.RegisterBundles(BundleTable.Bundles);
      // intialisation application - case 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }
 
    protected void Session_Start()
    {
      // counter initialization - case 1
      Session["compteur"] = 0;
      // counter initialization - case 2
      Session["data"] = new SessionModel();
    }
  }

  • linha 14: é criada uma instância de [ApplicationModel];
  • linha 15: é inicializada;
  • linha 16: e colocada no dicionário [Application], associada à chave [data]. [Application] é uma propriedade da classe [HttpApplication] da linha 1;
  • linha 24: é criada uma instância de [SessionModel] e colocada no dicionário [Session], associada à chave [data]. [Session] é uma propriedade da classe [HttpApplication] da linha 1;

Com base no que vimos até agora, a assinatura


    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)

significa que o pedido HTTP processado pela ação deve incluir parâmetros denominados [applicationData] e [sessionData]. Não será esse o caso. Temos de criar um novo modelo de ligação de dados para que, quando uma ação receber um tipo como parâmetro:

  • [ApplicationModel], os dados com âmbito [Application] e chave [data] lhe sejam fornecidos;
  • [SessionModel], os dados com âmbito [Session] e chave [data] lhe sejam fornecidos.

Para tal, precisamos de criar classes que implementem a interface [IModelBinder].

Começamos por criar uma pasta [Infrastructure] no projeto [Example-02]:

Image

Nela, criamos a seguinte classe [ApplicationModelBinder]:


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [Application]
      return controllerContext.RequestContext.HttpContext.Application["data"];
    }
  }
}

  • linha 5: a classe implementa a interface [IModelBinder]. Para compreender o seu código, é necessário saber que será chamada sempre que uma ação tiver um parâmetro do tipo [ApplicationModel]. Esta ligação [ApplicationModel] --> [ApplicationModelBinder] será estabelecida quando a aplicação for iniciada, no método [Application_Start] do [Global.asax];
  • linha 7: o único método da interface [IModelBinder];
  • Linha 7: O parâmetro [ControllerContext] dá-nos acesso ao pedido HTTP que está a ser processado;
  • linha 7: o parâmetro do tipo [ModelBindingContext] dá-nos acesso a informações sobre o modelo a ser construído, neste caso o tipo [ApplicationModel];
  • linha 7: o resultado de [BindModel] é o objeto que será atribuído ao parâmetro vinculado, neste caso um parâmetro do tipo [ApplicationModel];
  • linha 10: simplesmente devolvemos o objeto com o âmbito [Application] e a chave [data].

A classe [ SessionModelBinder] segue o mesmo padrão:


using System.Web.Mvc;
 
namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // render scope data [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

Resta apenas associar cada modelo [XModel] ao seu [XModelBinder]. Isto é feito no método [Application_Start] do [Global.asax]:


    protected void Application_Start()
    {
....
      // intialisation application - case 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // model binders
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}

  • linha 9: quando uma ação tem um parâmetro do tipo [ApplicationModel], o método [ApplicationModelBinder.Bind] será chamado. Sabemos que ele retorna os dados no escopo [Application] associados à chave [data];
  • linha 10: o mesmo se aplica ao tipo [SessionModel].

Voltemos à nossa ação [Action17]:


    // Action17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // retrieve range info Application
      string infoAppli1 = applicationData.InfoAppli1;
      // and Session range
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // customer response
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: Quando [Action17] for chamada, receberá
    • primeiro parâmetro: os dados do âmbito [Application] associados à chave [data],
    • segundo parâmetro: os dados do âmbito [Session] associados à chave [data];

Estes dois conjuntos de dados podem ser tão complexos quanto desejado e podem incluir, por um lado, todos os dados do âmbito [Application] e, por outro, todos os dados do âmbito [Session].

Eis um exemplo da execução da ação [Action17]:

Image

4.11. Ligação tardia do modelo de ação

Escrevemos a seguinte [Ação12]:


// Action12
    public ContentResult Action12(ActionModel05 modèle)
    {
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

Nos bastidores, o ASP.NET MVC:

  • cria uma instância do tipo [ActionModel05] utilizando o seu construtor sem parâmetros;
  • inicializa-a com informações da solicitação que têm o mesmo nome (sem distinção entre maiúsculas e minúsculas) que uma das propriedades de [ActionModel05].

Por vezes, este comportamento não é o que pretendemos. Isto acontece especialmente quando queremos utilizar um construtor específico do modelo de ação. Podemos então proceder da seguinte forma:


    // Action18
    public ContentResult Action18()
    {
      ActionModel05 modèle = new ActionModel05();
      TryUpdateModel(modèle);
      string erreurs = getErrorMessagesFor(ModelState);
      string texte = string.Format("taux={0}, erreurs={1}", modèle.Taux, erreurs);
      return Content(texte, "text/plain", Encoding.UTF8);
}

  • Linha 2: A ação já não recebe parâmetros. Por conseguinte, já não existe qualquer ligação automática de dados;
  • linha 4: criamos nós próprios uma instância do modelo de ação. É aqui que poderíamos usar um construtor diferente;
  • linha 5: inicializamos o modelo com as informações da solicitação. O ASP.NET MVC realiza este trabalho. Faz-o da mesma forma que faria se o modelo fosse um parâmetro;
  • linha 6: estamos agora na mesma situação que na ação [Action12].

Eis um exemplo de execução:

Image

4.12. Conclusão

Voltemos à arquitetura de uma aplicação ASP.NET MVC:

Uma solicitação [1] transporta várias informações que o ASP.NET MVC apresenta [2a] à ação na forma de um modelo, ao qual chamamos de modelo de ação.

  • A solicitação HTTP do cliente chega a [1];
  • em [2], as informações contidas na solicitação são transformadas num modelo de ação [3];
  • Em [4], a ação, com base neste modelo, irá gerar uma resposta. Esta resposta terá dois componentes: uma vista V [6] e o modelo M para essa vista [5];
  • a vista V [6] utilizará o seu modelo M [5] para gerar a resposta HTTP destinada ao cliente.

No modelo MVC, a ação [4] faz parte do C (controlador), o modelo de vista [5] é o M e a vista [6] é o V.

Este capítulo examinou os mecanismos que ligam a informação transportada pela solicitação — que é inerentemente composta por strings — ao modelo da ação, que pode ser uma classe com propriedades de vários tipos. Também vimos que é possível validar o modelo apresentado à ação. Por fim, vimos como estender este modelo para incluir dados dos escopos [Session] e [Application].

Vamos agora concentrar-nos no final da cadeia de processamento da solicitação [1]: a criação da vista [6] e do seu modelo [5]. Estes dois elementos são produzidos pela ação [4].