jueves, 29 de abril de 2010

[jQuery] DropDownList anidados (nested DropDownList)

 

Introducción


La finalidad del articulo es mostrar como utilizando puramente jquery se puede vincular dos dropdownlist de forma tal que al cambiar la selección de uno de ellos se recargue el contenido del siguiente.

En el articulo se hará uso de una invocación a un Page Method, para exponer la información y poder recuperar de forma dinámica.

 

Inicialización de las listas


En el evento Page_Load se proceder a la carga inicial de los combos, esto será útil para que el usuario visualice una selección por defecto

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        ddlPaises.DataSource = GetPaises();
        ddlPaises.DataTextField = "Pais";
        ddlPaises.DataValueField = "Cod";
        ddlPaises.DataBind();

        ddlCiudades.DataSource = GetCiudadesByPais(Convert.ToInt32(ddlPaises.SelectedValue));
        ddlCiudades.DataTextField = "descripcion";
        ddlCiudades.DataValueField = "cod";
        ddlCiudades.DataBind();
    }

    
}


[WebMethod]
public static List<CiudadEntity> GetCiudadesByPais(int pais)
{
    var query = from item in GetCiudades().AsEnumerable()
                where Convert.ToInt32(item["CodPais"]) == pais
                select new CiudadEntity
                { 
                  cod = Convert.ToInt32(item["Cod"]),
                  descripcion = Convert.ToString(item["Ciudad"])
                };

    return query.ToList<CiudadEntity>();

}

Es importante remarcar como uno de los métodos tiene definido el atributo [WebMethod] y además se ha definido como static, este método será clave en la invocación desde el cliente con jQuery.

 

Cambio de selección desde el cliente, con jQuery


La definición del html es muy simple, solo son 2 DropDownList.

<form id="form1" runat="server">
<table>
    <tr>
        <td>Paises:</td>
        <td>
            <asp:DropDownList ID="ddlPaises" runat="server">
            </asp:DropDownList>
        </td>
    </tr>
     <tr><td colspan="2"></td></tr>
    <tr>
        <td>Ciudades:</td>
        <td>
            <asp:DropDownList ID="ddlCiudades" runat="server">
            </asp:DropDownList>
        </td>
    </tr>
</table>

</form>

La definición de script seria la siguiente:

<script language="javascript" type="text/javascript">

    $().ready(function() {

        $("#<%=ddlPaises.ClientID%>").change(function() {

            // armo el objeto que servira de parametro, para ello utilizo una libreria de JSON
            //este parametro mapeara con el definido en el web service
            var params = new Object();
            params.pais = $("#<%=ddlPaises.ClientID%>").val();
            params = JSON.stringify(params);

            $.ajax({
                type: "POST",
                url: "Default.aspx/GetCiudadesByPais",
                data: params,
                contentType: "application/json; charset=utf-8",
                dataType: "json",
                async: false,
                success: LoadCiudades,
                error: function(XMLHttpRequest, textStatus, errorThrown) {
                    alert(textStatus + ": " + XMLHttpRequest.responseText);
                }
            });

        });

        $("#<%=ddlCiudades.ClientID%>").change(function() {
            alert("Ha seleccionado: " + $("#<%=ddlCiudades.ClientID%> :selected").text());
        });

    });

    function LoadCiudades(result) {
        //quito los options que pudiera tener previamente el combo
        $("#<%=ddlCiudades.ClientID%>").html("");

        //recorro cada item que devuelve el servicio web y lo añado como un opcion
        $.each(result.d, function() {
            $("#<%=ddlCiudades.ClientID%>").append($("<option></option>").attr("value", this.cod).text(this.descripcion))
        });
    }
</script>

En el $().ready() se asocian a los combos dos eventos para poder atrapar un cambio de selección, es por eso que se usa el change().

Ante el cambio de selección de un país se procede en primer instancia al armado de parámetro que será enviado como filtro el Web Method, el cual se ha visto en el código anterior.

Para esta operación se hará uso de una librería de nombre json2.js, esta permitirá armar con objetos de javascript la información de una forma simple, y mediante el método JSON.stringify() convertir esta en json para ser enviada como parámetro.

En la operación también se toma el valor seleccionado en el combo usando:

$("#<%=ddlPaises.ClientID%>").val()

también se podría haber usado:

$(this).val()

ya que quien lanza el evento es el propio combo de países.

Es importante remarcar en este punto como se seleccionan los controles de asp.net usando en el selector de jquery, como asp.net renombran el atributo id de los controles es necesario usar el ClientID para tener el nombre correcto.

El siguiente paso es la invocación al método del servicio, para lo cual se utiliza el $.ajax(), hay algunas propiedades importantes que deben tenerse en cuenta, una de ellas es la definición de la url a invoca:

url: "Default.aspx/GetCiudadesByPais"

como se vera define directamente la propia pagina web donde se encuentra y luego el nombre del Web Method, pero podría ponerse los método del servicio en otro pagina si se requiere.

En el parámetro

data: params,

se define la variable del paso previo, en esta estará contenida el json que será enviado el web method.

También es importante definir si la invocación será asíncrona o no

async: true

en este caso no se espera a que el servicio web responda para continuar. Aquí no afecta si es es asíncrona o no la invocación ya que no hay código que continúe en la función javascript, pero puede que sea necesario a veces esperar una repuesta para poder tomar una decisión.

También se definen eventos en caso de una ejecución satisfactoria o un error

success: LoadCiudades,

hay varias formas de hacer esto, aquí se decidió usar una función nueva para procesar el contenido resultante de la invocación. Este método recibirá por parámetro de nombre “result” en donde se alojara el json de resultado.

 

Carga del combo de Ciudades


Es interesante analizar la función responsable de la carga de las ciudades proveniente de la invocación

function LoadCiudades(result) {
    //quito los options que pudiera tener previamente el combo
    $("#<%=ddlCiudades.ClientID%>").html("");

    //recorro cada item que devuelve el servicio web y lo añado como un opcion
    $.each(result.d, function() {
        $("#<%=ddlCiudades.ClientID%>").append($("<option></option>").attr("value", this.cod).text(this.descripcion))
    });
}

Esta recibe el resultado, y la primer operación será limpiar el combo, para ello es que lo selecciona y quita el html dentro de este, o sea quita los tag <option> que posea.

Luego toma cada valor devuelto en el json y lo recorre:

$.each(result.d, function() {..}

algo que seguro notaran extraño es el uso de result.d, este “d” es una propiedad del objeto result y allí contiene la información devuelta en la invocación, en este caso la lista de ciudades.

Se se pone un breakpoint en el código javascript, se podrá analizar claramente el valor devuelto en el json

Por supuesto el this dentro de la función representa cada ítem en el loop, y por tratarse de json este esta tipado con los mismas propiedades definida en la clase CiudadEntity devuelta por el web method.

 

 

[C#]
[VB.NET]

46 comentarios:

  1. Hola Leandro, una muy buena entrada. Me sirvió mucho. Gracias!!

    ResponderEliminar
  2. Buenas Leandro, la librería json2.js que mencionas, la has sacado de acá http://www.json.org/ ?? , si la respuesta es si me podrías indicar de donde exactamente??

    ResponderEliminar
  3. hola

    claro de ese link

    veras al final del mismo muchos links busca la seccion que dice: "JavaScript", justo alli

    te lleva a
    https://github.com/douglascrockford/JSON-js


    saludos

    ResponderEliminar
  4. Buenos días Leandro, en primer lugar muchas gracias por tu aporte ayuda a comprender como funciona el Ajax de Jquery a la perfección, pero al implementarlo en mi aplicación me indica el siguiente error "devolución de llamadas no valido", buscando en la Web encontré que hay que agragar EnableEventValidation="false" en el Page, pero ahora no puedo obtener el valor de la dropdownlist ddlCiudades cuando cambio de país, podrias orientarme?

    ResponderEliminar
  5. hola Nesoft

    te has encontrado con el eterna incompatibilidad entre asp.net y javascript

    resulta que si desde el cliente cambias los items de un control de asp.net este difiere con la info que la pagina tiene en el viewstate por lo tanto asume el contenido se altero y ya no puedes confiar en tomar el valor como lo venias realizando

    usar jquery apunta a volver a los controles de html, dejando de la los eventos y todo lo que plantea asp.net

    es mas no se si leiste algo de asp.net mvc, bueno las vistas se desarrollan usando los controels de html simples y jquery, ya no existe mas evento o viewstate, ni nada de eso

    si quieres seguir con asp.net quizas debas usar algo como ser

    CascadingDropDown

    saludos

    ResponderEliminar
  6. Muchas gracias por tu recomendación

    ResponderEliminar
  7. Tutti... sos una maquina de magia... siempre lo dije y siempre lo voy a seguir diciendo! Gracias por el artículo... me vino de pelos ;-)

    ResponderEliminar
  8. Que buen post, me sirvio para todo, Leandro una duda en la variable result en el lado de ajax cargas la matriz de datos, la desc y el value, que cantidad de datos puedes cargar de esta forma? es posible cargar unos 5000 a 50000 registros? Esto para armar bajo esta logica una tabla en vez de un dropdown.

    ResponderEliminar
  9. Primero felicidades y gracias por la ecepcional ayuda que tus blogs nos ofrece, ahora bien mi duda, es en base a la pregunta de Edalo, sobre la libreria json2, yo implemente tu ejemplo un proyecto, y por olvido no hice referencia a esta libreria y funciono correctamente, para que exactamente me sirve esta libreria?
    Gracias

    ResponderEliminar
  10. hola IvanKike

    esa libreria la usarias cuando quieres converir objetos de javascript a json para quizas trabjarlo como json, o como en estos casos enviarla como informacion a un webmethjod y que este mapee directo con clases de .net

    si en tu caso no envias informacion compleja a los webmethods
    o si armas el json directo como un string esta libreria no la usarias

    hay muchos que el json lo armas concatenando un string en javascript, eso tambien es valido, pero con la libreria se hace mas simple

    saludos

    ResponderEliminar
  11. Se ve muy interesante,ahora mismo me pongo a entenderlos.Gracias :)

    ResponderEliminar
  12. Hola, genial el post, me funciona casi perfecto, el problema que tengo es que cuando estoy llenado el dropdownlist y al llegar al item 1500 ya produce error.
    Te ha paso ?
    Sabes si recibir los datos en javascript tiene algun limite maximo ?

    Gracias

    ResponderEliminar
  13. hola Javier

    la verdad no me ha pasaso, pero tampoco soy de cargar tantos items en un combo, por lo general implemento otros metodos de selecion si los items son tan numerosos

    puede que estes teniendo algun problema con el limite de informacion que puedas enviar en un servicio web, estos por defecto tienen un limite establecido y tambien un timeout

    que error es el que recibes?

    saludos

    ResponderEliminar
  14. Hola Leandro, muy bueno tu ejemplo. Lo he implementado en un dialog con un drop dependiente de otros dos y funciona perfectamente.
    ´Por rizar más el rizo, ¿has hecho algo con un gridview que permita eliminación de filas por "RowCommand" y que esté dentro de un dialog de jquery?. He llegado hasta la eliminación de la fila pero para actualizar el grid no veo como hacerlo sin un postback. Si pudiera rellernarlo con $.ajax...pero no se me ocurre como. Si se te ocurre algo me orientas, gracias de antemano

    ResponderEliminar
  15. Por cierto, Javier Rojas comentó el error por que cargaba demasiados items, si es algo como "...La longitud de la cadena supera el valor establecido en la propiedad maxJsonLength...", simplemnte hay que añadir en el webconfig el tamaño que deseamos, por ejemplo:
    en la etiqueta system.web.extensions
    dentro de ella en scripting
    y dentro de ella en webServices
    y dentro de ella: jsonSerialization maxJsonLength="1000000"

    ResponderEliminar
  16. hola Bonaerge

    para eliminar de un grid que este dentro de jquery dialog podrias tomar dos caminos, o lo haces con $.ajax o sino suas el UpdatePanel

    con este ultimo podrias seguir usando eventos sin que se actualice toda la pagina

    con $.ajax ya no usarias el RowCommand, sino que vas a definir un boton o link de html y a este le adjuntas un click de jquery para que realice la accion

    en realidad si vas a este punto lo mejor es dejar de usar el GridView y usas el jQGrid es muhco mejor control y esta preparado para este tipo de acciones

    saludos

    ResponderEliminar
  17. Gracias po rtu consejo Leandro, no quería usar updatepanel porque el ScrptManager me ralentiza uno ssegundos la carga de la página y no le gusta al cliente, asi que evito usar las herraminetas Ajax de Visual Studio y las Ajax Toolkit. Para eliminar el registro que me presenta el grid efectivamente lo hago sin problemas con $.ajax, pero obviamente eso no me refresca el grid del dialog, necesitaría recargarlo un poco como haces en los dd anidados con el segundo dropdown. jgrid no lo he usado, lo estudiare para ver si puedo aplicarlo. Gracias por tus consejos y cualquier nueva sugerencia sera bienvenida. Saludos

    ResponderEliminar
  18. hola Bonaerges

    imagino cuando dices grid es que usas el GridView, el tema es que este retorna el render de la tabla como html, y no permite volver a recargarlo solo en esa seccion.

    quizas si usaras el grid dentro de un div y por medio de $.get podrias invocar el render del gridview para cargarlo con .html()

    igual lo veo dificil porque en asp.net no existe el concepto de vista parcial como si esta en asp.net mvc, por lo que hacer esto implicaria que el viewstate los tag de html para el body y head volverian aparecer lo cual no seria util.

    el jqGrid seria la mejor opcion, pero tambien esta el
    http://www.datatables.net/

    podria ser otra alternativa

    saludos

    ResponderEliminar
  19. muchas gracias leando me has ayudado mucho, te lo agradezco, saludos!

    ResponderEliminar
  20. Hola Leandro buenas noches, nuevamente molestandote y a la vez agradeciendote por compartir conocimientos...

    Bueno lo que pretendo hacer es devolver desde un procedimiento almacenado de sql server una consulta por medio de for xml raw ó for xml auto; bueno devolverlo es facil, ahora el tema es recibirlo desde la capa datos o web service con XmlReader pero dicho procedimiento devolvera una cadena (string) para ser transformado a json para de ahi procesarlo a mi gusto..

    Estoy tratando de hacer ese juego de la conversion xml a json pero no hallo la manera, agradecere mucho tu apoyo

    Estoy usando ahora ultimo el SqlXml SP3.0 pero aun no hallo la manera!!

    Gracias...

    ResponderEliminar
  21. hola Franky

    pero porque desde un procedure tienes que devolver un xml, porque no conviertes los datos usando un datareader a una entidad o list definiendo una clase

    para asi poder usar Json.net y convertir la lista a json

    esto es mucho ams simple de lograr y no necesitas pasar por xml

    saludos

    ResponderEliminar
  22. Hayayay...al fin una luz de esperanza jeje. Leandro lo que pasa es que me dijeron que si devuelvo desde sql en formato xml (for xml raw) mis consultas serian mas rapidas, por eso buscaba esa opcion; favor de aclararme si el tiempo de respuesta con las listas genericas son las mismas.. Gracias

    ResponderEliminar
  23. hola Franky

    no veo que usar xml sea mas rapido que devolver registros simples y volcar los datos a entidades de negocio

    aqui

    jqGrid – Listar Orden Compra (Maestro-Detalle)

    defino la capa de datos devolviendo entidades que luego se convirten para serializarlas a xml

    en tu caso quizas no necesites convertir la entidad como si lo requiere jqgrid, pero si podras ver como volcar los datos de una query a entidades usando un datareader

    saludos

    ResponderEliminar
  24. Hola Leandro, mira que otra vez molestando, esta vez tengo un ModalPopUpExtender que contiene un panel el cual a su vez tiene un mantenimiento completo, todo eso ok!! pero el problema surge cuando cierro esa ventana modal y por estar dentro de un updatepanel no actualiza un dropdownlist que esta fuera con los nuevos datos, me podrias indicar si hay alguna manera de poder hacerlo aunque sea con javascript!! Gracias

    ResponderEliminar
  25. hola Franky

    el tema es que si los demas controles que quieres actualizar estan fuera del updatepanel vas a tener que actualizarlo usando javascript

    aqui explico sobre el tema

    [ASP.NET] PopUp Edición - Usando Ajax Toolkit ModalPopupExtender

    saludos

    ResponderEliminar
  26. Muy buen aporte, me sirvio bastante Gracias...

    ResponderEliminar
  27. Muy buen aporte me sirvio bastante, Gracias!!!

    ResponderEliminar
  28. Hola Leandro, buenas noches, me parece muy interesante tu propuesta pero necesito saber de donde viene el objecto query que utilizas, ya que yo quiero devolver algo como esto para el combo return query.ToList();

    Pero no tengo el objeto query

    Muchas Gracias.

    ResponderEliminar
  29. Hola Leandro, buenas noches, me parece muy interesante tu propuesta pero necesito saber de donde viene el objecto query que utilizas, ya que yo quiero devolver algo como esto para el combo return query.ToList();

    Pero no tengo el objeto query

    Muchas Gracias.

    ResponderEliminar
  30. Hola Leandro ya tengo el metodo web montado tal como explicas ahora lo que necesito es llamarlo desde cualquier parte de la aplicacion.
    ¿Me podrias indicar como hacer la llamada en el parametro url del $.ajax o bien como podria incluir en un fichero de C# y hacer la llamada...?

    Muchas Gracias.

    ResponderEliminar
  31. hola leandro...
    una pregunta...
    la carga de paises.. tambien podria utilizar webmethod ?
    habria alguna diferencia :
    -cargaria mas rapido ?

    ResponderEliminar
  32. hola Maicol8k

    si tambien podria realizarse, es mas podrias no cargar nada en el Load de la pagina y ni bien se carga la pagina en el browser tener codigo jquery en el ready() para que realice estas invocaciones y cargue los combos

    es cuestion de gustos, quizas para editar es mas simple tener los combos cargados desde codigo servidor asi se puede seleccionar el valor que se recupera de la db y luego la iteraccion con el usuario si realizarla dinamicamente
    pero tambien se podria realizar todos desde el browser
    saludos

    ResponderEliminar
  33. Justo lo que andaba buscando, gracias por publicarlo.

    Saludos.

    ResponderEliminar
  34. Leandro qué tal, es grandioso tu post y de mucha ayuda...

    Mira tengo una duda, espero y puedas ayudarme, sino, de ante mano muchas gracias, lo que tengo es un dropdownlis, el cual es llenado de una BD, el método de llenado está en el codebehind; lo que necesito es obtener el valor o el id, al seleccionar un item de éste, obvio, lo quiero obtener para usarlo en Jquery, de igual froma uso un webmethod.

    Ya he intendo varias formas de obtner el valor y pues ninguna me resulta.

    ¡Saluods!

    ResponderEliminar
  35. hola Angel

    podrias usar

    var id = $("#<%=dropdownlist1.ClientID%>").val();

    o sino

    var id = $("[id*='dropdownlist1']").val();

    usando jquery y tomar el valor

    saludos

    ResponderEliminar
  36. Leandro que tal, me ocurre que al momento de ejecutar este post los eventos de los demás controles no funcionan, a que crees que se debe que el evento click por ejemplo no funcione?
    Gracias.

    ResponderEliminar
    Respuestas
    1. hola
      Partamos de la base que si usas jquery para cargar lso items usando esta tecnica no deberias usar evento de asp.net.
      Ahora para que los eventos funcionen deberias asignar el AutoPostBack = true del DropDownList
      saludos

      Eliminar
    2. Gracias por responder, pero ya lo he intentado y no funciona, aunque si hace al parecer postback, pero no llega al código del control.
      Esto es lo que tengo:
      $(document).ready(function () {
      $("#<%= SelectGrupo.ClientID%>").on('change', function () {
      var idGrupo = document.getElementById('<%=SelectGrupo.ClientID%>').value;
      var Parametros = { Grupo: idGrupo };
      $.ajax({
      type: "POST",
      url: "/carpeta/default.aspx/LlenaComboCursos",
      data: JSON.stringify(Parametros),
      contentType: "application/json; charset=utf-8",
      dataType: "Json",
      success: function (response) {
      $('#<%=SelectCurso.ClientID%>').html(response.d);
      }
      });
      });







      No me funciona el evento del botón ni el del dropdownlist,
      Que crees que pueda estar sucediendo?

      Eliminar
    3. hola
      No entiendo, cual es el objetivo que buscas, si alli usas ajax con jquery cual es la necesidad que el evento realiza post al servidor en su evento ?

      Otro detalle, si usas jquery puede seleccionar sin el getElementById(), podrias usar

      var idGrupo = $(this).val();

      en este caso this es el control al cual le adjuntas el evento
      saludos

      Eliminar
    4. hola, recuerda que en una pagina tengo de todo, botones, caja de texto , dropdownlist, etc. pero por el momento solo necesito llenar el dropdownlist con el método que tu muestras mas arriba, el asunto esta en que luego de implementar tu método, los botones no funcionan al dar clic, no se ejecuta su evento correspondiente.

      Eliminar
    5. hola
      Puede que se este generando un error de javascript, no validaste con la Developer Tools (a la cual accedes con F12) usando la solapa de consola podrias ver si un error en el codigo cliente anula los eventos
      Igualmente hay que remarcar que asp.net no se lleva muy bien con jquery cuando alteras los items de los controles, lo ideal es que si usas esta tecnica no uses evento, sino que las operaciones las realices con $.ajax de jquery invocando webmethods
      saludos

      Eliminar
  37. Hola Leandro

    cuando cambio el pais y la ciudad que se carga por primera vez me pasa este error:
    Invalid postback or callback argument. Event validation is enabled using in configuration or <%@ Page EnableEventValidation="true" %> in a page. For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them. If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation.

    ResponderEliminar
    Respuestas
    1. hola
      De casualidad estos combos los trabajas con javascript o jquery ? porque este mensaje suele aparecer cuando trabjas con los controles de asp.net con codigo cliente
      Es una validacion que aplica asp.net cuando compara los datos del control control el viewstate
      saludos

      Eliminar
  38. Hola Leandro, el llamado de una funcion o metodo me funciona perfectamente dentro no tengo que invocar ningun control u objeto previamente definido. Tengo un caso en el que tengo que llenar un gridview y el datasource lo obtengo accediendo a una clase que me devuelve un datatable. Si convierto en estatico el metodo que realiza esta tarea, no puedo acceder ni al gridview ni al objeto que permite consumir los metodos de la clase.

    private void CargarUsuarios()
    {
    try
    {

    DataTable dtUsuarios = OBJMensajes.CargarUsuarios(Convert.ToInt32(HttpContext.Current.Session["UsuarioActual"].ToString()));
    gvUsuarios.DataSource = dtUsuarios;
    gvUsuarios.DataBind();
    }
    catch (Exception ex)
    {
    throw ex;
    }
    }

    Como puedo solucionar este problema? Muuchas gracias.

    ResponderEliminar