Para empezar, una pequeña explicación sobre qué es AJAX (con el permiso de la wikipedia).
En otras palabras, AJAX permite, mediante la suma de las citadas tecnologías, enviar y recibir datos entre cliente (navegador) y servidor sin provocar el refresco de la página, y sin tener que transferir aquel contenido estático (imágenes, maquetación de la web, menús, etc...) que consume ancho de banda innecesariamente.
En éste tutorial vamos a usar la librería libre de AJAX para Asp.NET para implementar un sistema de muestra de datos en una tabla (traidos de BD con un DataSet) con su correspondiente paginación y la capacidad de seleccionar filas y actualizarlas. Algo tal que así (lamento no poder mostrar activamente la aplicación aquí, pero este es un servidor Linux PHP - MySql) :
Antes de entrar al tutorial en sí, vamos a ver cual es la forma genérica de utilizar ésta librería.
Teniendo en cuenta lo anterior, vamos con el ejercicio.
Si desea investigar más...
La librería es libre, y podeis descargarosla o bajaros el proyecto de éste tutorial, que lógicamente ya trae la librería (implementada y funcional).
Sencillamente, copiar y pegar el siguiente httpHandler:
<configuration> <system.web> <httpHandlers> <add verb="POST,GET" path="ajax/*.ashx"
type="Ajax.PageHandlerFactory, Ajax" /> </httpHandlers> ... <system.web> </configuration>
En el evento Page_Load hay que implementar la llamada a la función:
private void Page_Load(object sender, EventArgs e) { }
Como veis, se especifica como TypeOf la clase _Default, que es la clase del formulario por defecto que crear Visual Studio .NET. En cada formulario donde vayamos a utilizar AJAX deberemos poner por TypeOf la clase por defecto del formulario, que viene especificada al principio de cada página *.cs (página de código), es decir, si nuestra página es:
public class Alta_Usuarios : System.Web.UI.Page
La llamada debería ser:
private void Page_Load(object sender, EventArgs e) { }
Ojo con esto si queremos evitarnos algún que otro dolor de cabeza. Además tendremos que usar el nombre de dicha clase para llamar a las funciones JavaScript que interactúen con el Servidor. Lo veremos más adelante.
Cada función que reciba peticiones por JavaScript desde cliente tiene la particularidad de que hay que añadirle el AjaxMethod(), por lo demás, es igual que cualquier otra función:
[Ajax.AjaxMethod()] public tipo_dato_devuelto nombre_función(tipo_dato variable, tipo_dato_2 variable_2...) { //Operaciones return valor; }
Las funciones en JavaScript deben tener por nombre el mismo que la función de Servidor más "_CallBack" (Ojo con mayúsculas y minúsculas) . En lo que se refiere a los parámetros que recibirá, siempre será la variable "response", ya reciba un parametro o varios.
Por último, obtendremos el resultado de Servidor mediante "response.value" aun que hay otras formas, por ejemplo al trabajar con DataSet's, ya lo veremos.
function nombre_funcion_en_servidor_CallBack(response){ //Operaciones //Obtendremos los valores devueltos con //response.value, aaunque hay una forma //distinta si trabajamos con DataSet, //Más adelante lo veremos. }
Una vez ya está todo implementado tanto en Cliente como en Servidor, necesitamos desencadenar el proceso, haciendo la llamada a la función JavaScript en el momento deseado, por ejemplo, en el click de un botón:
<INPUT onclick="_Default.nombre_funcion_servidor(parámetro1, parámetro2, ...,
parámetroN, nombre_funcion_servidor_CallBack);"
En el evento JavaScript deseado, por ejemplo, el evento onclick de un botón, (no confundir este evento onclick que es de JavaScript con un evento de Servidor, estamos en Cliente) ponemos, por este orden:
Veamos un ejemplo muy básico de los puntos 5º a 7º... El usuario escribe 2 valores en 2 TextBox, los enviamos a servidor y devolvemos la suma de ambos. Suponiendo que los puntos 1º a 4º ya están cumplimentados (esos pasos son siempre igual...):
Creamos la función de servidor que recibirá y procesará las peticiones:
[Ajax.AjaxMethod()] public string SumaValores(int firstNumber, int secondNumber) { return ("La suma de los valores " + firstNumber.ToString() + " y " +
secondNumber.ToString() + " es igual a " + (firstNumber + secondNumber)); }
La función de JavaScript:
<script type="text/javascript"> function SumaValores_CallBack(response){ if (response.error != null){ alert("Los valores introducidos no son válidos."); return; } target="contenido"; document.getElementById(target).innerHTML = response.value; } </script>
Por último, necesitamos los 2 TextBox y el botón con la llamada a la función JavaScript:
Y con esto ya tenemos operativo este sencillo sistema AJAX que comunicará con Servidor sin provocar el refresco de la página.
Aunque esto no tiene nada que ver con Asp.Net ni con la librería de AJAX, si pretendemos trabajar con este sistema será imprescindible manejar algunas funciones de JavaScript para maquetar (CSS) los resultados que traigamos de Servidor.
Esta función sirve para localizar e interactuar con una etiqueta html o xhtml por su identificador (atributo id) , por ejemplo:
//Dar estilos CSS document.getElementById("nombreCapa").style.visibility="visible"; //Asignar un contenido document.getElementById("nombreCapa").innerHTML="Asignar contenido."; //Etc...
Funciona exáctamente igual que la anterior, con la excepción de que no afecta a un elemento concreto, sino a todas las etiquetas de un tipo. Por ejemplo:
//Metemos todas las etiquetas "td" en un array var nodes = document.getElementsByTagName("td"); //obtenemos el nº total de etiquetas "td" de la página var max = nodes.length; //Recorremos el array dándole estilos a las etiquetas for(var i = 0;i ) { var nodeObj = nodes.item(i); nodeObj.style.color = "#000000"; nodeObj.style.background = "#FFFFFF"; }
En JavaScript no se especifica el tipo de dato de las variables, y esto puede traer problemas, ya que muchas veces al escribir:
var num1 = 2; var num2 = 4; midiv.innerHTML = "El resultado de sumar " + num1 + " más " + num2 + " es "
+ (num1 + num2);
Nos devuelve: "El resultado de sumar 2 más 4 es 24"... parece que algo ha sucedido mal. Evidentemente, JavaScript a interpretado que eran variables de texto y las ha concatenado.
Para esto y el caso contrario, tenemos estas 3 funciones, que fuerzan a JavaScript a interpretar la variable como un tipo u otro de dato.
Hay muchas más funciones que serían útiles, sin embargo este no pretende ser un tutorial de JavaScript, y con estas funciones podemos salir del paso.
El ejercicio utiliza la Base de Datos NorthWind que es la que trae por defecto SQL Server 2000, así que solo tendreis que poner vuestro usuario y contraseña en las cadenas de conexión.
Me he permitido la libertad de añadir algunos ticks de DOM y CSS para ocultar filas de la tabla, colorearlas al pasar el ratón (y descolorearlas al salir) y dejar coloreada de un color distinto la fila que se está editando, de modo que aunque cambiemos de página, al regresar seguirá marcada. Algunos de estos ticks, en particular el de ocultar filas, no pretende hacer nada en concreto, simplemente es para aquellos que estén interesados y quieran ver como manipular CSS dinámicamente con DOM de JavaScript.
Realmente no hay mucho que explicar, las particularidades de la librería de AJAX ya están explicadas en los puntos anteriores, lo único que aún no hemos visto es como mostrar y maquetar los datos traidos de un DataSet.
El modo de trabajar con un DataSet mediante JavaScript una vez lo hemos traido de nuestra consulta a Servidor es practicamente identico a como se hace tradicionalmente:
ds.Tables[numTabla].Rows[numFila].campoDB /* Donde: numTabla es la tabla que queremos manipular, ya que un DataSet puede llevar
varias. Si solo hemos hecho una consulta, será 'cero' 0. numFila es el registro o tupla actual. campoDB es el nombre de la columna de la select que queremos mostrar. */
Evidentemente, si vamos a traer más de una fila, deberemos recorrer el DataSet con un bucle:
//Obtenemos el DataSet con response.value //Y lo asignamos a una variable... var ds = response.value; //Recorremos el DataSet practicamente igual //que lo harÃamos en Servidor. for(var i=0; i<ds.Tables[0].Rows.length; i++){ alert(ds.Tables[0].Rows[i].Campo1); alert(ds.Tables[0].Rows[i].Campo2); alert(ds.Tables[0].Rows[i].Campo3); ... alert(ds.Tables[0].Rows[i].CampoN); }
Para maquetar los datos, la mejor solución es ir asignando los resultados en un array que finalmente volcaremos a un div:
//Obtenemos el DataSet con response.value //Y lo asignamos a una variable... var ds = response.value; //Declaramos el array //Para meter los datos solo tendremos que asignarlos //y JavaScript se encarga automáticamente de incrementar //el indice en cada asignación, del siguiente modo: //s[s.length] = "valor como texto"; //s[s.length] = "valor como texto2"; //La segunda asignación no machacará la anterior, //ya que el Ãndice del array se autoincrementa var s = new Array(); s[s.length] = "<table>"; s[s.length] = "<tr><th>Campo 1º</th><th>Campo 2º</th></tr>"; for(var i=0; i<ds.Tables[0].Rows.length; i++){ s[s.length] = "<tr>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].Campo1 + "</td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].Campo2 + "</td>"; s[s.length] = "</tr>"; } s[s.length] = "</table>"; //Volcamos el array a un div de id 'resultado' resultado.innerHTML = s.join("");
Salvando la declaración del AjaxMethod(), no difiere de lo de siempre...
//Obtenemos los datos que rellenarán la tabla. [Ajax.AjaxMethod()] public DataSet GetDataSet() { SqlConnection conn = new SqlConnection("Data Source=localhost;User id=sa;
Password=;Initial Catalog=northwind;"); SqlCommand cmd = new SqlCommand("SELECT Top 95 OrderID, ShipName,
ShipAddress, ShipCity, ShipCountry FROM Northwind.dbo.Orders ORDER BY 1", conn); try { conn.Open(); try { da.Fill(ds); } catch { return null; } finally { conn.Close(); conn.Dispose(); } } catch(Exception) { return null; } return ds; } //obtenemos la fila que se va a editar [Ajax.AjaxMethod()] public DataSet SacarFila(string id) { SqlConnection conn = new SqlConnection("Data Source=localhost;User id=sa;
Password=;Initial Catalog=northwind;"); SqlCommand cmd = new SqlCommand("select OrderID, ShipName, ShipAddress,
ShipCity, ShipCountry FROM Northwind.dbo.Orders where OrderId = @id", conn); cmd.Parameters.Add("@id",SqlDbType.Decimal).Value = Decimal.Parse(id); try { conn.Open(); try { da.Fill(ds); } catch { return null; } finally { conn.Close(); conn.Dispose(); } } catch(Exception) { return null; } return ds; } //Actualizamos el registro modificado [Ajax.AjaxMethod()] public string Actualiza_Fila(string OrderId, string ShipName, string ShipAddress,
string ShipCity, string ShipCountry) { SqlConnection conn = new SqlConnection("Data Source=localhost;User id=sa;
Password=;Initial Catalog=northwind;"); SqlCommand cmd = new SqlCommand("update Northwind.dbo.Orders set ShipName =
@ShipName, ShipAddress = @ShipAddress, ShipCity = @ShipCity, ShipCountry =
@ShipCountry where OrderId = @OrderId", conn); cmd.Parameters.Add("@ShipName", SqlDbType.NVarChar, 40).Value = ShipName; cmd.Parameters.Add("@ShipAddress", SqlDbType.NVarChar, 60).Value =
ShipAddress; cmd.Parameters.Add("@ShipCity", SqlDbType.NVarChar, 15).Value = ShipCity; cmd.Parameters.Add("@ShipCountry", SqlDbType.NVarChar, 15).Value =
ShipCountry; cmd.Parameters.Add("@OrderId", SqlDbType.Int).Value = Int32.Parse(OrderId); try { cmd.Connection.Open(); cmd.ExecuteNonQuery(); } catch (Exception errorMens) { return "<h4>El Expedidor <u style=\"background-color:#FF8000;\">
<span>" + ShipName + "</span></u> de Id <u style=\"background-color:#FF8000;
\"><span>" + OrderId + "</span></u> no ha sido actualizado. Tubo lugar
el siguiente error: <br><ul><li><em><u><span>" +
errorMens.Message + ".</span></u></em></li></ul></h4>"; } finally { cmd.Connection.Close(); } return "<h4>El Expedidor <u><span>" + ShipName + "</span></u> de Id
<u><span>" + OrderId + "</span></u> ha sido actualizado con éxito.</h4>"; }
Recomiendo encarecidamente que mireis este código descargándoos el proyecto, por que leerlo de aquí es harto complicado.
<!-- Función para sacarlos datos de la fila seleccionada de la tabla para su posterior modificación de datos (Update) -------- --> <script type="text/javascript"> /* Se envÃa a servidor el id de la tupla seleccionada para obtener dicha fila de DB */ function SacarFila_callback(response){ var ds = response.value; contenido2b.innerHTML = ""; /* Si la consulta trae datos, se crea dinámicamente una tabla con textbox para poder modificar los datos y reenviar (Update) */ if(ds != null && typeof(ds) == "object" && ds.Tables != null){ contenido2b.innerHTML += "<br><h4>Actualizar Datos:</h4><table
style='display:block; margin:0 auto;' border=0><tr><th>Id:</th>
<td><input class=noinput type=text id=ident value='" +
ds.Tables[0].Rows[0].OrderID + "' readonly=true></td></tr><tr>
<th>Expedidor:</th><td><input type=text id=user value='" +
ds.Tables[0].Rows[0].ShipName + "'></td></tr><tr><th>Dirección</th><td>
<input type=text id=nom value='" + ds.Tables[0].Rows[0].ShipAddress +
"'></td></tr><tr><th>Ciudad:</th><td><input type=text id=ape1 value='" +
ds.Tables[0].Rows[0].ShipCity + "'></td></tr><tr><th>Pais:</th><td><input
type=text id=ape2 value='" + ds.Tables[0].Rows[0].ShipCountry + "'></td></tr>
<tr><td colspan=2><input type=button class=deco style='width:100%'
value=actualizar id=enviar onclick='changeElementsStylebyTag(\"tr\",\"#000000\",
\"#FEFCFD\"); document.Form1.sel.value=\"-1\";
_Default.Actualiza_Fila(ident.value, user.value, nom.value, ape1.value,
ape2.value, Actualiza_Fila_callback);'><td></tr></table><br>"; } else { alert("Error. [3001] " + response.request.responseText); } } </script> <!-- ---------------------------------- --> <!-- Una vez modificados los datos se envÃan al servidor para concluir la actualización de datos (Update) --> <script type="text/javascript"> function Actualiza_Fila_callback(response){ contenido2b.innerHTML = response.value; _Default.GetDataSet(GetDataSet_callback); } </script> <!-- ----------------------------- --> <!-- Creamos la tabla y la paginación --> <script type="text/javascript"> /* Función para cambiar los estilos (css) por etiquetas. 'el' especifica el elemento, para el caso será los elementos 'tr', para colorear las filas de la tabla. 'clr' será el color del texto y 'bclr' el color de fondo (background) --------------------- */ function changeElementsStylebyTag(el,clr,bclr){ if(document.getElementsByTagName) { var nodes = document.getElementsByTagName(el); var max = nodes.length; for(var i = 0;i < max;i++) { var nodeObj = nodes.item(i); nodeObj.style.color = clr; nodeObj.style.background = bclr; } } } /* ------------------------------------- */ /* Igual que la anterior función, pero en puesto de seleccionar por etiqueta, lo hace por id --------------------------------- */ function changeElementsStylebyId(el,clr,bclr){ var nodeObj = document.getElementById(el); nodeObj.style.color = clr; nodeObj.style.background = bclr; } /* ------------------------------------- */ /* Funciones para colorear las filas al pasar el ratón --------------------- */ function changeElementsStylebyId_tr(el,clr,bclr,id){ var fila = document.Form1.sel.value; if (parseInt(id) != parseInt(fila)) { var nodeObj = document.getElementById(el); nodeObj.style.color = clr; nodeObj.style.background = bclr; } } function changeElementsStylebyId_tr_salida(el,clr,bclr,id){ var fila = document.Form1.sel.value; if (parseInt(id) != parseInt(fila)) { var nodeObj = document.getElementById(el); nodeObj.style.color = clr; nodeObj.style.background = bclr; } else { var nodeObj = document.getElementById(el); nodeObj.style.color = "#F7F7F7"; nodeObj.style.background = "#494949"; } } /* --------------------------------- */ /* Funciones para ocultar las filas */ function oculta_capa (capa) { if (document.getElementById(capa).style.visibility == "visible") { document.getElementById(capa).style.visibility = "hidden"; } else if (document.getElementById(capa).style.visibility == "hidden") { document.getElementById(capa).style.visibility = "visible"; } else { document.getElementById(capa).style.visibility = "hidden"; } } function oculta_capa_display (capa) { if (document.getElementById(capa).style.display == "block") { document.getElementById(capa).style.display = "none"; } else if (document.getElementById(capa).style.display == "none") { document.getElementById(capa).style.display = "block"; } else { document.getElementById(capa).style.display = "none"; } } /* -------------------------------- */ /* Con ésta función creamos la tabla y la paginación, que puede ser tanto con botones como con links. --------------- */ function GetDataSet_callback(response){ /* Obtenemos el DataSet */ var ds = response.value; /* Comprobamos si trae datos, de ser asà comenzamos a maquetar los datos ----- */ if(ds != null && typeof(ds) == "object" && ds.Tables != null){ /* Vamos metiendo los contenidos en un array que posteriormente volcaremos a un div -------------------------- */ var s = new Array(); s[s.length] = "<br>"; /* Miramos el valor del input type=hidden de nombre 'num' para saber que resultados de paginación debemos mostrar -------- */ var pag = document.Form1.num.value; var numpag = (parseInt(pag)/parseInt(10)); numpag = (parseInt(numpag) + parseInt(1)); var z = 1; s[s.length] = "<div id='paginacion'><div id='contenido'>"; /* quitar si se usa paginación de botones */ //s[s.length] = "|"; /* ************************************** */ /* formamos la paginación de la tabla */ for (var a=0; a<ds.Tables[0].Rows.length; a++){ /* Paginación con botones */ if (a%10 == 0) { s[s.length] = "<INPUT"; s[s.length] = " style='"; if (z == numpag) { s[s.length] = "background-color:#494949;
color:#F3F3F3; "; } s[s.length] = "width:70px;'"; s[s.length] = " class=deco
OnClick='document.Form1.num.value=" + a + ";
_Default.GetDataSet(GetDataSet_callback);'
type=button value=Pág." +
z + "> "; z += 1; } /* ********************** */ /* Paginación con links */ /* if (a%10 == 0) { s[s.length] = "| <a style='"; if (z == numpag) { s[s.length] = "width:90px; color:#1248A5;
font-weight:bold;
border:1px solid #5D5D5D; background-color:#EAEEEF; padding-left:10px;
padding-right:10px;"; } else { s[s.length] = "width:50px;"; } s[s.length] = "' href='#'
OnClick='document.Form1.num.value=" + a + ";
_Default.GetDataSet(GetDataSet_callback);'>" + "Pág. " +
z + "</a> |"; z += 1; } */ /* ******************** */ } /* quitar si se usa paginación de botones */ //s[s.length] = "|"; /* ************************************** */ s[s.length] = "</div></div>"; s[s.length] = "<br><br>"; /* Construimos la tabla con los contenidos del DataSet --------------------------- */ s[s.length] = "<table border=1 style='display:block; margin:0 auto;'>"; s[s.length] = "<tr><th>Actualizar/Ocultar</th><th>Id</th><th>Expedidor
</th><th>Dirección</th><th>Ciudad</th><th>Pais</th></tr>"; /* Número de filas que mostraremos por table */ var oper = 9; var comparar = parseInt(pag) + parseInt(oper); /* Construimos la tabla mostrando solo la página seleccionada ------------------ */ for(var i=0; i<ds.Tables[0].Rows.length; i++){ if ((parseInt(i) >= parseInt(pag)) && (parseInt(i) <=
parseInt(comparar))) { s[s.length] = "<tr"; /* Si el registro actual estaba seleccionado lo coloreamos --------------------------- */ if (document.Form1.sel.value ==
ds.Tables[0].Rows[i].OrderID) { s[s.length] = " style='background-color:#494949;
color:#F7F7F7'"; /* Le damos un nombre al td del botón seleccionar para que al colorear la fila, tras seleccionarla podamos descolorear (al final de la función), el fondo del botón, simplemente por que queda feo */ var identif = "td" + ds.Tables[0].Rows[i].OrderID; } else { s[s.length] = " onmouseover=\"
changeElementsStylebyId_tr('tr" + ds.Tables[0].Rows[i].OrderID
+ "','#F7F7F7','#8FA6B0'," + ds.Tables[0].Rows[i].OrderID +
"); changeElementsStylebyId_tr('td" +
ds.Tables[0].Rows[i].OrderID + "','#000000','transparent'," +
ds.Tables[0].Rows[i].OrderID + ");\" onmouseout=\"changeElementsStylebyId_tr(
'tr" + ds.Tables[0].Rows[i].OrderID + "','#000000','#FEFCFD'," +
ds.Tables[0].Rows[i].OrderID
+ ");\""; } s[s.length] = " id=tr" + ds.Tables[0].Rows[i].OrderID + ">"; s[s.length] = "<td id=\"td" + ds.Tables[0].Rows[i].OrderID
+ "\"><input onmouseover=\"this.style.background='#BF0000';
this.style.color='#F2F8FC';\"
onmouseout=\"this.style.background='#EAF4FA'; this.style.color='#242424';\"
class='deco2' style='width:100%' value='Seleccionar'
onclick='document.Form1.sel.value=" + ds.Tables[0].Rows[i].OrderID + ";
changeElementsStylebyTag(\"tr\",\"#000000\",\"#FEFCFD\");
changeElementsStylebyId_tr_salida(\"tr" + ds.Tables[0].Rows[i].OrderID +
"\",\"#F7F7F7\",\"#494949\"," + ds.Tables[0].Rows[i].OrderID + ");
changeElementsStylebyId(\"td" + ds.Tables[0].Rows[i].OrderID +
"\",\"transparent\",\"transparent\"); _Default.SacarFila(\"" +
ds.Tables[0].Rows[i].OrderID + "\", SacarFila_callback);' type=button id=" +
ds.Tables[0].Rows[i].OrderID + "><input style='visibility: visible;'
value='Visibilidad' type='button' onmouseover=\"this.style.background='#FF7400';
this.style.color='#F2F8FC';\" onmouseout=\"this.style.background='#EAF4FA';
this.style.color='#242424';\" class='deco2' style='width:100%'
onclick='oculta_capa(\"tr" + ds.Tables[0].Rows[i].OrderID +
"\");'></td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].OrderID +
"</td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].ShipName +
"</td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].ShipAddress +
"</td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].ShipCity +
"</td>"; s[s.length] = "<td>" + ds.Tables[0].Rows[i].ShipCountry +
"</td>"; s[s.length] = "</tr>"; } } s[s.length] = "</table>"; /* Volcamos el array con los contenidos en el div ---- */ contenido2.innerHTML = s.join(""); } else { /* Si el DataSet no traÃa datos, mostramos el mensaje de error del servidor ------ */ alert("Error. [3001] " + response.request.responseText); } try { /* Descoloreamos el fondo del botón de la tuple seleccionada ------- */ changeElementsStylebyId(identif,'transparent','transparent'); } catch (e) {} } /* ------------------------------- */ </script> <!-- ---------------------------- --> <!-- Aquà guardamos la fila seleccionada para que al movernos por la paginación de la tabla, al volver a la página donde se haya dicha fila, ésta permanezca resaltada. Si está a -1 es que no hay ninguna fila seleccionada ----------------------------- --> <input id="sel" type="hidden" value="-1"> <!-- ------------------------------------- --> <!-- Aquà guardamos la página (Paginación) en la que nos encontramos, para poder resaltar el enlace o botón de paginación en el que nos encontramos ------------------------------ --> <input id="num" type="hidden" value="0"> <!-- ------------------------------------- -->
La paginación esta implementada tanto con botones como con enlaces, ya va en el código comentado, solo teneis que comentar el código del que no querais usar, el resultado es: