jueves, 18 de junio de 2009

Ajax y DropDownLists anidados

Cuando me inicie en el mundo del desarrollo web me tope con lo que todo desarrollador que inicia se topara en algún momento de su vida, me refiero al tedioso procedimiento de utilizar combos anidados, es decir combos que depende de los valores elegidos en otros, en buen castellano quiere decir que los valores que tengo listado en el combo numero 2 dependen del valor que elegí en el combo numero 1 y que los valores que tengo listados en el combo numero 3 dependen del valor que elegí en el combo numero 2 y así sucesivamente, pudiendo llegar al en\’e9simo nivel de anidamiento sin ningún problema (claro que no conocí casos de pasaran de 4 combos anidados en proyectos reales).

Las soluciones que rondaban eran diversas, la mas sencilla de ellas requería que cada ves que eligiera un valor del combo numero 1 este recargara toda la pagina para poder capturar el valor elegido y con ello poder llenar el combo numero 2, el que a su ves cuando era elegido un valor nuevamente recargaba la pagina para capturar el valor y poder llenar el combo numero 3, etc.

Llego ajax y se facilitaron mas las cosas ya que cuando se eligiera un valor del combo numero 1 este enviaba una petición por detrás de la aplicación la misma que devolvía la lista de valores del combo numero 2 sin necesidad de recargar la pagina.

Bueno luego de tanta melancolía por aquellos años (hablo como si tuviese 60 no?), definiremos el dilema de hoy: me llegan los bocetos y las maquetas de la nueva aplicación que esta en desarrollo y le echo un ojo, lo mas fácil de desarrollar generalmente en proyectos comunes (el pan de cada día), son los famosísimos formularios de contacto entonces procedo a revisarlos de forma rápida pero sorpresa ..... el bendito formulario trae como dato obligatorio el UBIGEO (Ubicación Geográfica), que encima de todo se propone de la siguiente manera Departamento - Provincia - Distrito (osea 3 niveles), con mucha tristeza y lagrimas en los ojos por que me tomara mas tiempo de lo que pensé en hacerlo, me bajo el ultimo ubigeo disponible en las bases de datos de INEI (Instituto nacional de estadística e informática - Perú), y me dispongo a desarrollar.

En principio debemos saber que ajax como corre en el lado del cliente no deberíamos tener problemas con el lenguaje que estemos usando, esto cumplía para todos los lenguajes que yo utilizaba menos para uno .... si señores ASP.NET (c#), como sabemos existen controles listos para usarse y una variedad de alternativas en toda la web, busque pero ninguno se prestaba para lo que yo necesitaba exactamente así es que decidí usar ajax nativamente e idear la forma de llenar los dropdownlists vía javascript, esto lo dejo claro ya que no estoy usando ninguna implementación de ajax para .NET sino simplemente funciones javascript.

En principio necesitamos 3 DropdowLists que se llaman en mi caso:

cmbDpto - para los departamentos
cmbProv- para las provincias
cmbDist - para los distritos

Demás esta decirles que cmbDist depende de cmProv y cmbProv depende directamente de cmbDPto, y los defino de la siguiente forma:

   1: <td>
   2: <asp:dropdownlist id="cmbDpto" runat="server" cssclass="form_combos" onchange="return cmbDptoOnChange()"></asp:dropdownlist>
   3: </td>
   4: <td align="left">
   5: <asp:dropdownlist id="cmbProv" runat="server" cssclass="form_combos" onchange="return cmbProvOnChange()"></asp:dropdownlist>
   6: </td>
   7: <td align="left">
   8: <asp:dropdownlist id="cmbDist" runat="server" cssclass="form_combos">
   9: </td>

cmbDpto dispara la función javascript cmbDptoOnChange() y cmbProv dispara cmbProvOnChange(), también se darán cuenta que solo tienen que llenar los datos del control cmbDpto (código del departamento y nombre del departamento - la forma de llenarlo depende de ustedes), los otros dos dropDownLists no contienen elementos. Ahora necesitamos definir las funciones mencionadas arriba...

   1: function cmbDptoOnChange(){
   2:      var cmbDpto = document.getElementById("cmbDpto");
   3:      var selectedDpto = cmbDpto.options[cmbDpto.selectedIndex].value;
   4:      var requestUrl = "AjaxServer.aspx?ff=dp&tipo=" + encodeURIComponent(selectedDpto);
   5:      CreateXmlHttp();
   6:  
   7:      if(XmlHttp){
   8:          XmlHttp.onreadystatechange = HandleResponseDP;
   9:          XmlHttp.open("GET", requestUrl, true);
  10:          XmlHttp.send(null);
  11:      }
  12: }
   1: function cmbProvOnChange(){
   2:      var cmbDpto = document.getElementById("cmbDpto");
   3:      var cmbProv = document.getElementById("cmbProv");
   4:      var selectedDpto = cmbDpto.options[cmbDpto.selectedIndex].value;
   5:      var selectedProv = cmbProv.options[cmbProv.selectedIndex].value;
   6:      var requestUrl = "AjaxServer.aspx?ff=dd&tipo=" + encodeURIComponent(selectedDpto+selectedProv);
   7:  
   8:      CreateXmlHttp();
   9:  
  10:      if(XmlHttp){
  11:          XmlHttp.onreadystatechange = HandleResponseDD;
  12:          XmlHttp.open("GET", requestUrl, true);
  13:          XmlHttp.send(null);
  14:      }
  15: }

OK creo que los javascript se explican por si mismos solo que tienen dependencias es decir dependen de otras funciones, en general tenemos la función básica ajax para crear el objeto XmlHttp e inicializar una petición osea:

   1: var XmlHttp;
   2:  
   3:  function CreateXmlHttp(){
   4:      try{
   5:          XmlHttp = new ActiveXObject("Msxml2.XMLHTTP");
   6:      }catch(e){
   7:          try{
   8:              XmlHttp = new ActiveXObject("Microsoft.XMLHTTP");
   9:          } catch(oc){
  10:              XmlHttp = null;
  11:          }
  12:      }
  13:   
  14:      if(!XmlHttp && typeof XMLHttpRequest != "undefined"){
  15:          XmlHttp = new XMLHttpRequest();
  16:      }
  17: }

Si todo va bien con la petición de la función cmbDptoOnChange() entonces se traslada el manejo a otra función javascript HandleResponseDP() que es la que se encargara de procesar la respuesta que genera el objeto XMLHTTP esta función es como sigue:

   1: function HandleResponseDP(){
   2:      if(XmlHttp.readyState == 4){
   3:          if(XmlHttp.status == 200){
   4:              LlenarProvincias(XmlHttp.responseXML.documentElement);
   5:          }else{
   6:              alert("Problemas con el servidor Ajax." );
   7:          }
   8:      }
   9: }

si todo va bien deriva la respuesta hacia la función LlenarProvincias() que es la que se encargara de pintar nuestro control con los datos de las provincias. Ahora si es que son observadores se dieron cuenta que la función cmbDptoOnChange() y la funcion cmbProvOnChange() llaman a un archivo que lleva por nombre ajaxserver.aspx que no es mas ni menos que un webform sin código html dentro del cual se implementan funciones que devolverán los datos que se le soliciten en formato XML es decir si yo solicitara provincias devolvería algo así como :

   1: <provincias> 
   2:      <provincia>
   3:          <valor>02</valor> 
   4:          <titulo>Lima</titulo>
   5:      </provincia>
   6:      <provincia>
   7:          <valor>03</valor>
   8:          <titulo>Canta</titulo>
   9:      </provincia>
  10: </provincias>

esta respuesta es la que tenemos que parsear en la funcion LlenarProvincias() para poder pintar los elementos del control cmbProv la función es como sigue:

   1: function LlenarProvincias(NodoProvincias){
   2:      var listaProvincias = document.getElementById("cmbProv");
   3:  
   4:      for (var count = listaProvincias.options.length-1; count >-1; count--){
   5:          listaProvincias.options[count] = null;
   6:      }
   7:  
   8:      var NodoProvincia = NodoProvincias.getElementsByTagName(’provincia’);
   9:      var textValue;
  10:      var textTitulo;
  11:      var optionItem;
  12:   
  13:      for (var count = 0; count < NodoProvincia.length; count++){
  14:          var NodoValor=NodoProvincia[count].getElementsByTagName(’valor’);
  15:          textValue = GetInnerText(NodoValor[0]);
  16:          var NodoTitulo=NodoProvincia[count].getElementsByTagName(’titulo’);
  17:          textTitulo = GetInnerText(NodoTitulo[0]);
  18:          optionItem = new Option( textTitulo, textValue, false, false);
  19:          listaProvincias.options[listaProvincias.length] = optionItem;
  20:      }
  21:   
  22:      cmbProvOnChange();
  23: }

Al final de esta función se hace un llamado a la función cmbProvOnChange() directamente ya que si yo acabo de llenar mi lista de provincia lo ideal seria tomar el primer elemento de la misma y con su valor llenar mi lista de distritos no es cierto? (si lo dudas es que tienes que volver a leer desde el principio jejeje). cmbProvOnChange() procede de la misma forma que cmbDptoOnChange() es decir cmbProvOnChange llamara a la función HandleResponseDD que se encargara de recepcionar la respuesta de nuestro ajaxServer.aspx y llenara el cmbDist de distritos con la función LlenarDistritos(), las funciones se las muestro a continuación.

   1: function HandleResponseDD(){ 
   2:      if(XmlHttp.readyState == 4){ 
   3:          if(XmlHttp.status == 200){
   4:              LlenarDistritos(XmlHttp.responseXML.documentElement);
   5:          }else{
   6:              alert("Problemas con el servidor Ajax." );
   7:          }
   8:      }
   9: }
   1: function LlenarDistritos(NodoDistritos){
   2:      var listaDistritos = document.getElementById("cmbDist");
   3:      for (var count = listaDistritos.options.length-1; count >-1; count--){
   4:          listaDistritos.options[count] = null;
   5:      }
   6:   
   7:      var NodoDistrito = NodoDistritos.getElementsByTagName(’distrito’);
   8:      var textValue;
   9:      var textTitulo;
  10:      var optionItem;
  11:   
  12:      for (var count = 0; count < NodoDistrito.length; count++){
  13:          var NodoValor=NodoDistrito[count].getElementsByTagName(’valor’);
  14:          textValue = GetInnerText(NodoValor[0]);
  15:          var NodoTitulo=NodoDistrito[count].getElementsByTagName(’titulo’);
  16:          textTitulo = GetInnerText(NodoTitulo[0]);
  17:          optionItem = new Option( textTitulo, textValue, false, false);
  18:          listaDistritos.options[listaDistritos.length] = optionItem;
  19:      }
  20: }

Con esto ya tenemos todo lo de nuestro webform de contáctenos, ahora les explico lo del ajaxserver.aspx, este webform no tiene código en el archivo aspx pero si tienen funciones en el archivo ajaxserver.aspx.cs, si se dan cuenta las llamadas en las funciones cmbDptoOnChange() y cmbProvOnChange() son de tipo GET y las rutas son:

"AjaxServer.aspx?ff=dp&tipo=" + encodeURIComponent(selectedDpto); y’par
"AjaxServer.aspx?ff=dd&tipo=" + encodeURIComponent(selectedDpto+selectedProv);’par ’par

respectivamente, esto quiere decir que el webform ajaxserver.aspx recibe dos parámetros el primero ff que puede ser dp (provincias) o dd (distritos) es decir lo que se le esta solicitando, el segundo parámetro viene a ser tipo que en realidad lo que contiene es el código del departamento en el primer caso y en el segundo el código del departamento mas el código de la provincia, estos parámetros serán procesados en el webform de la siguiente manera (ajaxserver.aspx.cs):

   1: protected void Page_Load(object sender, EventArgs e){
   2:      if (!IsPostBack){
   3:          string tipo = Request.QueryString["tipo"];
   4:          string funcion=Request.QueryString["ff"];
   5:          string respuestaXML="";
   6:   
   7:          if (tipo.Length > 0 && funcion.Length>0){
   8:              Response.Clear();
   9:   
  10:              if (funcion == "dp"){
  11:                  ubigeo miubigeo = new ubigeo();
  12:                  respuestaXML = miubigeo.dameProvXML(tipo);
  13:              }
  14:              if (funcion == "dd"){
  15:                  ubigeo miubigeo = new ubigeo();
  16:                  respuestaXML = miubigeo.dameDistXML(tipo.Substring(0, 2), tipo.Substring(2, 2));
  17:              }
  18:   
  19:              Response.Clear();
  20:              Response.ContentType = "text/xml";
  21:              Response.Write(respuestaXML);
  22:              Response.End();
  23:          }else{
  24:              Response.Clear();
  25:              Response.End();
  26:          }
  27:      }else{
  28:          Response.Clear();
  29:          Response.End();
  30:      }
  31: }

Bueno y solo falta definir como serán las funciones que devuelvan XML para hacerlo simple solamente junte cadenas de texto y forme las etiquetas concatenando cadenas y nada mas. El procedimiento que muestro es solo una idea ustedes pueden generar sus cadenas XML como mas les convenga además que ustedes tienen sus propias formas de conectarse a bases de datos que obviamente difieran de la mía, aquí el código:

   1: public string dameListaXML(string idioma){
   2:      string milista = "";
   3:      milista = milista + "<provincias>";
   4:      beanitemlista miitemlista;
   5:      DbDataReader resp;
   6:      fabricaSgbd mifabrica = new fabricaSgbd();
   7:      Isgbd msgbd = mifabrica.dameSgbd();
   8:   
   9:      resp = msgbd.consultar("select * from provincias where estado=’H’ order by nombre asc");
  10:      while (resp.Read()){ 
  11:          milista = milista + "<provincia>";
  12:          milista = milista + "<codigo>" + Convert.ToString(resp["provincia_id"]) + "</codigo>’n";
  13:          milista = milista + "<nombre>" + Convert.ToString(resp["nombre"]) + "</nombre>’n";
  14:          milista = milista + "</provincia>’n";
  15:      }
  16:   
  17:      milista = milista + "</provincias>’n";
  18:      msgbd.cerrar();
  19:      return milista;
  20: }

y ..... por fin terminamos como verán anidar combos no es muy sencillo que digamos y varia con respecto al lenguaje que usemos al menos en el presente articulo intento mostrar algo que nos puede servir para todos los lenguajes ya que solo tendríamos que reemplazar nuestro ajaxserver en el lenguaje que necesitemos
Suerte y hasta que me den ganas de escribir otro articulo :P