Usar ViewData e implementar clases ViewModel
de Microsoft
Este es el paso 6 de un tutorial de la aplicación "NerdDinner" gratuito que le guía por el proceso de creación de una aplicación web pequeña, pero completa, con MVC 1 de ASP.NET.
En el paso 6 se muestra cómo habilitar la compatibilidad con escenarios de edición de formularios con más funciones y características y también se describen dos enfoques que se pueden usar para pasar datos de controladores a vistas: ViewData y ViewModel.
Si se usa MVC 3 de ASP.NET, es aconsejable seguir los tutoriales Introducción a MVC 3 o MVC Music Store.
Paso 6 de NerdDinner: ViewData y ViewModel
Se han tratado varios escenarios posteriores al formulario y se ha analizado cómo implementar la compatibilidad con la creación, actualización y eliminación (CRUD). Ahora se dará un paso más en la implementación de DinnersController, se habilitará la compatibilidad con escenarios más completos para la edición de formularios. También se analizarán dos enfoques que se pueden usar para usar datos de controladores en las vistas: ViewData y ViewModel.
Uso de datos de controladores en las plantillas de vista
Una de las características definitorias del patrón de MVC es que ayuda a aplicar una estricta "separación de los problemas" de los distintos componentes de las aplicaciones. Los modelos, los controladores o las vistas tienen roles y responsabilidades bien definidos, y las distintas maneras de comunicarse entre ellos también están perfectamente definidas, lo que ayuda a promover la capacidad de prueba y la reutilización del código.
Si una clase Controller decide volver a representar una respuesta HTML a un cliente, se encarga de pasar explícitamente a la plantilla de vista todos los datos necesarios para representar la respuesta. En ningún caso deben realizar las plantillas de vista ninguna recuperación de datos o lógica de aplicación (más bien deben limitarse a tener código de representación que se elimine del modelo o los datos que el controlador le haya pasado).
Los datos del modelo que la clase DinnersController pasa a las plantillas de vista son sencillos y directos, una lista de objetos Dinner en el caso de Index() y un único objeto Dinner en el caso de Details(), Edit(), Create() y Delete(). A medida que se incorporan funcionalidades dela interfaz de usuario a la aplicación, va a ser preciso pasar más elementos, además de los datos, para representar las respuestas HTML en las plantillas de vista. Por ejemplo, tanto en la vista de edición como en la de creación el campo "País" podría pasar de ser un cuadro de texto HTML a una lista desplegable. En lugar de codificar de forma rígida la lista desplegable que contiene los nombres de países y regiones en la plantilla de vista, es posible que se quieta generarla a partir de una lista de países y regiones admitidos que se rellena dinámicamente. Se necesitará una forma de pasar el objeto Dinner y la lista de países y regiones admitidos del controlador a las plantillas de vista.
Hay dos formas de hacerlo.
Uso del diccionario ViewData
La clase base Controller expone una propiedad diccionario "ViewData" que se puede usar para pasar elementos de datos adicionales de Controladores a Vistas.
Por ejemplo, para admitir el escenario en el que se desea que en la vista de edición el cuadro de texto "País" pase de ser un cuadro de texto HTML a una lista desplegable, podemos actualizar nuestro método de acción Edit() para que pase (además de un objeto Dinner) un objeto SelectList que se pueda usar como modelo de una lista desplegable "Países".
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
ViewData["Countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
El constructor de este objeto SelectList acepta una lista de países y regiones para rellenar la lista desplegable con, así como el valor seleccionado actualmente.
Luego se puede actualizar la plantilla de vista Edit.aspx para usar el método auxiliar Html.DropDownList(), en lugar del método Html.TextBox() que se usaba anteriormente:
<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>
El método auxiliar Html.DropDownList() utiliza dos parámetros. El primero es el nombre del elemento del formulario HTML que se va a generar. El segundo es el modelo "SelectList" que se pasa a través del diccionario ViewData. Se usa la palabra clave "as" de C# para convertir el tipo de dentro del diccionario en SelectList.
Y ahora, cuando se ejecute la aplicación y se acceda a la dirección URL /Dinners/Edit/1 desde el explorador, se apreciará que la interfaz de usuario de edición se ha actualizado, muestra una lista desplegable de países y regiones, en lugar de un cuadro de texto:
Como también se representa la plantilla de vista Edit desde el método Edit de HTTP-POST (en escenarios en que se producen errores), es aconsejable asegurarse de que también se actualiza este método para agregar SelectList a ViewData cuando la plantilla de vista se representa en escenarios de error:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
ViewData["countries"] = new SelectList(PhoneValidator.AllCountries, dinner.Country);
return View(dinner);
}
}
Ahora, el escenario de edición DinnersController admite un elemento DropDownList.
Uso de un patrón ViewModel
El enfoque del diccionario ViewData tiene la ventaja de ser bastante rápido y fácil de implementar. Sin embargo, a algunos desarrolladores no les gusta usar diccionarios basados en cadenas, ya que los errores tipográficos pueden provocar errores que no se detectarán en tiempo de compilación. El diccionario ViewData no tipado también requiere que se use bien el operador "as" bien la conversión cuando en una plantilla de vista se utiliza un lenguaje fuertemente tipado, como C#.
Se podría usar un enfoque, se conoce como el patrón "ViewModel". Cuando se usar este patrón, se crean clases fuertemente tipadas que están optimizadas para escenarios de vista concretos y que exponen propiedades para los valores dinámicos y el contenido que necesitan las plantillas de vista. Las clases de controlador pueden rellenar y pasar estas clases optimizadas para vistas a la plantilla de vista para que esta las use, lo que aporta seguridad de tipos, comprobación en tiempo de compilación y la funcionalidad IntelliSense del editor a las plantillas de vista.
Por ejemplo, para habilitar escenarios de edición de formularios de cena, se puede crear una clase "DinnerFormViewModel" como la siguiente que expone dos propiedades fuertemente tipadas: un objeto Dinner y el modelo SelectList necesario para rellenar la lista desplegable "Países":
public class DinnerFormViewModel {
// Properties
public Dinner Dinner { get; private set; }
public SelectList Countries { get; private set; }
// Constructor
public DinnerFormViewModel(Dinner dinner) {
Dinner = dinner;
Countries = new SelectList(PhoneValidator.AllCountries, dinner.Country);
}
}
Luego, se puede actualizar el método de acción Edit() para crear DinnerFormViewModel mediante el objeto Dinner que se recupera del repositorio y, después, se pasa a la plantilla de vista:
//
// GET: /Dinners/Edit/5
[Authorize]
public ActionResult Edit(int id) {
Dinner dinner = dinnerRepository.GetDinner(id);
return View(new DinnerFormViewModel(dinner));
}
Luego, se actualizara la nuestra plantilla de vista para que espere un elemento "DinnerFormViewModel", en lugar de un objeto "Dinner". Y para ello, es preciso cambiar el atributo "inherits" de la parte superior de la página de edit.aspx de la siguiente manera:
Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>
Después, la característica IntelliSense de la propiedad "Model" de la plantilla de vista se actualizará para reflejar el modelo de objetos del tipo DinnerFormViewModel que se va a pasar:
Luego, se puede actualizar el código de la vista para trabajar a partir de él. Observe a continuación que no se cambian los nombres de los elementos de entrada que se crean (los elementos de formulario se seguirán llamando "Title", "Country"), pero se van a actualizar los métodos auxiliares HTML para recuperar los valores mediante la clase DinnerFormViewModel:
<p>
<label for="Title">Dinner Title:</label>
<%= Html.TextBox("Title", Model.Dinner.Title) %>
<%=Html.ValidationMessage("Title", "*") %>
</p>
<p>
<label for="Country">Country:</label>
<%= Html.DropDownList("Country", Model.Countries) %>
<%=Html.ValidationMessage("Country", "*") %>
</p>
También se va a actualizar el método Edit post para usar la clase DinnerFormViewModel al representar errores:
//
// POST: /Dinners/Edit/5
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection collection) {
Dinner dinner = dinnerRepository.GetDinner(id);
try {
UpdateModel(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
Además, se pueden actualizar los métodos de acción Create() para volver a usar la misma clase DinnerFormViewModel para habilitar el elemento DropDownList "Countries" en ellos. A continuación se muestra la implementación de HTTP-GET:
//
// GET: /Dinners/Create
public ActionResult Create() {
Dinner dinner = new Dinner() {
EventDate = DateTime.Now.AddDays(7)
};
return View(new DinnerFormViewModel(dinner));
}
A continuación se muestra la implementación del método Create de HTTP-POST:
//
// POST: /Dinners/Create
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Dinner dinner) {
if (ModelState.IsValid) {
try {
dinner.HostedBy = "SomeUser";
dinnerRepository.Add(dinner);
dinnerRepository.Save();
return RedirectToAction("Details", new { id=dinner.DinnerID });
}
catch {
ModelState.AddModelErrors(dinner.GetRuleViolations());
}
}
return View(new DinnerFormViewModel(dinner));
}
Ahora, las pantallas de edición y de creación admiten el uso de listas desplegables para elegir el país o la región.
Clases ViewModel con forma personalizada
En el escenario anterior, la clase DinnerFormViewModel expone directamente el objeto de modelo de Dinner como una propiedad, junto con una propiedad de modelo SelectList compatible. Este enfoque funciona bien en escenarios en los que la interfaz de usuario HTML que se desea crear en la plantilla de vista se ajusta bastante a los objetos del modelo de dominio.
En los escenarios en los que no es así, se puede usar la opción de crear una clase ViewModel con forma personalizada cuyo modelo de objeto esté más optimizado para su consumo por parte de la vista (y que podría parecer completamente diferente del objeto modelo de dominio subyacente). Por ejemplo, podría exponer diferentes nombres de propiedad o propiedades de agregado recopiladas de varios objetos de modelo.
Las clases ViewModel con forma personalizada se pueden usar para pasar de los controladores a las vistas los datos que se van a representar, así como para ayudar a controlar los datos de formulario publicados de nuevo en el método de acción de un controlador. En este último escenario, es posible que el método de acción actualice un objeto ViewModel con los datos publicados en el formulario y que, después, use la instancia de ViewModel para asignar o recuperar un objeto modelo de dominio real.
Las clases ViewModel con forma personalizada pueden proporcionar gran flexibilidad, por lo que se deben investigar en cuanto el código de representación de las plantillas de vista o el código de publicación de formularios de los métodos de acción empiece a complicarse demasiado. Esto suele ser una señal de que los modelos de dominio no se corresponden inequívocamente a la interfaz de usuario que está generando y que una clase ViewModel de forma personalizada intermedia puede resultar de gran ayuda.
siguiente paso
Ahora veamos cómo podemos usar líneas de código parcialmente ejecutada y páginas maestras para volver a usar y compartir la interfaz de usuario en una aplicación.