Combos en ASP.NET MVC

Buenas! Una de las preguntas más referentes en ASP.NET MVC consiste en como crear combos, enlazarlas, etc, etc… La verdad es que la documentación sobre este punto es un poco difusa y dispersa así que intentaré en este post mostrar un poco las distintas formas que tenemos en ASP.NET MVC de crear combos.

Para ilustrar las distintas opciones partimos de una clase “Database” que simula un contexto de ORM. Es una clase que simplemente tiene dos listas (estáticas), una de ciudades (Cities) y otra de provincias (States). La definición de las clases City y State son:

    public class City

    {

        public int Id { get; set; }

        public string Name { get; set; }

        public string CP { get; set; }

        public int StateId { get; set; }

    }

 

    public class State

    {

        public int Id { get; set; }

        public string Name { get; set; }

    }

1. SelectListItem

En HTML una combo (<select>) contiene una lista de opciones que siempre son clave y texto que se muestra (ambos alfanuméricos). Para representar esta información en ASP.NET MVC disponemos de la clase SelectListItem. SelectListItem nos permite almacenar la clave (Value) y el texto (Text), así como un valor booleano (Selected) que indica si este es el elemento seleccionado por defecto a la combo (se corresponde al atributo selected del tag <option>).

Una posible forma de usarlo sería así:

var items = new List<SelectListItem>();

items = Database.Cities.Select(c => new SelectListItem()

    {

        Text = c.Name,

        Value = c.Id.ToString()

    }).ToList();

 

ViewBag.Cities = items;

return View();

Obtenemos las ciudades y luego convertimos cada objeto “City” en un objeto SelectListItem. Finalmente guardamos esta lista de SelectListItem en el ViewBag.

Para mostrar esta combo basta con usar en la vista:

@Html.DropDownList("Cities")

El nombre usado “Cities” es el nombre del campo usado en el ViewBag y donde se encuentra la lista de SelectListItem.

La combo generada tendrá un atributo name llamado Cities y esto es importante a la hora de recibir el valor seleccionado de la combo. Hay una gran confusión en esto. Una combo envia UN SOLO ELEMENTO al controlador: El ID del elemento seleccionado.

Veamos como podemos recibir la ciudad seleccionada:

[HttpPost]

public ActionResult Index(string Cities)

{

    var id = Int32.Parse(Cities);

    // Recuperamos la ciudad ==> Consulta a BBDD

    var city = Database.Cities.FirstOrDefault(c => c.Id == id);

    // Operamos con la ciudad

}

Dos cosas a destacar:

  1. Lo que recibimos en Cities NO es la lista de ciudades. Es el valor de la propiedad Value del SelectListItem seleccionado (en mi caso el ID de la ciudad seleccionada).
  2. El nombre del parámetro (Cities) es el mismo que el nombre del campo del ViewBag (y el mismo que pusimos en la llamada a Html.DropDownList).

2. IEnumerable

Tener que convertir siempre nuestros datos (en este caso una lista de ciudades) a una lista de SelectListItem es muy pesado. Por suerte hay una clase SelectList que hace esto por nosotros. Basta con pasarle el IEnumerable que queremos, el nombre de la propiedad que es la clave y el nombre de la propiedad que contiene el texto a mostrar.

public ActionResult Index()

{

    var items = Database.Cities;

    ViewBag.Cities = new SelectList(items, "Id", "Name");

    return View();

}

En este punto ViewBag.Cities contiene una SelectList (que implemente IEnumerable<SelectListItem>) y el resto del código ya es exactamente el mismo que antes.

3. Otros orígenes de datos

Hasta ahora en la vista hemos usado Html.DropDownList pasándole tan solo una cadena (Cities). Esta cadena determina:

  • El nombre del atributo name del <select> generado. Que a su vez es el nombre del parámetro cuando recibimos los datos
  • El nombre del campo del ViewBag que tiene los elementos.

Si no queremos que estos dos valores sean iguales, podemos espcificarle a Html.DropDown donde está el IEnumerable<SelectListItem> que contiene los datos de la combo. Así en la vista podríamos utilizar:

@Html.DropDownList("selectedCity", ViewBag.Cities as IEnumerable<SelectListItem>)

Con esto le estamos diciendo que nos genere un <select> cuyo atributo name valga “selectedCity” y que los datos están en ViewBag.Cities.

Ahora cuando recibimos los datos debemos tener presente que el parámetro ya NO se llama Cities, si no selectedCity:

[HttpPost]

public ActionResult Index(string selectedCity)

{

    var id = Int32.Parse(selectedCity);

    // Recuperamos la ciudad ==> Consulta a BBDD

    var city = Database.Cities.FirstOrDefault(c => c.Id == id);

    // Operamos con la ciudad

}

4. HtmlDropDownListFor

Este helper lía un poco porque tendemos a compararlo con el resto de helpers similares. Así, si yo hago Html.TextboxFor(m=>m.Name) me va a generar un Textbox vinculado a la propiedad Name del ViewModel de la vista. HtmlDropDownListFor también espera una propiedad del modelo pero NO es la propiedad que tiene los elementos, si no donde dejará el valor del elemento seleccionado.

Mucha gente se confunde y se cree que la propiedad que pasamos a Html.DropDownListFor es la propiedad que contiene los valores a mostrar. Imaginemos que tenemos el siguiente ViewModel:

public class ComboCitiesViewModel

{

    public IEnumerable<City> Cities { get; set; }

    public int SelectedCity { get; set; }

}

La acción Index nos queda ahora de la siguiente forma:

public ActionResult Index()

{

    var items = Database.Cities;

    var vm = new ComboCitiesViewModel();

    vm.Cities = items;

    return View(vm);

}

Para usar Html.DropDownListFor podemos hacerlo tal y como sigue:

@Html.DropDownListFor(m=>m.SelectedCity, new SelectList(Model.Cities, "Id", "Name"))

Le paso DOS parámetros a Html.DropDownListFor:

  1. La propiedad del ViewModel que contendrá el valor seleccionado
  2. El IEnumerable<SelectListItem> con los datos. Fijaos que en este caso dado que mi ViewModel contiene un IEnumerable<City> uso la clase SelectList que hemos visto antes para hacer la conversión.

Para recibir los datos puedo declarar la siguiente acción:

[HttpPost]

public ActionResult Index(ComboCitiesViewModel info)

{

    var id = info.SelectedCity;

    // Recuperamos la ciudad ==> Consulta a BBDD

    var city = Database.Cities.FirstOrDefault(c => c.Id == id);

    // Operamos con la ciudad

}

Y aquí es donde hay otro punto de confusión: en info NO VAS A RECIBIR la lista de ciudades. Es decir la propiedad Cities va a ser null:

image

¿Y eso? Pues bueno, simple y llanamente nadie manda estos valores de vuelta para el controlador. La lista de ciudades NO está en la petición POST que hace el navegador y por lo tanto el controlador no la recibe.

De hecho, podríamos modificar el parámetro ComboCitiesViewModel para que fuese un string llamado SelectedCity y funcionaría igual.

5. Combos encadenadas

Lo que vamos a ver es una implementación de combos encadenadas pero SIN ajax. Es decir, seleccionas provincia, envías la petición y te aparecen las ciudades. En una web “actual” seguramente se haría via Ajax, pero hacerlo de la “manera antigua” nos permitirá terminar de ver como funcionan las combos.

Antes que nada modificamos el viewmodel:

public class ComboCitiesViewModel

{

    public IEnumerable<City> Cities { get; set; }

    public int SelectedCity { get; set; }

    public IEnumerable<State> States { get; set; }

    public int SelectedState { get; set; }

}

La acción Index que envía la página inicial la modificamos para que rellene States:

public ActionResult Index()

{

    var items = Database.States;

    var vm = new ComboCitiesViewModel();

    vm.States = items;

    return View(vm);

}

La vista nos quedará de la siguiente forma:

@model MvcCombos.Models.ComboCitiesViewModel

 

@using (Html.BeginForm()) {

    <label for="Cities">Ciudad:</label>

 

        @Html.DropDownListFor(m=>m.SelectedState, new SelectList(Model.States, "Id", "Name"))

 

    if (Model.SelectedState != 0)

    {

        @Html.DropDownListFor(m=>m.SelectedCity, new SelectList(Model.Cities, "Id", "Name"))

    }

 

    <input type="submit" value="enviar" />

}

Es importante entender lo que hacemos en la vista:

  1. Si NO hay provincia seleccionada (Model.SelectedState vale 0) entonces mostramos la primera combo para seleccionar estado.
  2. Si hay estado seleccionado entonces generamos la combo para seleccionar la ciudad.

Nota: Este código tiene algunos problemas, como p.ej. que ocurre si el usuario selecciona una provincia, envía el formulario, y cuando aparece de nuevo la vista con las dos combos modifica la provincia seleccionada? En una aplicación real deberías, al menos, deshabilitar la combo de estados cuando ya haya estado seleccionado.

Y finalmente ahora la acción que recibe los resultados debe gestionar que será llamada dos veces (una para seleccionar el estado, la segunda con estado y ciudad):

[HttpPost]

public ActionResult Index(ComboCitiesViewModel info)

{

    if (info.SelectedState != 0 && info.SelectedCity == 0)

    {

        info.States = Database.States;

        info.Cities = Database.Cities.Where(c => c.StateId == info.SelectedState);

        return View(info);

    }

 

    var id = info.SelectedCity;

    // Recuperamos la ciudad ==> Consulta a BBDD

    var city = Database.Cities.FirstOrDefault(c => c.Id == id);

    // Operamos con la ciudad

}

Fíjate en un par de cosas:

  1. Debemos volver a cargar todos las provincias dentro del viewmodel. Si no cuando la vista intente renderizar la combo de provincias dará error.
  2. En las ciudades seleccionamos tan solo aquellas que son de la provincia que el usuario ha seleccionado.

Insisto, en una vista “real” la segunda vez no mostraríamos la combo de provincias, quizá mostraríamos el nombre de la ciudad seleccionada. Veamos como podríamos hacerlo.

Por un lado podemos modificar el viewmodel:

public class ComboCitiesViewModel

{

    public IEnumerable<City> Cities { get; set; }

    public int SelectedCity { get; set; }

    public IEnumerable<State> States { get; set; }

    public int SelectedState { get; set; }

    public string SelectedStateName { get; set; }

}

Añadimos la propiedad para guardar el nombre de la provincia seleccionada. Y en la vista usamos esta propiedad o Html.DropDownList en función de si hay o no provincia seleccionada:

@model MvcCombos.Models.ComboCitiesViewModel

 

@using (Html.BeginForm()) {

    <label for="Cities">Ciudad:</label>

 

    if (Model.SelectedState == 0)

    {

        @Html.DropDownListFor(m => m.SelectedState, new SelectList(Model.States, "Id", "Name"))

    }

    else

    {

        <text>Provincia @Model.SelectedStateName </text>

        @Html.DropDownListFor(m=>m.SelectedCity, new SelectList(Model.Cities, "Id", "Name"))

    }

 

    <input type="submit" value="enviar" />

}

Finalmente en la acción que recibe los datos nos ahorramos de rellenar de nuevo las provincias (ya que la vista ya no las usará la segunda vez) y ponemos el nombre de la provincia seleccionada (fíjate que hemos de ir a buscarlo a la BBDD ya que solo tenemos el ID):

[HttpPost]

public ActionResult Index(ComboCitiesViewModel info)

{

    if (info.SelectedState != 0 && info.SelectedCity == 0)

    {

        info.SelectedStateName = Database.States.Single(s => s.Id == info.SelectedState).Name;

        info.Cities = Database.Cities.Where(c => c.StateId == info.SelectedState);

        return View(info);

    }

 

    var id = info.SelectedCity;

    // Recuperamos la ciudad ==> Consulta a BBDD

    var city = Database.Cities.FirstOrDefault(c => c.Id == id);

    // Operamos con la ciudad

}

Con esta aproximación:

  1. La primera vez la vista mostrará la combo de provincias y el controlador recibirá en SelectedState el id de la provincia seleccionada.
  2. La segunda vez la vista mostrará un texto con el nombre de la provincia y la combo de ciudades y el controlador recibirá en SelectedCity el ID de la ciudad seleccionada. Esta segunda vez el controlador NO recibirá SelectedState ya que no se envía. En nuestro caso no es necesario ya que lo podemos sacar de la ciudad. Si fuese necesario deberíamos usar un campo Hidden.

Bueno… creo que esto más o menos es todo. ¡Espero que este post os ayude a resolver las dudas que podáis tener con las combos en ASP.NET MVC!

¡Un saludo!

4 comentarios en “Combos en ASP.NET MVC”

  1. Eduard, este caso muestra el ejemplo de dos tablas, una para estado y otra para city.

    que cambiara en el caso de que sea un combo multiple para una sola tabla?. ejemplo tipico es la tabla categorias con multiples parent id.
    ejemplo:
    categoryid | nombre de categoria | parent_categoryid

    existe forma de hacer un multiples combos en funcion de la cantidad de parent categories que tenga?

    muchas gracias!
    sebastian.

  2. Buenas @sebastianherrera

    ASP.NET MVC no soporta directamente el concepto de “combo múltiple para una sola tabla”. Es decir, tu debes leer los datos de esa tabla y organizarla en dos combos: dos objectos SelectList que mandas hacia la vista.

    Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *