Utilizar ViewData e implementar clases ViewModel

por Microsoft

Descargar PDF

Este es el paso 6 de un tutorial gratuito de aplicación "NerdDinner" que le guía a través de cómo compilar una aplicación web pequeña, pero completa con ASP.NET MVC 1.

En el paso 6 se muestra cómo habilitar la compatibilidad con escenarios de edición de formularios más enriquecidos y también se describen dos enfoques que se pueden usar para pasar datos de controladores a vistas: ViewData y ViewModel.

Si usas ASP.NET MVC 3, te recomendamos seguir los tutoriales introducción a MVC 3 o MVC Music Store .

Paso 6 de NerdDinner: ViewData y ViewModel

Hemos cubierto una serie de escenarios de envío de formularios y hemos analizado cómo implementar la compatibilidad para crear, actualizar y eliminar (CRUD). Ahora mejoraremos nuestra implementación de DinnersController y habilitaremos soporte para escenarios de edición de formularios más avanzados. Al hacerlo, analizaremos dos enfoques que se pueden usar para pasar datos de controladores a vistas: ViewData y ViewModel.

Pasar datos de controladores a View-Templates

Una de las características de definición del patrón MVC es la estricta "separación de preocupaciones" que ayuda a aplicar entre los distintos componentes de una aplicación. Los modelos, controladores y vistas tienen roles y responsabilidades bien definidos, y se comunican entre sí de formas bien definidas. Esto ayuda a promover la capacidad de prueba y la reutilización del código.

Cuando una clase Controller decide volver a representar una respuesta HTML a un cliente, es responsable de pasar explícitamente a la plantilla de vista todos los datos necesarios para representar la respuesta. Las plantillas de vista nunca deben realizar ninguna recuperación de datos o lógica de aplicación; en su lugar, deben limitarse a tener solamente código de representación que se base en el modelo o los datos proporcionados por el controlador.

En este momento, los datos del modelo que pasa nuestra clase DinnersController a las plantillas de vista son simples 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 agregamos más funcionalidades de interfaz de usuario a nuestra aplicación, a menudo vamos a necesitar pasar más que solo estos datos para representar respuestas HTML dentro de nuestras plantillas de vista. Por ejemplo, es posible que deseemos cambiar el campo "País" dentro de nuestro cuadro de texto Editar y crear vistas de ser un cuadro de texto HTML a una lista desplegable. En lugar de codificar de forma rígida la lista desplegable de nombres de país y región en la plantilla de vista, es posible que deseemos generarla a partir de una lista de países y regiones admitidos que rellenamos dinámicamente. Necesitaremos una manera de pasar el objeto Dinner y la lista de países y regiones admitidos de nuestro controlador a nuestras plantillas de vista.

Echemos un vistazo a dos maneras de lograrlo.

Uso del diccionario ViewData

La clase base Controller expone una propiedad de 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 queremos cambiar el cuadro de texto "País" dentro de nuestra vista Editar de ser un cuadro de texto HTML a una lista desplegable, podemos actualizar nuestro método de acción Editar() para pasar (además de un objeto Dinner) un objeto SelectList que se puede 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 SelectList anterior acepta una lista de países y regiones para poblar la lista desplegable, así como el valor que está seleccionado actualmente.

A continuación, podemos actualizar nuestra plantilla de vista de Edit.aspx para usar el método auxiliar Html.DropDownList() en lugar del método auxiliar Html.TextBox() que usamos anteriormente:

<%= Html.DropDownList("Country", ViewData["Countries"] as SelectList) %>

El método auxiliar Html.DropDownList() anterior toma dos parámetros. El primero es el nombre del elemento de formulario HTML que se va a generar. El segundo es el modelo "SelectList" que pasamos mediante el diccionario ViewData. Usamos la palabra clave "as" de C# para convertir el tipo dentro del diccionario como SelectList.

Y ahora cuando ejecutamos nuestra aplicación y accedemos a la dirección URL /Dinners/Edit/1 en nuestro explorador, veremos que nuestra interfaz de usuario de edición se ha actualizado para mostrar una lista desplegable de países y regiones en lugar de un cuadro de texto:

Captura de pantalla de la interfaz de usuario de edición con la lista desplegable de países y regiones resaltados con una flecha roja.

Dado que también generamos la plantilla de la vista de edición desde el método HTTP-POST Edit (en escenarios en los que se producen errores), queremos asegurarnos de que también actualizamos este método para agregar SelectList al ViewData cuando la plantilla de vista se genera 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);
    }
}

Y ahora nuestro escenario de edición de DinnersController soporta una lista desplegable.

Usar 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 sin escribir también requiere el uso del operador "as" o la conversión cuando se usa un lenguaje fuertemente tipado como C# en una plantilla de vista.

Un enfoque alternativo que podríamos usar es uno que a menudo se conoce como el patrón "ViewModel". Al usar este patrón, creamos clases fuertemente tipadas optimizadas para nuestros escenarios de vista específicos y que exponen propiedades para los valores dinámicos y el contenido que necesitan nuestras plantillas de vista. Nuestras clases controladoras pueden rellenar y pasar estas clases optimizadas para la vista a nuestra plantilla de vista para su uso. Esto permite la seguridad de tipos, la verificación en tiempo de compilación y la función IntelliSense del editor dentro de las plantillas de vista.

Por ejemplo, para habilitar escenarios de edición de formularios de cena, podemos 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);
    }
}

A continuación, podemos actualizar el método de acción Edit() para crear el modelo DinnerFormView mediante el objeto Dinner que recuperamos del repositorio y, a continuación, pasarlo a nuestra plantilla de vista:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);
    
    return View(new DinnerFormViewModel(dinner));
}

A continuación, actualizaremos nuestra plantilla de vista para que espere un "DinnerFormViewModel" en lugar de un objeto "Dinner" cambiando el atributo "inherits" en la parte superior de la página de edit.aspx de la siguiente manera:

Inherits="System.Web.Mvc.ViewPage<NerdDinner.Controllers.DinnerFormViewModel>

Una vez hecho esto, la intellisense de la propiedad "Model" dentro de nuestra plantilla de vista se actualizará para reflejar el modelo de objetos del tipo DinnerFormViewModel que se va a pasar:

Captura de pantalla de la ventana del editor de código con una lista desplegable y el elemento de lista Cena resaltado con un rectángulo azul.

Captura de pantalla de la ventana del editor de código con una lista desplegable y el elemento Lista de direcciones resaltado con un rectángulo de puntos gris.

A continuación, podemos actualizar el código de vista para que se base en ello. Observe a continuación cómo no estamos cambiando los nombres de los elementos de entrada que estamos creando (los elementos de formulario seguirán denominados "Title", "Country"), pero estamos actualizando 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 actualizaremos 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));
    }
}

También podemos actualizar nuestros métodos de acción Create() para volver a usar la misma clase DinnerFormViewModel para habilitar el dropDownList "Countries" dentro de 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 HTTP-POST Create:

//
// 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));
}

Y ahora, tanto las pantallas de Editar como de Crear admiten listas desplegables para elegir el país o la región.

Clases ViewModel con forma personalizada

En el escenario anterior, nuestra 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 queremos crear dentro de nuestra plantilla de vista corresponde relativamente estrechamente a nuestros objetos de modelo de dominio.

En escenarios en los que esto no es el caso, una opción que puede usar es crear una clase ViewModel con forma personalizada cuyo modelo de objetos está más optimizado para su consumo por parte de la vista y que podría parecer completamente diferente del objeto de modelo de dominio subyacente. Por ejemplo, podría exponer nombres de propiedad diferentes o propiedades de agregado recopiladas de varios objetos de modelo.

Las clases ViewModel con forma personalizada se pueden usar para pasar datos de controladores a vistas para 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 escenario posterior, puede que el método de acción actualice un objeto ViewModel con los datos publicados por el formulario y, a continuación, use la instancia de ViewModel para asignar o recuperar un objeto de modelo de dominio real.

Las clases ViewModel de forma personalizada pueden proporcionar mucha flexibilidad y son algo que se debe investigar cada vez que el código de representación en sus plantillas de vista o el código de envío de formularios en sus métodos de acción empiece a volverse demasiado complicado. Esto suele ser un signo de que los modelos de dominio no corresponden limpiamente a la interfaz de usuario que está generando y que una clase ViewModel de forma personalizada intermedia puede ayudar.

Paso siguiente

Ahora veamos cómo podemos usar vistas parciales y páginas maestras para reutilizar y compartir la interfaz de usuario en nuestra aplicación web.