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 para a ação [2a] que o irão processar, um mecanismo a que se chama encaminhamento. Apresentámos também as diferentes respostas que uma ação pode enviar ao navegador. Até ao momento, apresentámos ações que não exploravam a solicitação que lhes era apresentada. Uma solicitação [1] transporta consigo diversas informações que ASP.NET e MVC apresentam à ação sob a forma de um modelo. Este termo não deve ser confundido com o modelo M de uma vista V [2c], que é produzido pela ação:

  • a solicitação HTTP do cliente chega como [1];
  • em [2], as informações contidas na solicitação serão transformadas no modelo de ação [3] — frequentemente, mas não necessariamente, uma classe — que servirá de entrada para a ação [4];
  • em [4], a ação, a partir deste modelo, irá gerar uma resposta. Esta terá duas componentes: uma vista V [6] e o modelo M dessa 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 da vista [5] é o M e a vista [6] é o V.

Este capítulo analisa os mecanismos de ligação entre as informações transportadas pela solicitação, que são, por natureza, cadeias de caracteres, e o modelo da ação, que pode ser uma classe com propriedades de vários tipos.

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

Adicionamos à solução existente um novo projeto [1], com base nos projetos ASP.NET e MVC:

  • em [2], o nome do novo projeto;
  • em [3, 4], escolhemos um projeto de base ASP.NET MVC;
  • em [5], o novo projeto.

Faremos do novo projeto o projeto inicial da solução.

Tal como foi feito no parágrafo 3.1, criamos um controlador denominado [First] [1]:

  

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


using System.Web.Mvc;

namespace Exemple_02.Controllers
{
  public class FirstController : Controller
  {
    // Ação01
    public ContentResult Action01(string nom)
    {
      return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
    }

  }
}

A novidade reside na linha 8: o método [Action01] tem um parâmetro. Neste capítulo, abordamos as diferentes formas de inicializar os parâmetros de uma ação. O parâmetro [nom] acima é inicializado, por ordem, com os seguintes valores:

Request.Form["nom"]
um parâmetro denominado [nom] enviado por um comando POST
RouteData.Values["nom"]
um elemento de URL denominado [nom]
Request.QueryString["nom"]
um parâmetro denominado [nom] enviado por um comando GET
Request.Files["nom"]
um ficheiro carregado denominado [nom]

Vamos analisar estes diferentes casos. Vamos solicitar diretamente no navegador o URL [/First/Action01?nom=someone]. Obtemos a seguinte resposta:

 

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: a solicitação é um GET. O URL solicitado inclui o parâmetro [nom]. Do lado do servidor, a solicitação chega à ação [Action01], que tem a seguinte assinatura:

public ContentResult Action01(string nom)

Para atribuir um valor ao parâmetro «nom», a ação ASP.NET MVC tenta sucessivamente e por ordem os valores Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"]. O processo pára assim que encontrar um valor. O parâmetro [nom], incorporado no URL do GET, foi colocado pelo framework no Request.QueryString["nom"]. É com este valor [someone] que será inicializado o parâmetro [nom] de [Action01]. 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:

 

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


public ContentResult Action01(string NOM)

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

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

  • em [1], o URL solicitado;
  • em [2], será utilizado o comando POST;
  • em [3], os parâmetros de POST.

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

  • no [1], o POST;
  • no [2], os parâmetros do POST. Tecnicamente, foram enviados a seguir aos cabeçalhos do HTTP, após a linha em branco que assinala o fim desses cabeçalhos;
  • no [3], a resposta obtida. Recupera-se corretamente o parâmetro [nom] do POST. Nos valores testados para o parâmetro «nom» Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], foi o primeiro que funcionou.

Agora, vamos alterar a rota predefinida em [App_Start/RouteConfig]. Atualmente, essa rota é a seguinte:


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

Vamos alterá-la para:


      routes.MapRoute(
          name: "Default",
          url: "{controller}/{action}/{nom}",
          defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);
  • na linha 3, atribuímos o nome [nom] ao terceiro elemento de uma rota;
  • na linha 4, este elemento é declarado como opcional.

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

 

Nos valores testados para o parâmetro «nome» — Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], foi a segunda que funcionou.

Vamos fazer a mesma consulta com um POST e um [Advanced Rest Client]:

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

Nos valores testados para o parâmetro [nom], Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], dois eram adequados: os dois primeiros. Foi utilizado o primeiro.

4.2. Verificar a validade dos parâmetros da ação

Se uma ação tiver um parâmetro denominado [p], o ASP.NET e o MVC tentarão atribuir-lhe um dos valores Request.Form["p"], RouteData.Values["p"], Request.QueryString["p"], 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 ocorrer problemas.

Vamos criar a seguinte nova ação:


    // Ação02
    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);
}
  • Na linha 2, a ação [Action02] aceita um parâmetro denominado [age] do tipo int. A cadeia de caracteres recuperada terá de ser convertível em int.

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

 

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

 

Desta vez, foi apresentada 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 octetos (linha 5) para explicar as possíveis razões deste erro.

Vamos agora criar a seguinte ação [Action03]:


    // Ação03
    public ContentResult Action03(int? age)
    {
      ...
}

[Action03] é idêntico a [Action02], com a única diferença de que alterámos o tipo do parâmetro [age] para int?, o que significa inteiro ou nulo.

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

 

O ASP.NET MVC não conseguiu converter o [21x] para o tipo int. Por isso, atribuiu o valor nulo ao parâmetro [age], tal como permitido pelo seu tipo int?. No entanto, é possível saber se o parâmetro conseguiu receber um valor da consulta ou não.

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


    // Ação04
    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, nomeadamente, que a consulta não forneça o parâmetro [age], que recebe, assim, o valor nulo;
  • linha 4: verifica-se se o modelo da ação é válido. O modelo da ação é constituído pelo conjunto dos seus parâmetros, neste caso [age]. O modelo é válido se todos os parâmetros tiverem obtido um valor da solicitação ou, caso o tipo do parâmetro o permita, o valor nulo;
  • linha 5: adiciona-se o valor da variável [valide] ao texto enviado ao cliente.

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

 

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

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


    // Ação05
    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 novidade está na linha 4. Nela, é chamado um método privado [getErrorMessagesFor], ao qual é passado o estado do modelo da ação. Este método devolve uma cadeia de caracteres que reúne as mensagens de todos os erros 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 efetivo [ModelState] passado ao método é do tipo [ModelStateDictionary];
  • linha 3: uma lista de mensagens de erro, inicialmente vazia;
  • linha 5: verifica-se se o estado passado como parâmetro é válido ou não. Se não for, então agregam-se todas as mensagens de erro numa única cadeia de caracteres;
  • linha 7: o tipo [ModelStateDictionary] possui uma propriedade [Values] que é uma coleção de tipos [ModelState]. Existe um [ModelState] por cada 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 eventual mensagem de erro n.º i para o parâmetro [age] do modelo
    • ModelState["age"].Errors[i].Exceção: a exceção do erro n.º i da coleção de erros relativa ao parâmetro [age],
    • ModelState["age"].Errors[i].Exception.InnerException: a causa desta exceção,
    • ModelState["age"].Errors[i].Exception.InnerException.Message: a mensagem relativa à causa da exceção;
  • linha 9: percorre-se a coleção [Errors] de um [ModelState] específico;
  • linha 11: recupera-se a mensagem de erro de um [ModelError] específico e adiciona-se à lista de mensagens de erro da linha 3;
  • linhas 14-17: agregam-se os elementos da lista de mensagens de erro numa única cadeia de caracteres.

O método [getErrorMessageFor] da 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: recebe-se um tipo [ModelError] que encapsula um erro num dos elementos do modelo da ação. A mensagem de erro é obtida em 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, verifica-se que a mensagem de erro é encontrada nestes três locais, dependendo da natureza do elemento do modelo. Deve existir uma regra que permita obter com certeza a mensagem de erro associada a um elemento do modelo, mas não a conheço. Por isso, procuro-a nos diferentes locais onde a posso encontrar, seguindo uma determinada ordem. Assim que for encontrada uma mensagem não vazia, esta é devolvida.

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

 

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

Consideremos a seguinte nova ação:


    // Ação06
    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, [poids] e [age].

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

 
 

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

Vamos definir uma classe que servirá de modelo para uma ação. Colocamo-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 [Poids] e [Age], analisados anteriormente. Esta classe será o parâmetro de entrada da ação [Action07]:


    // Ação07
    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 da ação é uma instância do tipo [ActionModel01].

Voltemos aos mesmos dois exemplos anteriores:

 
 

Note-se que a ligação dos parâmetros não distingue maiúsculas de minúsculas. Os parâmetros da consulta eram [age] e [poids]. Estes alimentaram as propriedades [Age] e [Poids] da classe [ModelAction01].

Além disso, até agora utilizámos as consultas HTTP e [GET]. Vamos demonstrar que as consultas [POST] têm o mesmo comportamento. Para tal, vamos utilizar novamente a aplicação [Advanced Rest Client]:

  • em [1], a URL solicitada;
  • em [2], será solicitada por um comando POST;
  • no [3], os parâmetros do POST.

Obtém-se a mesma resposta que com o GET:

Image

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

Com o modelo anterior:


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

os parâmetros [poids] e [age] podem estar ausentes da consulta. Nesse caso, as propriedades [Poids] e [Age] recebem o valor [null] e não é sinalizado qualquer erro. Poder-se-á querer transformar o modelo da seguinte forma:


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

Nas linhas 5 e 6, as propriedades [Poids] e [Age] já não podem ter o valor [null]. Vamos ver o que acontece com este novo modelo quando os parâmetros [poids] e [age] não constam da consulta.

 

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

  • criou uma instância do modelo através de um «new» ActionModel01. Foi aí que as propriedades [Poids] e [Age] receberam o valor 0;
  • não atribuiu qualquer valor a estas duas propriedades, uma vez que não existia nenhum parâmetro com o respetivo nome.

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

Consideremos o seguinte novo modelo de ação:

  

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 [Poids] é obrigatório;
  • linha 7: indica que o campo [Poids] deve estar no intervalo [1,200];
  • linha 9: indica que o campo [Age] é obrigatório;
  • linha 7: indica que o campo [Age] deve estar no intervalo [1,150];

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


    // Ação08
    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 fazer alguns testes:

 
 
 
 

Os erros são corretamente detetados. Agora, vamos alterar 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; }
  }
}

Nas linhas 8 e 11, as propriedades já não podem ter o valor [null]. Vamos compilar e repetir o teste sem parâmetros:

 

A ausência de parâmetros fez com que as propriedades [Poids] e [Age] mantivessem o valor adquirido durante a instanciação do modelo: 0. A validação ocorre em seguida. O atributo [Required] é, então, satisfeito. Verifica-se que a mensagem de erro acima corresponde ao atributo [Range]. Assim, para verificar a presença de um parâmetro, é necessário que a propriedade associada seja nullable, ou seja, que possa receber o valor null.

Voltemos ao modelo inicial [ActionModel02] e consideremos uma ação cujo modelo é constituído por uma instância [ActionModel02] e por um tipo [DateTime] nullable:


    // Ação09
    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 fazer alguns testes:

 

Não foram passados parâmetros para a ação. Os atributos [Required] das propriedades [Poids] e [Age] cumpriram a sua função. A data, por sua vez, recebeu o valor null e não foi sinalizado qualquer erro.

Passamos agora a introduzir parâmetros inválidos:

 

Passamos agora valores válidos:

 

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

  

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 que nós próprios definimos;
  • linha 7: o atributo [EMailAddress] exige que o campo [Email] contenha um endereço de e-mail com um formato válido;
  • linha 11: o atributo [RegularExpression] exige que o campo [Jour] contenha uma sequência de um ou dois algarismos. O primeiro parâmetro é a expressão regular que o campo deve verificar;
  • 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, no mínimo, 2 caracteres;
  • linhas 23-24: os atributos [MaxLength] e [MinLength], em conjunto, exigem que o campo [Info3] tenha exatamente 4 caracteres;

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


// Ação10
    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 fazer alguns testes com esta ação.

Primeiro, sem parâmetros:

 

Depois, com parâmetros inválidos:

 

Depois, com parâmetros válidos:

 

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

Apresentamos outras restrições de integridade. O novo modelo da 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: solicita 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:


    // Ação11
    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], o URL da ação [Action11];
  • em [2], este URL será solicitado juntamente com um POST;
  • no [3], seleciona-se o separador [Form];
  • em [4], os valores dos quatro parâmetros esperados. Esta inicialização é uma funcionalidade oferecida por [ARC]. Os parâmetros efetivamente enviados podem ser visualizados na guia [Raw] [5];
  • em [6], os parâmetros do POST.

Para esta consulta, recebe-se a seguinte resposta:

Vamos passar parâmetros inválidos:

 

Obtemos então a seguinte resposta:

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

Por vezes, as restrições de integridade disponíveis não são suficientes. Nesse caso, é possível criar as nossas próprias restrições. Em particular, podemos utilizar um modelo que implemente a interface [IValidatableObject]. Neste caso, adicionamos as nossas próprias verificações do modelo no método [Validate] desta interface. Vejamos um exemplo. O novo modelo da 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. Este método devolve uma coleção de elementos do tipo [ValidationResult]. Este tipo encapsula os erros que se pretende sinalizar;
  • linha 9: uma taxa válida é uma taxa <4,2 ou > 6,7;
  • linha 12: cria-se uma lista vazia de elementos do tipo [ValidationResult];
  • linha 13: verifica-se a validade da propriedade [Taux];
  • linhas 14-17: se a propriedade [Taux] for inválida, então adiciona-se um elemento do tipo [ValidationResult] à lista de resultados. O primeiro parâmetro é uma mensagem de erro. O segundo parâmetro, opcional, é uma coleção das propriedades afetadas por este erro.

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


    // Ação12
    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);
}

Eis um exemplo de execução:

 

4.8. Modelo de ação do tipo Tabela ou Lista

Consideremos a seguinte ação [Action13]:


// Ação13
    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 da ação é constituído por uma tabela de [string]. Permite-nos recuperar um parâmetro denominado [data], que pode estar presente várias vezes nos parâmetros da consulta, como em [?data=data1&data=data2&data=data3]. Os diferentes parâmetros [data] da solicitação irão alimentar a tabela [data] do modelo da ação. Este caso ocorre com listas de escolha múltipla. O navegador envia então os diferentes valores selecionados pelo utilizador, com o mesmo nome de parâmetro.

Eis um exemplo:

 

O modelo também pode ser uma lista:


    // Ação14
    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 é, neste caso, uma lista de números inteiros (linha 2). Eis uma primeira execução:

 

e uma segunda:

 

4.9. Filtragem de um modelo de ação

Por vezes, dispomos de um modelo, mas pretendemos que apenas determinados elementos do modelo sejam inicializados pela consulta HTTP. Consideremos 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] da linha 12 é excluído da ligação entre a consulta HTTP e o seu modelo.

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


    // Ação 15
    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);
}

Eis um exemplo de execução:

  • em [1]: o parâmetro [info2] é passado para o URL;
  • no [2]: a propriedade [Info2] do modelo da ação permaneceu vazia.

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

Voltemos à arquitetura de execução de uma ação:

A classe da ação é instanciada no início do pedido do cliente e destruída no final do mesmo. Por isso, não pode ser utilizada para armazenar dados entre dois pedidos, mesmo que seja chamada repetidamente. Pode ser necessário armazenar dois tipos de dados:

  • dados partilhados por todos os utilizadores da aplicação web. Trata-se, geralmente, de dados de leitura única. 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]: permite definir uma classe, denominada classe global da aplicação, cuja duração corresponde à da própria aplicação, bem como gestores para determinados eventos dessa mesma aplicação.

A classe global da aplicação permite definir dados que estarão disponíveis para todas as consultas de todos os utilizadores.

  • os dados partilhados pelas solicitações de um mesmo cliente. Esses dados são armazenados num objeto denominado «Sessão». Fala-se então de «sessão do cliente» para designar a memória do cliente. Todas as solicitações de um cliente têm acesso a essa sessão. Podem armazenar e ler informações nessa sessão.

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

  • a memória da aplicação, que na maioria das vezes contém dados de leitura única e que está 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 que está acessível às solicitações sucessivas do mesmo utilizador;
  • não representada acima, existe uma memória de pedido, ou contexto de pedido. O pedido de um utilizador pode ser processado por várias ações sucessivas. O contexto do pedido permite que uma ação 1 transmita informação a uma ação 2.

Vejamos um primeiro exemplo que ilustra estas diferentes memórias:

Em primeiro lugar, alteramos o ficheiro [Web.config] do projeto [Exemple-02] da seguinte forma:


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

Acrescentamos a linha 4, que associa o valor [infoAppli1] à chave [infoAppli1]. Este será o nosso dado de âmbito [Application]: estará acessível a todas as consultas de todos os utilizadores.

Em seguida, alteramos o método [Application_Start] do ficheiro [Global.asax]. Este método é executado uma única vez no arranque da aplicação. É aqui que devemos 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);
      // inicialização da aplicação
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}

Adicionamos a linha 10. Esta linha faz duas coisas:

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

No mesmo ficheiro [Gloabal.asax], adiciona-se o seguinte método [Session_Start]:


    protected void Session_Start()
    {
      // inicialização do contador
      Session["compteur"] = 0;
}

O método [Session_Start] é executado para qualquer novo utilizador. O que é um novo utilizador? Um utilizador é «acompanhado» 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;
  • reenviado pelo navegador do utilizador em cada nova solicitação que este efetua. Isto permite ao servidor reconhecer o utilizador e gerir uma memória para ele, a que se chama sessão do utilizador.

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

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

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


// Ação 16
    public ContentResult Action16()
    {
      // recuperar o contexto da solicitação HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // recuperar as informações do âmbito «Aplicação»
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // e as do âmbito da sessão
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // a resposta ao cliente
      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 que está a ser processada. Este contexto dar-nos-á acesso aos dados de âmbito [Application] e [Session];
  • linha 7: recuperam-se as informações do âmbito [Application];
  • linha 9: recupera-se o contador da sessão;
  • linhas 10-11: este é incrementado e, em seguida, devolvido à sessão;
  • linhas 13-14: ambas as informações são enviadas ao cliente.

Eis alguns exemplos de execução:

[Action16] é solicitado uma primeira vez como [1]; em seguida, a página é atualizada duas vezes como [F5] e [2]:

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

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

No [3], o segundo utilizador recupera, de facto, a mesma informação de alcance do [Application], mas tem o seu próprio contador de alcance [Session].

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


// Ação 16
    public ContentResult Action16()
    {
      // recupera-se o contexto do pedido HTTP
      HttpContextBase contexte = ControllerContext.HttpContext;
      // recuperam-se as informações do âmbito da aplicação
      string infoAppli1 = contexte.Application["infoAppli1"] as string;
      // e as do âmbito da sessão
      int? compteur = contexte.Session["compteur"] as int?;
      compteur++;
      contexte.Session["compteur"] = compteur;
      // a resposta ao cliente
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

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


    // Ação 17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // recuperam-se as informações do âmbito «Aplicação»
      string infoAppli1 = applicationData.InfoAppli1;
      // e as do âmbito «Sessão»
      int compteur = sessionData.Compteur++;
      // a resposta ao cliente
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}

O código já não apresenta dependências em relação à consulta HTTP. Por conseguinte, pode ser testado independentemente de um servidor web.

Vamos ver como o fazer. Em primeiro lugar, temos de criar as classes [ApplicationModel] e [SessionModel], que irão encapsular, respetivamente, os dados de âmbito [Application] e [Session]. 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, temos de alterar os métodos [Application_Start] e [Session_Start] do 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);
      // inicialização da aplicação - caso 1
      Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
      // inicialização da aplicação - caso 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
    }

    protected void Session_Start()
    {
      // inicialização do contador - caso 1
      Session["compteur"] = 0;
      // inicialização do contador - caso 2
      Session["data"] = new SessionModel();
    }
  }
  • linha 14: é criada uma instância de [ApplicationModel];
  • linha 15: esta é inicializada;
  • linha 16: e colocada no dicionário de [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 de [Session], associada à chave [data]. [Session] é uma propriedade da classe [HttpApplication] da linha 1;

Se nos basearmos no que vimos até agora, a assinatura


    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)

significa que a consulta HTTP processada pela ação deverá 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 como parâmetro um tipo:

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

Para tal, é necessário criar classes que implementem a interface [IModelBinder].

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

  

Nessa pasta, criamos a seguinte classe [ApplicationModelBinder]:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class ApplicationModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // fornecem-se os dados do escopo [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 ela será chamada sempre que uma ação tiver um parâmetro do tipo [ApplicationModel]. Esta ligação [ApplicationModel] --> [ApplicationModelBinder] será estabelecida no arranque da aplicação, no método [Application_Start] de [Global.asax];
  • linha 7: o único método da interface [IModelBinder];
  • linha 7: o parâmetro do tipo [ControllerContext] dá-nos acesso à consulta HTTP que está a ser processada;
  • linha 7: o parâmetro de tipo [ModelBindingContext] dá-nos acesso a informações sobre o modelo a construir, neste caso o tipo [ApplicationModel];
  • linha 7: o resultado de [BindModel] é o objeto que será atribuído ao parâmetro associado, neste caso um parâmetro do tipo [ApplicationModel];
  • linha 10: limitamo-nos a definir o objeto com o escopo [Application] e a chave [data].

A classe [SessionModelBinder] segue o mesmo esquema:


using System.Web.Mvc;

namespace Exemple_02.Infrastructure
{
  public class SessionModelBinder : IModelBinder
  {
    public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
      // são apresentados os dados do âmbito [Session]
      return controllerContext.HttpContext.Session["data"];
    }
  }
}

Resta-nos apenas associar cada um dos modelos [XModel] ao seu binder e [XModelBinder]. Isto é feito no método [Application_Start] de [Global.asax]:


    protected void Application_Start()
    {
....
      // inicialização da aplicação - caso 2
      ApplicationModel data=new ApplicationModel();
      data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
      Application["data"] = data;
      // ligadores de modelos
      ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
      ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}
  • linha 9: quando uma ação tiver um parâmetro do tipo [ApplicationModel], o método [ApplicationModelBinder.Bind] será chamado. Sabe-se que este método devolve o dado de âmbito [Application] associado à chave [data];
  • linha 10: o mesmo se aplica ao tipo [SessionModel].

Voltemos à nossa ação [Action17]:


    // Ação 17
    public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
    {
      // recuperam-se as informações do âmbito «Aplicação»
      string infoAppli1 = applicationData.InfoAppli1;
      // e as do âmbito «Sessão»
      sessionData.Compteur++;
      int compteur = sessionData.Compteur;
      // a resposta ao cliente
      string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
      return Content(texte, "text/plain", Encoding.UTF8);
}
  • linha 2: quando a ação [Action17] for chamada, receberá como
    • primeiro parâmetro: os dados de âmbito [Application] associados à chave [data],
    • segundo parâmetro: o dado de âmbito [Session] associado à chave [data];

Estes dois dados podem ser tão complexos quanto se desejar e agrupar, num deles, todos os dados do âmbito [Application] e, no outro, todos os dados do âmbito [Session].

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

 

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

Escrevemos a seguinte ação [Action12]:


// Ação 12
    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);
}

De forma oculta, a ação ASP.NET MVC:

  • cria uma instância do tipo [ActionModel05] utilizando o seu construtor sem parâmetros;
  • inicializa-a com as 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 nos convém. É nomeadamente o caso quando se pretende utilizar um construtor específico do modelo da ação. Nesse caso, pode-se proceder da seguinte forma:


    // Ação 18
    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 há ligação automática de dados;
  • linha 4: criamos nós próprios uma instância do modelo da ação. É aqui que poderíamos utilizar um construtor diferente;
  • linha 5: inicializamos o modelo com as informações do pedido. É o ASP.NET MVC que realiza esta tarefa. Faz-o da mesma forma que o teria feito se o modelo tivesse sido passado como parâmetro;
  • linha 6: estamos agora na mesma situação que na ação [Action12].

Eis um exemplo de execução:

 

4.12. Conclusion

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

Uma solicitação [1] transporta consigo diversas informações que ASP.NET MVC apresenta a [2a] para a ação sob a forma de um modelo a que chamámos modelo de ação.

  • o pedido HTTP do cliente chega a [1];
  • em [2], as informações contidas na solicitação são transformadas no modelo de ação [3];
  • em [4], a ação, a partir desse modelo, irá gerar uma resposta. Esta terá duas componentes: uma vista V [6] e o modelo M dessa 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 da vista [5] é o M e a vista [6] é o V.

Este capítulo analisou os mecanismos de ligação entre as informações transportadas pela solicitação, que são, por natureza, cadeias de caracteres, e o modelo da ação, que pode ser uma classe com propriedades de vários tipos. Vimos também que era possível verificar a validade do modelo apresentado à ação. Por fim, vimos como alargar este modelo aos dados de âmbito [Session] e [Application].

Vamos agora centrar-nos na fase final da cadeia de processamento da requisição [1]: a criação da vista [6] e do seu modelo [5]. Estes dois elementos são gerados pela ação [4].