Autor en Google+
Saltar al contenido

Variable not found. Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo de software, internet, u otros temas relacionados con la tecnología. C#, ASP.NET, ASP.NET MVC, HTML, Javascript, CSS, jQuery, Ajax, VB.NET, componentes, herramientas...

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, noticias, curiosidades, reflexiones... sobre el mundo del desarrollo
de software, internet, u otros temas relacionados con la tecnología

¡Microsoft MVP!
lunes, 30 de junio de 2008
Una cascadaLos desplegables en cascada (también llamados cascading dropdowns o enlazados) son elementos muy frecuentes en todo tipo de aplicaciones, pues suponen una gran ayuda al usuario y dotan de mucho dinamismo al interfaz.

En pocas palabras, consiste en llenar una lista desplegable con elementos elegidos en función de una decisión previa, como la selección en otra lista. El ejemplo típico lo encontramos en aplicaciones donde debemos introducir una dirección y nos ponen por delante un desplegable con las provincias y otro con los municipios, y en éste último sólo aparecen aquellos que pertenecen a la provincia seleccionada.

Esto, que en aplicaciones de escritorio no tiene dificultad alguna, en el entorno Web se complica un poco debido a la falta de comunicación entre la capa cliente (HTML, javascript) y servidor (ASP, .NET, JSP, PHP...), propia de un protocolo desconectado como HTTP.

Años atrás (e incluso todavía hoy) una forma de solucionar el problema es forzando un postback, es decir, una recarga de la página completa, con objeto de llenar el desplegable dependiente en función del valor seleccionado en el principal. Supongo que no es necesario detallar los inconvenientes: tiempos de espera, asombro del usuario ante el pantallazo, problemas asociados al mantenimiento del estado...

La aparición de Ajax (¡en minúsculas!) supuso una revolución para las aplicaciones Web, añadiendo a las amplias capacidades de Javascript para la manipulación del interfaz e información en cliente mecanismos ágiles para conseguir comunicación en tiempo real con el lado del servidor.

En este post explicaré cómo conseguir desplegables en cascada utilizando la plataforma ASP.NET MVC (Preview 3) en el lado servidor y Javascript en el lado cliente, apoyándome en la librería jQuery, que tanto juego da. Aprovecharé de paso para comentar algunos aspectos interesantes de la preview 3 del framework MVC, de jQuery y de novedades de C# 3.0. Tocho de post, por tanto ;-)

El resultado será un formulario como el mostrado en la siguiente imagen:

El proyecto en ejecución

Antes de nada: estructurar la solución

Para lograr el objetivo pretendido respetando el patrón MVC, debemos contar con los siguientes elementos:
  • El modelo, que representará la información manejada por el sistema. Para no complicar el ejemplo (o complicarlo, según se mire ;-)), vamos a crear una colección en memoria sobre la que realizaremos las consultas.

  • La vista, que será un formulario en el que colocaremos los dos desplegables. El primero de ellos permitirá seleccionar una categoría tomada del Modelo, y el segundo mostrará los elementos de dicha categoría de forma dinámica.

  • El controlador, en el que incluiremos acciones, en primer lugar, para inicializar y mostrar el formulario y, en segundo lugar, para obtener la lista de elementos relacionados con la categoría seleccionada. Esta última acción será invocada por la vista cuando el primer desplegable cambie de valor.

Primero: el modelo

En escenarios reales será el conjunto de entidades que representen el modelo de datos del sistema. En este caso montaremos un ejemplo muy simple, basado en un diccionario en memoria, y un par de métodos de consulta de la información en él mantenida.
public class Datos
{
private static Dictionary<string, string[]> datos =
new Dictionary<string, string[]>
{
{ "Animal", new[] { "Perro", "Gato", "Pez"} },
{ "Vegetal", new[] { "Flor", "Árbol", "Espárrago"} },
{ "Mineral", new[] { "Cuarzo", "Pirita", "Feldespato"} }
};

public static IEnumerable<string> GetCategories()
{
return datos.Keys;
}

public static IEnumerable<string> GetElementsForCategory(string category)
{
string[] els = null;
datos.TryGetValue(category, out els);
return els;
}
}

Destacar del código anterior los siguientes aspectos:
  • La información se almacenará en un Dictionary<string,string[]>. Esto, por si no estás muy familiarizado con los generics o tipos genéricos, sólo quiere decir que se trata de un diccionario cuya clave será de tipo string (las categorías) y el valor será un array de strings (los elementos asociados a cada categoría).

    De esta forma, podremos obtener fácilmente la lista de categorías simplemente enumerando las claves (Keys) del diccionario, y los elementos contenidos en una categoría sólo con devolver el valor asociado a la misma, como vemos en los métodos GetCategories() y GetElementsForCategories().

  • Para inicializar el diccionario he utilizado un inicializador de colecciones, una característica de C# 3.0 ya comentada en un post anterior.

  • Los dos métodos de consulta de datos retornan un IEnumerable<string> (¡me encantan los generics!), flexibilizando el tipo devuelto. Así, podríamos devolver cualquier tipo de objeto que implemente este interfaz, prácticamente todas las listas, colecciones y arrays, siempre que contengan en su interior un string.


Segundo: la vista

Para nuestro ejemplo sólo necesitamos una vista, el formulario con los desplegables, que crearemos en el archivo Index.aspx. Además de componer el interfaz, la vista gestionará la interacción con el usuario haciendo que ante la selección de una opción del desplegable de categorías se invoque al controlador para solicitarle la lista de elementos de la categoría seleccionada, y cargar con ella el segundo desplegable.

El código fuente del proyecto completo lo podéis encontrar en un enlace al pie del post, pero incluiré aquí las porciones más importantes. En primer lugar, el formulario HTML se compone así:
<form method="post" action="#">
<fieldset><legend>Formulario</legend>
<label for="ddCategory">Categoría:</label>
<%= Html.DropDownList("ddCategory") %>
<label for="ddElement">Elemento:</label>
<%= Html.DropDownList("ddElement") %>
</fieldset>
</form>

Fijaos un momento en el código. Los desplegables, en lugar de utilizar el tag HTML <select> se han definido utilizando los métodos de ayuda (Html helpers) incluidos en el framework MVC. Estos, además de generar las etiquetas apropiadas, realizan otra serie de tareas como establecer valores de la lista, que nos ahorrarán mucho tiempo. En el controlador veremos cómo se le inyectan los elementos (los distintos <option>) desde código de forma muy sencilla.

jQuery logoDespués del formulario, llegamos a la parte de scripting de la página, la implementación de la obtención y llenado del desplegable de elementos en función de la opción seleccionada, al más puro estilo Ajax. Utilizaremos jQuery, pero los conceptos son idénticos para otros sistemas; de hecho, esto mismo podríamos realizarlo con cualquier otra librería Javascript que permita enviar peticiones JSON, e incluso, aunque es bastante más trabajoso, sería posible hacerlo "a pelo" utilizando las facilidades nativas (HttpRequest).
<script type="text/javascript">

// Inicialización

$(document).ready(function() {
$("#ddCategory").change(function() {
cambiaElementos($("#ddCategory").val());
});
cambiaElementos($("#ddCategory").val());
});

// Carga el desplegable de elementos en función
// de la categoría que le llega como parámetro.


function cambiaElementos(cat) {

var dd = document.getElementById("ddElement");
dd.options.length = 0;
dd.options[0] = new Option("Espere...");
dd.selectedIndex = 0;
dd.disabled = true;

// Control de errores

$("#ddElement").ajaxError(function(event, request, settings) {
dd.options[0] = new Option("Categoría incorrecta");
});

// Obtenemos los datos...

$.getJSON(
'<%= Url.Action("GetElements") %>', // URL a la acción
{ category: cat }, // Objeto JSON con parámetros
function(data) { // Función de retorno exitoso
$.each(data, function(i, item) {
dd.options[i] = new Option(item, item);
});
dd.disabled = false;
});
}
</script>
 
En la primera parte se utiliza el evento jQuery ready, ejecutada cuando la página está lista para ser manipulada, para introducir código de inicialización. En este momento se asocia al evento change del desplegable de categorías la llamada apropiada a la función cambiaElementos(), pasándole la categoría seleccionada. También se aprovecha para realizar la primera carga de elementos.

Carga del desplegable en procesoSeguidamente está la función cambiaElementos, que se encargará de actualizar el desplegable con los elementos asociados a la categoría seleccionada. Para ello, en primer lugar, se introduce en el dropdown un elemento con el texto "Espere...", que aparecerá mientras el sistema obtiene los datos desde el servidor, a la vez que se deshabilita el control para evitar manipulación del usuario.

Después se define la función que se ejecutará si durante la llamada Ajax se produce un error. Para ello utilizamos el evento ajaxError, al que asociamos una función anónima que, simplemente, cargará en el desplegable secundario el texto "Categoría incorrecta" y lo dejará deshabilitado.

Por último, utilizamos el método getJSON para efectuar la llamada al servidor y realizar la carga del desplegable con la información devuelta. Son tres los parámetros que se le envían al método:
  • La URL de la acción, obtenida utilizando el helper Url.Action que devuelve la dirección de la acción cuyo nombre le pasamos como parámetro.

  • El objeto, en formato JSON, uyas propiedades representan los parámetros a enviar al controlador.

  • La función a ejecutar cuando se reciban datos del servidor. El parámetro de entrada data contendrá el array de string con el que tenemos que llenar el desplegable, enviado desde el servidor según la notación JSON.

    Aunque el recorrido de este vector y la carga de opciones en el control podría haberse realizado de forma sencilla mediante un bucle, he utilizado el método each para ilustrar el uso de este tipo de iteradores, tan de moda y muy al estilo funcional empleado, por ejemplo, en las expresiones lambda. Este método recibe dos parámetros; el primero es la colección sobre la cual se va a iterar, y el segundo es una función que se ejecutará para cada uno de los elementos recorridos.


Tercero: el controlador

Es el intermediario en toda esta historia: recibe las peticiones del cliente a través la invocación de acciones, ejecuta las tareas apropiadas y provoca la aparición o actualización de las vistas, utilizando para ello las clases ofrecidas por el Modelo.

El controlador define dos acciones: la primera de ellas (Index) es la encargada de cargar los valores iniciales de los desplegables y enviar al usuario la vista correspondiente (Index.aspx). La segunda es la responsable de devolver un objeto en notación JSON con los elementos de la categoría enviada como parámetro.

El código es el siguiente:

public class HomeController : Controller
{
public ActionResult Index()
{
ViewData["ddCategory"] = new SelectList( Datos.GetCategories() );
ViewData["ddElement"] = new SelectList( new [] {"(Selecciona)"} );
return View();
}

public ActionResult GetElements(string category)
{
IEnumerable<string> elements = Datos.GetElementsForCategory(category);
if (elements == null)
throw new ArgumentException("Categoría " + category +" no es correcta");

Thread.Sleep(1000); // Simulamos tiempo de proceso...

return Json(elements);
}
}
 
Hay varios puntos que me gustaría destacar en este código. En primer lugar, observad que ya las acciones siguen la nueva convención introducida en la preview 3, retornando objetos de tipo ActionResult. En el primer método, el return View() (sin parámetros) hace que el sistema envíe al cliente la vista cuyo nombre coincide con la acción actual (es decir, Index.aspx); en el segundo método se retorna un objeto de tipo JSonResult que serializará la información suministrada en formato JSON.

También es destacable la ayuda introducida en la tercera Preview para llenar listas de formularios utilizando la clase SelectList. Como podéis ver, basta con crear en el diccionario ViewData un elemento con el mismo nombre que el del desplegable y asignarle una instancia del SelectList inicializada con una colección de elementos (cualquier tipo que implemente IEnumerable). Él se encargará, al renderizar la página, de iterar sobre la colección y crear las etiquetas <option> oportunas.

Asimismo, se puede observar el uso del Modelo en ambos métodos para leer los datos. En este caso se utiliza una clase que encapsularía la lógica de obtención de información, pero nada impediría, por ejemplo, asumir que las clases del Modelo son las generadas desde el diseñador de Linq2Sql y utilizar consultas Linq desde el Controlador.

Observad también el lanzamiento de la excepción cuando la categoría especificada no existe. Lo que llega al navegador en este caso es un error HTTP que es capturado por la función que especificamos para el evento ajaxError, haciendo que en el desplegable enlazado aparezca un mensaje de error.

Por último, la inclusión de la orden Thread.Sleep() es por pura estética, para que se vea lo bien que queda el mensaje "Espere..." en el desplegable mientras carga los datos. En un escenario real este tiempo sería consumido por las comunicaciones cliente-servidor y la obtención de datos desde el modelo, que habitualmente utilizará una base de datos u otro mecanismo de persistencia.

Descargar proyecto Descargar proyecto (Visual Web Developer Express 2008 + SP1 + ASP.NET MVC Preview 3).

Publicado en: http://www.variablenotfound.com/.

Estos contenidos se publican bajo una licencia de Creative Commons Licencia Reconocimiento-No comercial-Compartir bajo la misma licencia 3.0 España de Creative Commons

27 Comentarios:

fernando dijo...

Simplemente muy buen ejemplo, no estoy muy al tanto del tema, pero me ayudaste a entender un poco más las cosas...
Gracias!

rodrijp dijo...

Muy bueno,

Tambien hay un buen ejemplo en:
http://stephenwalther.com/blog/archive/2008/09/07/asp-net-mvc-tip-41-creating-cascading-dropdown-lists-with-ajax.aspx

José M. Aguilar dijo...

Gracias!

Puedes encontrar también una mejora en el post Helper para desplegables enlazados con MVC, donde se implementa un helper actualizado a la versión beta de ASP.NET MVC.

Saludos.

Anónimo dijo...

Muy Bueno tu ejemplo, pero por que no trabajar con una conexion a base de datos o aun mas con procedimientos almacenados eso seria de mayor ayuda no crees. Si lo puedes hacer te lo agradeceria mucho.

kira dijo...

Tengo problemas en la función getJSON, le paso la url.action y tres parámetros {par1:par1,par2:par2, etc.
Pero llegan vacío a la función del controlador.. Ya no sé q más probar.
Gracias

José M. Aguilar dijo...

Hola, Kira.

Lo que te ocurre puede deberse a muuuchas cosas.

En el código fuente del proyecto al que hacía referencia más arriba puedes encontrar un ejemplo de envío mediante getJSON que espero que te valga.

De todas formas, te vendría bien utilizar Firebug (para Firefox) o alguna herramienta similar, de forma que podrías ver si tus datos realmente se están enviado o no al controlador y de qué forma, es decir, analizar la petición que realiza tu navegador al servidor.

Es posible también que tengas algún problema con los nombres de los parámetros; prueba a simplificar el escenario (por ejemplo, con un único parámetro, y que no pueda entrar en conflicto con nada de lo que estés usando).

Puedes ver ejemplos simples en en el web de jQuery, son buenos para hacer pruebas y echar a andar un caso simplificado.

Otra posibilidad es introducir un punto de ruptura en tu controlador y ver los parámetros de la petición (deberían llegar como parte de la URL en el método get).

En fin, ya me contarás. Si no averiguas nada, puedes contarme más detalles sobre el problema, o enviarme algún código.

Saludos.

kira dijo...

Hola! muchas gracias por tu respuesta. El problema estaba en que en la función del controlador, los parámetros eran los mismos pero se llamaban diferente (pensaba q no tenía nada q ver).
Los puse iguales y ya lee los parámetros :)

El problema que tengo es q creo que no "recibo" bien los datos de la consulta del controlador a sql:

Para aclararme he puesto así mi código en el form aspx:
...

$.getJSON('URL',{ dni:dni,tipus:trim(tipus),area:trim(area) }, dades);


....
la Función dades es esta:
function dades(json) {
alert("6 - dentro");
if (json.lenght > 0) {
alert("7 - dentro lenght");
$.each(json.items, function(i,item) {
alert("8 - bucle");
cap.options[i] = new Option(item,item);
});
} else {
alert("No hay datos para leer");
}}

Si la consulta no me devuelve datos todo funciona bien.
Pero si me devuelve datos va a .ajaxError. He puesto un alert allí para mostrar qué datos le llegaban:
...ajaxError(function(event, request, settings) {
cap.options[0] = new Option("Cap incorrecte");

........

En settings.url me incluye una URL correcta...

Qué puede ser? cómo puedo saber lo q contienen event i request?

Muchísimas gracias

kira dijo...

.

Anónimo dijo...

Excelente artículo, es la segunda vez que me salvas en MVC con tus artículos !! soy un novato en esta tecnología pero poco a poco estoy aprendiendo. Muchas gracias y sigue adelante.

Anónimo dijo...

Hola JOSE.

Espero que pronto tengas mas tutoriales de MVC. Una consulta sobre el jquery

Para que se usa este parametro:
" { category: cat }, "

Otra cosa, para que sirve ese metodo que usas ".val()" ?

Muchas gracias!!!

José M. Aguilar dijo...

Hola, gracias por comentar.

Sobre la primera cuestión, con el código:

{ category: cat }

estamos indicando en el interior de un objeto anónimo los datos a enviar al controlador. En el ejemplo del post, le estamos enviando un parámetro llamado "category" con el valor contenido en la variable "cat".

Si quisiéramos enviar más datos, se especificarían como propiedades del objeto anónimo:

{
category: cat,
name: "Juan",
edad: 56
}

Respecto a la segunda cuestión, $("#id").val() retorna el valor del control con identificador "id". Por ejemplo, si es un textbox, retornará el texto que haya introducido el usuario, si es un desplegable, el valor de la opción seleccionada.

Espero que te sea de ayuda.

Saludos.

Anónimo dijo...

Hola Jose, gracias por la respuesta de arriba, me quedo re claro, saludos y mil gracias !!!!

Hector dijo...

Hola Jose como te va, una consulta.

Viste que en el archivo global está definido el tema de las url amigables. Por defecto veo que siempre hay un parametro "{id}" y quería saber si es posible agregar algo como "{id}-{artTitulo}"
para que me quede algo como home/productos/145-tecladoUSB/"
mil gracias!!!

José M. Aguilar dijo...

Hola, Héctor.

Claro, puedes crear todos los patrones de URL que quieras, introduciendo en ellos los parámetros que necesitas.

Para tu ejemplo podrías insertar en la tabla de rutas algo como:

routes.MapRoute(
  "Products", // Route name
  "Products/Details/{id}-{art}",
  new { controller = "Products",
    action = "Details",
    art="", id = ""
  }
);

Y podrías tener una acción del tipo:

public class ProductsController: Controller
{
 public ActionResult Details(int id, string art)
 {
...
 }
}

Esta acción procesaría peticiones del tipo /Products/Details/321-TecladoUSB

Espero que te sea de ayuda.

Saludos.

Hector dijo...

Hola JOSE, buenisimo lo voy a probar.
MIL GRACIAS!!!!!

Hector dijo...

Hola Jose, como te va
Una consulta. Vos sabes que quiero que por ejemplo todos los combos que tengo un una lista hagan algo cuando se les cambie el valor entonces hago algo como esto

$("#myListBox").change(function(...){});

Me funciona bien, pero el tema es que solo funciona para el primer combo de la lista. Seguro esto se debe que todos los combos tiene el mismo nombre. Yo pensaba de alguna forma ponerle nombre distinto a cada combo y luego usar un for para agregarle la funcion ¿Pero con jquery no hay alguna forma de decirle que a todos los objetos combos de nombre "myList" le agrege una funcion??
Gracias Jose

José M. Aguilar dijo...

Hola, Héctor.

jQuery permite utilizar muchos selectores para identificar los elementos sobre los que quieres actuar.

Por ejemplo:

$(".combo") seleccionaría todos aquellos elementos cuya clase CSS sea "combo", es decir, que estén definidos como <select class="combo" ...>.

También podrías atacar directamente a la etiqueta. Así, $("select"), seleccionaría todos los <select> del documento.

Las posibilidades, como casi siempre, son infinitas...

Saludos.

Hector dijo...

Hola Jose, gracias por la respuesta me fue util.

Te quería hacer una consulta. Mira vos sabes que no se porque no me funcionan los htm.validationmessage()
Los pongo en la pagina de crear un producto, pero se envio en formulario sin completar los campos no me muestra el mensaje de error, y me fije de que en el metodo del controlador pongo
if(!ModelState.IsValid)
return view();

Y puse un punto de interrupción en el if y por mas que deje vació algunos o todos los campos el ModelState.IsValid simpre muestra "true". No entiendo porque antes cuando hice algunas pruebas me funcionaba bien, quería saber si tenes idea que puede ser. Los validation los tengo bien, me fije que tengan el nombre correcto y demás. Gracias desde ya Jose

José M. Aguilar dijo...

Hola, Hector.

Así a priori no puedo indicarte cuál es el origen del problema. Si usas MVC 2 debería funcionarte si has establecido correctamente las restricciones con las anotaciones de datos; si usas MVC 1, lo mejor es que te pases al 2 ;-), pues salvo que utilices algún framework externo como xVal, no traía incluido el sistema de validación.

De todas formas, Héctor, si tienes dudas no relacionadas con este post, por favor, utiliza el formulario de contacto. :-)

Saludos.

Gina dijo...

Buen ejemplo me ha servido de mucho. anexo pregunta.
Tengo informacion en un gridview que este tiene subelementos, lo que deseo hacer es que cuando borre un dato del elemento este vaya y elimine los subelementos que pertecen a ese elemento, puesto k si me elimina ese elemento pero no los subelementos. Por favor una horientacion???

kiga dijo...

oye te pregunto, esta caido el enlace? es que no puedo descargar el ejemplo para probarlo, por cierto en el caso de yo usar un textbox para consultar la información que va a tener el dd puedo hacer lo mismo? por otro lado yo no creo un diccionario con los datos, yo solo creo una lista con la información que consulto en la base de datos puedo hacerlo igual con el json? o debo usar otra forma?

muchas gracias

mmmartin dijo...

Gina! lo que podrías hacer es que en el método que mandas tu id para eliminar, hagas una consulta para obtener los sud-elementos y posteriormente también los elimines!!

Por cirto no me funciono con .getJson

Utilice .ajax y funciona bien!!!

alguien sabe porque con (.getJson) siempre me mandaba al .ajaxError después de ir al método??

José M. Aguilar dijo...

@mmartin, las versiones actuales de mvc requieren que las llamadas a acciones que retornan un JSON se realicen usando el método POST por motivos de seguridad.

Si quieres invocarlo con un get, en el return debes incluir JsonBehaviour.AllowGet expresamente como segundo parámetro del método Json.

Saludos.

mmmartin dijo...

José M. Aguilar.. Gracias!

Anónimo dijo...

No puedo descargarme el proyecto

Anónimo dijo...

Amigo esta muy bueno el ejemplo pero me aparecen algunos errores creo que van por el Javascript, podrías resubir el ejemplo para ver que no me ocurra el mismo problema.

gracias de antemano.

José M. Aguilar dijo...

Hola!

Lo lamento, este post tiene ya seis años y está desactualizado, por lo que el ejemplo no funcionará tal cual con las versiones actuales de las bibliotecas utilizadas (MVC, jQuery).

A ver si un día tengo tiempo y republico el post actualizado a las últimas versiones.

Saludos.