4. El modelo de una acción
Volvamos a la arquitectura de una aplicación ASP.NET MVC:
![]() |
En el capítulo anterior, analizamos el proceso que lleva la solicitud [1] al controlador y a la acción [2a] que la procesarán, un mecanismo que se denomina enrutamiento. Además, hemos presentado las diferentes respuestas que una acción puede enviar al navegador. Hasta ahora hemos presentado acciones que no aprovechaban la solicitud que se les presentaba. Una solicitud [1] lleva consigo diversa información que ASP.NET y MVC presentan a la acción en forma de modelo. No hay que confundir este término con la plantilla M de una vista V [2c], que es generada por la acción:
![]() |
- la solicitud HTTP del cliente llega a [1];
- en [2], la información contenida en la solicitud se transformará en la plantilla de acción [3] —una clase, a menudo, aunque no necesariamente, que servirá de entrada para la acción [4]—;
- en [4], la acción, a partir de este modelo, generará una respuesta. Esta tendrá dos componentes: una vista V [6] y el modelo M de dicha vista [5];
- la vista V [6] utilizará su modelo M [5] para generar la respuesta HTTP destinada al cliente.
En la plantilla MVC, la acción [4] forma parte del C (controlador), la plantilla de la vista [5] es el M y la vista [6] es el V.
En este capítulo se analizan los mecanismos de vinculación entre la información transportada por la solicitud —que, por naturaleza, son cadenas de caracteres— y el modelo de la acción, que puede ser una clase con propiedades de diversos tipos.
4.1. Inicialización de los parámetros de la acción
Añadimos a la solución existente un nuevo proyecto [1] basado en ASP.NET y MVC:
![]() |
![]() |
- en [2], el nombre del nuevo proyecto;
- en [3, 4], elegimos un proyecto base ASP.NET MVC;
- en [5], el nuevo proyecto.
Convertiremos el nuevo proyecto en el proyecto de inicio de la solución.
Tal y como se hizo en el apartado 3.1, creamos un controlador denominado [First] [1]:
![]() |
En este controlador, creamos la siguiente acción [Action01]:
using System.Web.Mvc;
namespace Exemple_02.Controllers
{
public class FirstController : Controller
{
// Acción01
public ContentResult Action01(string nom)
{
return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom));
}
}
}
La novedad se encuentra en la línea 8: el método [Action01] tiene un parámetro. En este capítulo nos centramos en las diferentes formas de inicializar los parámetros de una acción. El parámetro [nom] anterior se inicializa en orden con los siguientes valores:
Request.Form["nom"] | un parámetro denominado [nom] enviado por un comando POST |
RouteData.Values["nom"] | un elemento de URL denominado [nom] |
Request.QueryString["nom"] | un parámetro denominado [nom] enviado por un comando GET |
Request.Files["nom"] | un archivo subido llamado [nom] |
Analicemos estos diferentes casos. Solicitemos directamente en el navegador el URL [/First/Action01?nom=someone]. Obtenemos la siguiente respuesta:
![]() |
La solicitud HTTP del navegador fue la siguiente:
- línea 1: la solicitud es un GET. El URL solicitado incluye el parámetro [nom]. En el lado del servidor, la solicitud llega a la acción [Action01], que tiene la siguiente firma:
public ContentResult Action01(string nom)
Para asignar un valor al parámetro «nombre», ASP.NET MVC prueba sucesivamente y en el orden indicado los valores Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"]. Se detiene en cuanto encuentra un valor. El parámetro [nom], integrado en el URL del GET, ha sido colocado por el framework en Request.QueryString["nom"]. Con este valor, [someone], se inicializará el parámetro [nom] de [Action01]. A continuación, se ejecuta el código de [Action01]:
return Content(string.Format("Contrôleur=First, Action=Action01, nom={0}", nom), "text/plain", Encoding.UTF8);
Este código proporciona la respuesta enviada al cliente:
![]() |
Nota: el mecanismo de vinculación de parámetros no distingue entre mayúsculas y minúsculas. Por lo tanto, si nuestra acción se define como:
public ContentResult Action01(string NOM)
y el parámetro pasado es [?NoM=zébulon], la vinculación se llevará a cabo correctamente. El parámetro [NOM] de [Action01] recibirá el valor [zébulon].
Ahora, solicitemos el mismo URL con un POST. Para ello, utilizamos la aplicación [Advanced Rest Client]:
![]() |
- en [1], el URL solicitado;
- en [2], se utilizará el comando POST;
- en [3], los parámetros de POST.
Enviemos esta solicitud y veamos los registros de HTTP. La solicitud HTTP es la siguiente:
![]() |
- en [1], el POST;
- en [2], los parámetros de POST. Técnicamente, se han enviado tras los encabezados de HTTP, después de la línea en blanco que indica el final de dichos encabezados;
- en [3], la respuesta obtenida. Se recupera correctamente el parámetro [nom] del POST. Entre los valores probados para el parámetro «nombre» Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], fue el primero el que funcionó.
Ahora, modifiquemos la ruta por defecto en [App_Start/RouteConfig]. Actualmente, esta ruta es la siguiente:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Cambiémosla por:
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{nom}",
defaults: new { controller = "Home", action = "Index", nom = UrlParameter.Optional }
);
- En la línea 3, hemos denominado [nom] al tercer elemento de una ruta;
- en la línea 4, este elemento se declara opcional.
Ahora, recompilemos la aplicación y solicitemos el URL [/First/Action01/zébulon] directamente en el navegador. Obtenemos la siguiente respuesta:
![]() |
Entre los valores probados para el parámetro «nombre»: Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], fue el segundo el que funcionó.
Hagamos la misma consulta con un POST y un [Advanced Rest Client]:
![]() |
- en [1], le hemos asignado un valor al elemento {nombre} de la ruta;
- en [2], añadimos un parámetro [nom] a la solicitud enviada;
- la respuesta obtenida es [3].
Entre los valores probados para el parámetro [nom], Request.Form["nom"], RouteData.Values["nom"], Request.QueryString["nom"], Request.Files["nom"], dos eran válidos: los dos primeros. Se utilizó el primero.
4.2. Comprobar la validez de los parámetros de la acción
Si una acción tiene un parámetro llamado [p], ASP.NET y MVC intentarán asignarle uno de los valores Request.Form["p"], RouteData.Values["p"], Request.QueryString["p"], Request.Files["p"]. Los tres primeros valores son cadenas de caracteres. Si el parámetro [p] no es del tipo [string], pueden surgir problemas.
Creemos la siguiente acción nueva:
// Acción02
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);
}
- En la línea 2, la acción [Action02] admite un parámetro denominado [age] de tipo int. Será necesario que la cadena de caracteres recuperada sea convertible a int.
Solicitemos la acción URL [http://localhost:55483/First/Action02?age=21]. Obtenemos la siguiente página:
![]() |
Solicitemos el URL y el [http://localhost:55483/First/Action02?age=21x]. Obtenemos la siguiente página:
![]() |
En esta ocasión, se ha obtenido una página de error. Resulta interesante examinar los encabezados HTTP enviados por el servidor en este caso:
- línea 1: el servidor ha respondido con un código [500 Internal Server Error] y ha enviado una página HTML (línea 3) de 12 438 bytes (línea 5) para explicar las posibles causas de este error.
Creemos ahora la siguiente acción [Action03]:
// Acción03
public ContentResult Action03(int? age)
{
...
}
[Action03] es idéntica a [Action02], salvo que se ha cambiado el tipo del parámetro [age] a int?, lo que significa entero o nulo.
Solicitemos el URL [http://localhost:55483/First/Action03?age=21x]. Obtenemos la siguiente página:
![]() |
ASP.NET MVC no ha podido convertir [21x] al tipo «int». Por lo tanto, ha asignado el valor «null» al parámetro [age], tal y como permite su tipo «int?». Sin embargo, es posible saber si el parámetro ha podido recibir un valor de la consulta o no.
Creamos la siguiente acción nueva [Action04]:
// Acción04
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);
}
- línea 2: se ha conservado el tipo [int?]. Esto permite, en particular, que la consulta no proporcione el parámetro [age], que, por lo tanto, recibe el valor nulo;
- línea 4: se comprueba si el modelo de la acción es válido. El modelo de la acción está formado por el conjunto de sus parámetros, en este caso [age]. El modelo es válido si todos los parámetros han podido obtener un valor de la solicitud o bien el valor nulo si el tipo del parámetro lo permite;
- línea 5: se añade el valor de la variable [valide] al texto enviado al cliente.
Solicitemos URL [http://localhost:55483/First/Action04?age=21x]. Obtenemos la siguiente página:
![]() |
ASP.NET MVC no ha podido convertir [21x] al tipo int. Por lo tanto, ha asignado el valor nulo al parámetro [age], tal y como permite su tipo int?. Sin embargo, se han producido errores de conversión, tal y como muestra el valor de [valide].
Es posible que aparezca el mensaje de error asociado a una conversión fallida. Analicemos la siguiente acción nueva:
// Acción05
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);
}
La novedad está en la línea 4. En ella se llama a un método privado, [getErrorMessagesFor], al que se le pasa el estado del modelo de la acción. Este método devuelve una cadena de caracteres que reúne los mensajes de todos los errores que se han producido. Este método es el siguiente:
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;
}
- línea 1: el parámetro efectivo [ModelState] pasado al método es de tipo [ModelStateDictionary];
- línea 3: una lista de mensajes de error, vacía al principio;
- línea 5: se comprueba si el estado pasado como parámetro es válido o no. Si no lo es, se agrupan todos los mensajes de error en una única cadena de caracteres;
- línea 7: el tipo [ModelStateDictionary] tiene una propiedad [Values] que es una colección de tipos [ModelState]. Hay un [ModelState] por cada elemento del modelo. Por ejemplo:
- ModelState["age"]: el estado del modelo de la acción para el parámetro [age],
- ModelState["age"].Errors: la colección de errores para este parámetro. Los errores son del tipo [ModelError],
- ModelState["age"].Errors[i].ErrorMessage: el posible mensaje de error n.º i para el parámetro [age] de la plantilla
- ModelState["age"].Errors[i].Exception: la excepción del error n.º i de la colección de errores del parámetro [age],
- ModelState["age"].Errors[i].Exception.InnerException: la causa de esta excepción,
- ModelState["age"].Errors[i].Exception.InnerException.Message: el mensaje de la causa de la excepción;
- línea 9: se recorre la colección [Errors] de un [ModelState] concreto;
- línea 11: se recupera el mensaje de error de un [ModelError] concreto y se añade a la lista de mensajes de error de la línea 3;
- líneas 14-17: se agrupan los elementos de la lista de mensajes de error en una única cadena de caracteres.
El método [getErrorMessageFor] de la línea 11 es el siguiente:
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;
}
- línea 1: se recibe un tipo [ModelError] que encapsula un error en uno de los elementos del modelo de la acción. Se busca el mensaje de error en tres lugares diferentes:
- en [ModelError].ErrorMessage, líneas 3-6;
- en [ModelError].Exception.Message, líneas 7-10;
- en [ModelError].Exception.InnerException.Message, líneas 11-14;
Durante las pruebas, se observa que el mensaje de error se encuentra en estos tres lugares según la naturaleza del elemento del modelo. Debe de haber una regla que permita obtener con certeza el mensaje de error asociado a un elemento del modelo, pero yo no la conozco. Por lo tanto, lo busco en los distintos lugares donde puedo encontrarlo y lo hago siguiendo un orden determinado. En cuanto se encuentra un mensaje que no esté vacío, se devuelve.
Solicitemos el URL [http://localhost:55483/First/Action05?age=21x]. Obtenemos la siguiente página:
![]() |
4.3. Una acción con varios parámetros
Consideremos la siguiente acción nueva:
// Acción06
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);
}
- línea 2: tenemos dos parámetros, [poids] y [age].
Las reglas descritas anteriormente se aplican ahora a ambos parámetros. A continuación se muestran algunos ejemplos de ejecución:
![]() |
![]() |
4.4. Utilizar una clase como plantilla de una acción
Definamos una clase que servirá de plantilla para una acción. La colocamos en la carpeta [Models] [1].
![]() |
Su código será el siguiente:
namespace Exemple_02.Models
{
public class ActionModel01
{
public double? Poids { get; set; }
public int? Age { get; set; }
}
}
Nuestra clase tiene como propiedades automáticas los dos parámetros [Poids] y [Age] que hemos visto anteriormente. Esta clase será el parámetro de entrada de la acción [Action07]:
// Acción07
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);
}
- línea 2: el modelo de la acción es una instancia de tipo [ActionModel01].
Repasemos los mismos dos ejemplos que antes:
![]() |
![]() |
Cabe destacar que la vinculación de los parámetros no distingue entre mayúsculas y minúsculas. Los parámetros de la consulta eran [age] y [poids]. Estos alimentaron las propiedades [Age] y [Poids] de la clase [ModelAction01].
Por otra parte, hasta ahora hemos utilizado las consultas HTTP y [GET]. Demostraremos que las consultas [POST] tienen el mismo comportamiento. Para ello, volveremos a utilizar la aplicación [Advanced Rest Client]:
![]() |
- en [1], la URL solicitada;
- en [2], se solicitará mediante un comando POST;
- en [3], los parámetros de POST.
Se obtiene la misma respuesta que con el GET:
![]()
4.5. Modelo de la acción con restricciones de validez - 1
Con el modelo anterior:
namespace Exemple_02.Models
{
public class ActionModel01
{
public double? Poids { get; set; }
public int? Age { get; set; }
}
}
los parámetros [poids] y [age] pueden no aparecer en la consulta. En este caso, las propiedades [Poids] y [Age] reciben el valor [null] y no se señala ningún error. Podría ser conveniente transformar el modelo de la siguiente manera:
namespace Exemple_02.Models
{
public class ActionModel01
{
public double Poids { get; set; }
public int Age { get; set; }
}
}
En las líneas 5 y 6, las propiedades [Poids] y [Age] ya no pueden tener el valor [null]. Veamos qué ocurre con este nuevo modelo cuando los parámetros [poids] y [age] no aparecen en la consulta.
![]() |
No se han producido errores y las propiedades [Poids] y [Age] han conservado su valor de inicialización: 0. ASP.NET MVC:
- se ha creado una instancia del modelo mediante un «new ActionModel01». Es en este momento cuando las propiedades [Poids] y [Age] han recibido el valor 0;
- no asignó ningún valor a estas dos propiedades, ya que no había ningún parámetro con ese nombre.
El primer modelo nos permite comprobar la ausencia de un parámetro: la propiedad correspondiente toma entonces el valor [null]. El segundo no nos lo permite. Es posible añadir otras restricciones de validación además del simple tipo de los parámetros. A continuación las presentaremos.
Consideremos el siguiente nuevo modelo de acción:
![]() |
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; }
}
}
- línea 6: indica que el campo [Poids] es obligatorio;
- línea 7: indica que el campo [Poids] debe estar dentro del intervalo [1,200];
- línea 9: indica que el campo [Age] es obligatorio;
- línea 7: indica que el campo [Age] debe estar dentro del intervalo [1,150];
La acción que utilice esta plantilla será la siguiente acción [Action08]:
// Acción08
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);
}
- línea 2: la acción recibe una instancia del modelo [ActionModel02];
Hagamos algunas pruebas:
![]() |
![]() |
![]() |
![]() |
Los errores se detectan correctamente. Ahora, modifiquemos el modelo de la siguiente manera:
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; }
}
}
En las líneas 8 y 11, las propiedades ya no pueden tener el valor [null]. Compilemos y volvamos a realizar la prueba sin parámetros:
![]() |
La ausencia de parámetros ha hecho que las propiedades [Poids] y [Age] mantengan el valor que adquirieron al instanciar el modelo: 0. A continuación, se lleva a cabo la validación. El atributo [Required] cumple entonces los requisitos. Se observa que el mensaje de error anterior corresponde al atributo [Range]. Por lo tanto, para comprobar la presencia de un parámetro, es necesario que la propiedad asociada sea nullable, es decir, que pueda recibir el valor null.
Volvamos al modelo inicial [ActionModel02] y consideremos una acción cuyo modelo esté formado por una instancia [ActionModel02] y un tipo [DateTime] nullable:
// Acción09
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);
}
Hagamos algunas pruebas:
![]() |
No se han pasado parámetros a la acción. Los atributos [Required] de las propiedades [Poids] y [Age] han cumplido su función. La fecha, por su parte, ha recibido el valor null y no se ha señalado ningún error.
Ahora pasamos parámetros no válidos:
![]() |
Ahora pasamos valores válidos:
![]() |
Analicemos otras restricciones de validez. El nuevo modelo de acción es el siguiente:
![]() |
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; }
}
}
- línea 6: el atributo [Required], esta vez con un mensaje de error que establecemos nosotros mismos;
- línea 7: el atributo [EMailAddress] exige que el campo [Email] contenga una dirección de correo electrónico con un formato válido;
- línea 11: el atributo [RegularExpression] exige que el campo [Jour] contenga una cadena de uno o dos dígitos. El primer parámetro es la expresión regular que debe verificar el campo;
- línea 15: el atributo [MaxLength] exige que el campo [Info1] tenga un máximo de 4 caracteres;
- línea 19: el atributo [MinLength] exige que el campo [Info2] tenga al menos 2 caracteres;
- líneas 23-24: los atributos [MaxLength] y [MinLength], combinados, establecen que el campo [Info3] tenga exactamente 4 caracteres;
La acción [Action10] utilizará este modelo:
// Acción 10
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);
}
Hagamos algunas pruebas con esta acción.
En primer lugar, sin parámetros:
![]() |
A continuación, con parámetros no válidos:
![]() |
A continuación, con parámetros válidos:
![]() |
4.6. Modelo de la acción con restricciones de validez - 2
Presentamos otras restricciones de integridad. El nuevo modelo de la acción será la siguiente clase [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; }
}
}
- línea 8: indica que el campo anotado debe ser un URL válido;
- línea 13: exige que las propiedades [Info1] y [Info2] tengan el mismo valor;
- línea 16: exige que el campo anotado sea un número de tarjeta de crédito válido.
La acción que utilice esta plantilla será la siguiente:
// Acción11
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 probar la acción [Action11], utilizamos la aplicación [Advanced Rest Client]:
![]() |
- en [1], el URL de la acción [Action11];
- en [2], se solicitará este URL junto con un POST;
- en [3], se selecciona la pestaña [Form];
- en [4], los valores de los cuatro parámetros esperados. Esta inicialización es una facilidad que ofrece [ARC]. Los parámetros realmente enviados pueden verse en la pestaña [Raw] [5];
![]() |
- en [6], los parámetros de POST.
Para esta consulta, se recibe la siguiente respuesta:
![]() |
Pasemos a los parámetros no válidos:
![]() |
Entonces obtenemos la siguiente respuesta:
4.7. Modelo de la acción con restricciones de validez - 3
A veces, las restricciones de integridad disponibles no son suficientes. En ese caso, podemos crear las nuestras propias. En concreto, podemos utilizar un modelo que implemente la interfaz [IValidatableObject]. En este caso, añadimos nuestras propias comprobaciones del modelo en el método [Validate] de dicha interfaz. Veamos un ejemplo. El nuevo modelo de la acción será la siguiente clase [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;
}
}
}
- línea 6: el modelo implementa la interfaz [IValidatableObject];
- línea 10: el método [Validate] de esta interfaz. Devuelve una colección de elementos de tipo [ValidationResult]. Este tipo encapsula los errores que queremos señalar;
- línea 9: una tasa válida es una tasa <4,2 o > 6,7;
- línea 12: se crea una lista vacía de elementos de tipo [ValidationResult];
- línea 13: se comprueba la validez de la propiedad [Taux];
- líneas 14-17: si la propiedad [Taux] no es válida, se añade un elemento de tipo [ValidationResult] a la lista de resultados. El primer parámetro es un mensaje de error. El segundo parámetro, opcional, es una colección de las propiedades afectadas por este error.
La acción que utilice esta plantilla será la siguiente:
// Acción12
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);
}
A continuación se muestra un ejemplo de ejecución:
![]() |
4.8. Plantilla de acción de tipo Tabla o Lista
Consideremos la siguiente acción [Action13]:
// Acción13
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);
}
- línea 2: el modelo de la acción está formado por una tabla de [string]. Nos permite recuperar un parámetro denominado [data] que puede aparecer varias veces en los parámetros de la consulta, como en [?data=data1&data=data2&data=data3]. Los distintos parámetros [data] de la solicitud alimentarán la matriz [data] del modelo de la acción. Este caso se da con las listas de selección múltiple. El navegador envía entonces los distintos valores seleccionados por el usuario, con el mismo nombre de parámetro.
He aquí un ejemplo:
![]() |
El modelo también puede ser una lista:
// Acción 14
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);
}
En este caso, la plantilla es una lista de números enteros (línea 2). He aquí una primera ejecución:
![]() |
y una segunda:
![]() |
4.9. Filtrado de un modelo de acción
A veces disponemos de un modelo, pero queremos que la consulta HTTP solo inicialice determinados elementos del modelo. Consideremos el siguiente modelo de acción [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; }
}
}
- líneas 9-10: el parámetro [info1] es obligatorio;
- línea 6: el parámetro [info2] de la línea 12 queda excluido de la vinculación de la consulta HTTP con su modelo.
La acción será la siguiente [Action15]:
// Acción 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);
}
A continuación se muestra un ejemplo de ejecución:
![]() |
- en [1]: se pasa el parámetro [info2] a URL;
- en [2]: la propiedad [Info2] del modelo de la acción se ha dejado vacía.
4.10. Ampliar el modelo de enlace de datos
Volvamos a la arquitectura de ejecución de una acción:
![]() |
La clase de la acción se instancia al inicio de la solicitud del cliente y se destruye al final de la misma. Por lo tanto, no puede utilizarse para almacenar datos entre dos solicitudes, aunque se llame a ella repetidamente. Es posible que se desee almacenar dos tipos de datos:
- datos compartidos por todos los usuarios de la aplicación web. Por lo general, se trata de datos de solo lectura. Se utilizan tres archivos para implementar este intercambio de datos:
- [Web.Config]: el archivo de configuración de la aplicación
- [Global.asax, Global.asax.cs]: permite definir una clase, denominada clase global de la aplicación, cuya vida útil es la misma que la de la aplicación, así como controladores para determinados eventos de dicha aplicación.
La clase global de la aplicación permite definir datos que estarán disponibles para todas las consultas de todos los usuarios.
- los datos compartidos por las solicitudes de un mismo cliente. Estos datos se almacenan en un objeto denominado «Sesión». Se habla entonces de «sesión de cliente» para referirse a la memoria del cliente. Todas las solicitudes de un cliente tienen acceso a esta sesión. Pueden almacenar y leer información en ella.
![]() |
Arriba mostramos los tipos de memoria a los que tiene acceso una acción:
- la memoria de la aplicación, que suele contener datos de solo lectura y a la que pueden acceder todos los usuarios;
- la memoria de un usuario concreto, o sesión, que contiene datos de lectura y escritura y a la que pueden acceder las solicitudes sucesivas de un mismo usuario;
- aunque no aparece en la imagen anterior, existe una memoria de solicitud, o contexto de solicitud. La solicitud de un usuario puede ser procesada por varias acciones sucesivas. El contexto de la solicitud permite que una acción 1 transmita información a una acción 2.
Veamos un primer ejemplo que ilustra estas diferentes memorias:
En primer lugar, modificamos el archivo [Web.config] del proyecto [Exemple-02] de la siguiente manera:
<appSettings>
<add key="webpages:Version" value="2.0.0.0" />
...
<add key="infoAppli1" value="infoAppli1"/>
</appSettings>
Añadimos la línea 4, que asocia el valor [infoAppli1] a la clave [infoAppli1]. Este será nuestro dato de ámbito [Application]: será accesible para todas las consultas de todos los usuarios.
A continuación, modificamos el método [Application_Start] del archivo [Global.asax]. Este método se ejecuta una sola vez al iniciar la aplicación. Es aquí donde hay que utilizar el archivo [Web.config]:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
WebApiConfig.Register(GlobalConfiguration.Configuration);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
// Inicialización de la aplicación
Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
}
Añadimos la línea 10. Esta línea realiza dos acciones:
- recoge el valor de la clave [infoAppli1] del archivo [Web.config] mediante la clase [System.Configuration.ConfigurationManager];
- lo guarda en el diccionario [HttpApplication.Application], asociado a la clave [infoAppli1]. Todas las acciones tienen acceso a este diccionario.
En el mismo archivo [Gloabal.asax], se añade el siguiente método [Session_Start]:
protected void Session_Start()
{
// Inicialización del contador
Session["compteur"] = 0;
}
El método [Session_Start] se ejecuta para cada nuevo usuario. ¿Qué es un nuevo usuario? A un usuario se le «sigue» mediante un token de sesión. Este token:
- creado por el servidor web y enviado al nuevo usuario en los encabezados HTTP de la primera respuesta que se le envía;
- el navegador del usuario lo reenvía con cada nueva solicitud que realiza. Esto permite al servidor reconocer al usuario y gestionar un espacio de memoria para él, lo que se denomina «sesión del usuario».
El servidor web reconoce que se trata de un nuevo usuario cuando este no le envía un token de sesión. En ese caso, el servidor le crea uno.
En la línea 4 anterior, se introduce en la sesión del usuario un contador que se incrementará con cada solicitud de dicho usuario. Esto ilustrará la memoria asociada a un usuario. La clase [Session] se utiliza como un diccionario (línea 4).
Una vez hecho esto, escribimos la siguiente acción [Action16]:
// Acción 16
public ContentResult Action16()
{
// se recupera el contexto de la solicitud HTTP
HttpContextBase contexte = ControllerContext.HttpContext;
// se recuperan los datos del ámbito «Aplicación»
string infoAppli1 = contexte.Application["infoAppli1"] as string;
// y la del ámbito de sesión
int? compteur = contexte.Session["compteur"] as int?;
compteur++;
contexte.Session["compteur"] = compteur;
// la respuesta al cliente
string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
return Content(texte, "text/plain", Encoding.UTF8);
}
- línea 5: recuperamos el contexto de la solicitud HTTP que se está procesando. Este contexto nos dará acceso a los datos de ámbito [Application] y [Session];
- línea 7: se recupera la información del ámbito [Application];
- línea 9: se recupera el contador de la sesión;
- líneas 10-11: se incrementa y se vuelve a introducir en la sesión;
- líneas 13-14: se envían ambos datos al cliente.
A continuación se muestran algunos ejemplos de ejecución:
Se solicita [Action16] una primera vez, [1] y, a continuación, se actualiza la página: [F5] dos veces y [2]:
![]() |
En [2], el cliente ha realizado un total de tres solicitudes. En cada una de ellas, ha podido recuperar el contador actualizado por la solicitud anterior.
Para simular un segundo usuario, utilizamos un segundo navegador para solicitar el mismo URL:
![]() |
En [3], el segundo usuario recupera correctamente la misma información de alcance de [Application], pero tiene su propio contador de alcance [Session].
Volvamos al código de la acción [Action16]:
// Acción 16
public ContentResult Action16()
{
// se recupera el contexto de la solicitud HTTP
HttpContextBase contexte = ControllerContext.HttpContext;
// se recuperan los datos del ámbito de la aplicación
string infoAppli1 = contexte.Application["infoAppli1"] as string;
// y la información del ámbito de sesión
int? compteur = contexte.Session["compteur"] as int?;
compteur++;
contexte.Session["compteur"] = compteur;
// la respuesta al cliente
string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
return Content(texte, "text/plain", Encoding.UTF8);
}
Uno de los objetivos del marco ASP.NET MVC es permitir que los controladores y las acciones se puedan probar de forma aislada, sin necesidad de un servidor web. Sin embargo, en la línea 5 vemos que el contexto de la solicitud HTTP es necesario para recuperar la información del ámbito [Application] y del ámbito [Session]. Se propone crear una nueva acción [Action17] que recibiría los datos de los ámbitos [Application] y [Session] como parámetros:
// Acción 17
public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
{
// se recuperan los datos del ámbito «Aplicación»
string infoAppli1 = applicationData.InfoAppli1;
// y la del ámbito «Sesión»
int compteur = sessionData.Compteur++;
// la respuesta al cliente
string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
return Content(texte, "text/plain", Encoding.UTF8);
}
El código ya no tiene ninguna dependencia de la consulta HTTP. Por lo tanto, se puede probar de forma aislada de un servidor web.
Veamos cómo hacerlo. En primer lugar, debemos crear las clases [ApplicationModel] y [SessionModel], que encapsularán, respectivamente, los datos de ámbito [Application] y [Session]. Son las siguientes:
![]() |
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;
}
}
}
A continuación, debemos modificar los métodos [Application_Start] y [Session_Start] del archivo [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);
// Inicialización de la aplicación - caso 1
Application["infoAppli1"] = ConfigurationManager.AppSettings["infoAppli1"];
// inicialización de la aplicación - caso 2
ApplicationModel data=new ApplicationModel();
data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
Application["data"] = data;
}
protected void Session_Start()
{
// inicialización del contador - caso 1
Session["compteur"] = 0;
// Inicialización del contador - caso 2
Session["data"] = new SessionModel();
}
}
- línea 14: se crea una instancia de [ApplicationModel];
- línea 15: se inicializa;
- línea 16: y se coloca en el diccionario de [Application], asociada a la clave [data]. [Application] es una propiedad de la clase [HttpApplication] de la línea 1;
- línea 24: se crea una instancia de [SessionModel] y se coloca en el diccionario de [Session], asociada a la clave [data]. [Session] es una propiedad de la clase [HttpApplication] de la línea 1;
Si nos atenemos a lo que hemos visto hasta ahora, la firma
public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
significa que la consulta HTTP procesada por la acción deberá incluir parámetros denominados [applicationData] y [sessionData]. Pero no será así. Debemos crear un nuevo modelo de enlace de datos para que, cuando una acción reciba como parámetro un tipo:
- [ApplicationModel], se le proporcionen los datos de ámbito [Application] y de clave [data];
- [SessionModel], siempre que se le proporcionen los datos de ámbito [Session] y de clave [data].
Para ello, es necesario crear clases que implementen la interfaz [IModelBinder].
Empezamos creando una carpeta [Infrastructure] en el proyecto [Exemple-02]:
![]() |
En ella creamos la siguiente clase [ApplicationModelBinder]:
using System.Web.Mvc;
namespace Exemple_02.Infrastructure
{
public class ApplicationModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// se devuelven los datos del ámbito [Application]
return controllerContext.RequestContext.HttpContext.Application["data"];
}
}
}
- línea 5: la clase implementa la interfaz [IModelBinder]. Para entender su código, hay que saber que se llamará cada vez que una acción tenga un parámetro de tipo [ApplicationModel]. Esta vinculación [ApplicationModel] --> [ApplicationModelBinder] se establecerá al iniciar la aplicación, en el método [Application_Start] de [Global.asax];
- línea 7: el único método de la interfaz [IModelBinder];
- línea 7: el parámetro de tipo [ControllerContext] nos da acceso a la consulta HTTP que se está procesando;
- línea 7: el parámetro de tipo [ModelBindingContext] nos da acceso a información sobre el modelo que se va a construir, en este caso el tipo [ApplicationModel];
- línea 7: el resultado de [BindModel] es el objeto que se asignará al parámetro vinculado, en este caso un parámetro de tipo [ApplicationModel];
- línea 10: nos limitamos a devolver el objeto con el ámbito [Application] y la clave [data].
La clase [SessionModelBinder] sigue el mismo esquema:
using System.Web.Mvc;
namespace Exemple_02.Infrastructure
{
public class SessionModelBinder : IModelBinder
{
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
// se devuelven los datos del ámbito [Session]
return controllerContext.HttpContext.Session["data"];
}
}
}
Ahora solo nos queda asociar cada uno de los modelos [XModel] con su binder y [XModelBinder]. Esto se hace en el método [Application_Start] de [Global.asax]:
protected void Application_Start()
{
....
// Inicialización de la aplicación - caso 2
ApplicationModel data=new ApplicationModel();
data.InfoAppli1=ConfigurationManager.AppSettings["infoAppli1"];
Application["data"] = data;
// Enlazadores de modelos
ModelBinders.Binders.Add(typeof(ApplicationModel), new ApplicationModelBinder());
ModelBinders.Binders.Add(typeof(SessionModel), new SessionModelBinder());
}
- línea 9: cuando una acción tenga un parámetro de tipo [ApplicationModel], se llamará al método [ApplicationModelBinder.Bind]. Sabemos que devuelve el dato de ámbito [Application] asociado a la clave [data];
- línea 10: lo mismo ocurre con el tipo [SessionModel].
Volvamos a nuestra acción [Action17]:
// Acción 17
public ContentResult Action17(ApplicationModel applicationData, SessionModel sessionData)
{
// se recuperan los datos del ámbito «Aplicación»
string infoAppli1 = applicationData.InfoAppli1;
// y la del ámbito «Sesión»
sessionData.Compteur++;
int compteur = sessionData.Compteur;
// la respuesta al cliente
string texte = string.Format("infoAppli1={0}, compteur={1}", infoAppli1, compteur);
return Content(texte, "text/plain", Encoding.UTF8);
}
- línea 2: cuando se llame a [Action17], recibirá como
- primer parámetro: el dato de ámbito [Application] asociado a la clave [data],
- segundo parámetro: el dato de ámbito [Session] asociado a la clave [data];
Estos dos datos pueden ser tan complejos como se desee y agrupar, en el caso de uno, todos los datos del ámbito [Application] y, en el caso del otro, todos los datos del ámbito [Session].
A continuación se muestra un ejemplo de ejecución de la acción [Action17]:
![]() |
4.11. Enlace tardío del modelo de la acción
Hemos escrito la siguiente acción [Action12]:
// Acción 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, ASP.NET MVC:
- crea una instancia de tipo [ActionModel05] utilizando su constructor sin parámetros;
- la inicializa con la información de la solicitud que tiene el mismo nombre (sin distinción entre mayúsculas y minúsculas) que una de las propiedades de [ActionModel05].
A veces, este comportamiento no nos conviene. Este es el caso, en particular, cuando queremos utilizar un constructor concreto del modelo de la acción. En ese caso, podemos proceder de la siguiente manera:
// Acción 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);
}
- línea 2: la acción ya no recibe parámetros. Por lo tanto, ya no hay enlace automático de datos;
- línea 4: creamos nosotros mismos una instancia del modelo de la acción. Aquí es donde podríamos utilizar un constructor diferente;
- línea 5: se inicializa el modelo con la información de la solicitud. Son ASP.NET y MVC los que realizan esta tarea. Lo hacen de la misma forma que lo habrían hecho si el modelo se hubiera pasado como parámetro;
- línea 6: ahora nos encontramos en la misma situación que en la acción [Action12].
He aquí un ejemplo de ejecución:
![]() |
4.12. Conclusion
Volvamos a la arquitectura de una aplicación ASP.NET MVC:
![]() |
Una solicitud [1] transporta diversa información que ASP.NET MVC presenta a la acción [2a] en forma de un modelo que hemos denominado «modelo de acción».
![]() |
- la solicitud HTTP del cliente llega a [1];
- en [2], la información contenida en la solicitud se transforma en el modelo de acción [3];
- en [4], la acción, a partir de este modelo, generará una respuesta. Esta tendrá dos componentes: una vista V [6] y el modelo M de dicha vista [5];
- la vista V [6] utilizará su modelo M [5] para generar la respuesta HTTP destinada al cliente.
En la plantilla MVC, la acción [4] forma parte del C (controlador), la plantilla de la vista [5] es el M y la vista [6] es el V.
En este capítulo se han estudiado los mecanismos de vinculación entre la información transportada por la solicitud —que, por naturaleza, son cadenas de caracteres— y el modelo de la acción, que puede ser una clase con propiedades de diversos tipos. También hemos visto que es posible verificar la validez del modelo presentado a la acción. Por último, hemos visto cómo ampliar este modelo a los datos de ámbito [Session] y [Application].
Ahora nos centraremos en la parte final de la cadena de procesamiento de la consulta [1]: la creación de la vista [6] y de su modelo [5]. Estos dos elementos son generados por la acción [4].
























































