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:
- 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:
- 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:
![]()
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].
























































