lunes, 15 de octubre de 2007

(ModalPopup)Uso de un Modal Popup para mostrar y actualizar el contenido de un gridview

Se me presento un problema recientemente a la hora de utilizar el control modal popup extender para contener algo diferente de un label que cambie de texto; resulto que, pese a lo que habia escuchado a través de las introducciones y propagandas, los
controles AJAX no eran tan sencillos como me habian prometido.
Sucede que este control, (y no solamente este del toolkit de ajax) utiliza webservices, y para poder llegar a sacarles el jugo completamente es necesario tener tambien varios conocimientos de javascript (conocimientos de los cuales en su momento no disponia). Pero, siendo que en la red no encontre otro ejemplo que se ajustara mejor a mis necesidades me puse manos a la obra con el que encontre AQUI, pero tuve que adaptarlo a un escenario un poco diferente y más comun, pues yo estaba utilizando MasterPages.
El control modalpopup posee la posibilidad de utilizar contenido dinámico, para lo que es necesario utilizar las propiedades DynamicContextKey(el parametro que se enviará al webservice), DynamicServiceMethod(El nombre del método del webservice) y DynamicServicePath(El path al webservice), DynamicControlID(el control destino que va a recibir la información del webservice para mostrarla),(pueden encontrar el detalle de varias de las otras propiedades del modal popup extender AQUI). El principal problema con el que me encontré fue que, al tratar de utilizar algun otro control como destino de la información diferente al clásico label, la cosa no andaba para nada(nullreference en el ModalPopupExtender).

Esto va a ser, básicamente, un paso a paso del ejemplo en el link anterior con aquella información que no esta en el artículo y que tuve que descubrir por mi mismo:

Bueno para empezar, necesitaremos un gridview y un método para cargarle los datos que necesitamos (pueden utilizar el bindeo sencillo de asignar la fuente de datos a través del asistente para cargar la grilla, pero si también quieren actualizar los datos utilizando el modal es necesario que, una vez que se modificaron en el modalpopup se "refresque" la grilla, y es más sencillo(y rápido, y.... estético, y... todo) que vuelvan a invocar el método de cargado de la grilla en lugar de tener que hacer un postback completo).
En este caso asumimos que estoy queriendo ver y modificar la lista de una tabla clientes :

private void LlenarGrillaClientes()
{
string sql = "Select * from Clientes";
SqlDataAdapter da = new SqlDataAdapter(sql, “ connection string”);
DataTable dt = new DataTable();
da.Fill(dt);
GridView1.DataSource = dt;
GridView1.DataBind();
}

Una vez definido el método, es necesario que lo invoquemos en el Page_Load de la página. Procedemos a agregar un templatefield al gridview, y dentro del mismo agregamos un hyperlink(porque un hyperlink y nop un boton? porque si utilizamos un boton y no configuramos un par de cosillas extra, por defecto cuando presionemos el boton se producira un postback que evitará que el modal se muestre).

El siguiente paso es colocar los campos que necesitemos "expuestos" en el gridview a través del uso de DataKeys. En este caso seria"ID_Cliente"; al hacer esto, podemos "tomar" el valor del campo que pusimos en el DataKey para el row seleccionado en el evento RowDataBound.
protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
{
if (e.Row.RowType == DataControlRowType.DataRow)
{
HyperLink HyperLink1 = (HyperLink)e.Row.FindControl("HyperLink1");
HyperLink1.Attributes.Add("onclick",
"MostrarPopup('"+GridView1.DataKeys[e.Row.RowIndex].Value+"')");
}
}

En el codigo anterior, lo que hago es, primero buscar el hyperlink que agregue a la grilla, y luego agregarle el metodo onclick, para que invoque al método de Javascript MostrarPopup, al que paso como parámetro el valor del campo que especifique en el DataKey para el row seleccionado.

Aclaro algo, en el caso de que deseen activar el modalpopup a traves de un único boton "Actualizar" en la pagina, y no a través del hyperlink dentro de el grid, lo anterior deberia cambiar un poco:
protected void GridView1_RowCommand(object sender, GridViewCommandEventArgs e)
{
int index = Int32.Parse(e.CommandArgument.ToString());
BtnActualizarUsuario.Attributes.Add("onclick", "ShowMyModalActualizar('" + GrdVwListaAdministradores.DataKeys[index].Value+ "');return false;");

}

Es muy importante que agreguen el return false, para evitar que el botón con el cual estan queriendo invocar al modal cause un postback que refresque toda la pantalla y termine por hacer flashear la ventana modal para finalmente dejarla escondida, tambien noten que ya no se hace en el evento rowdatabound, sino en el command, esto es para tomar el valor seleccionado del gridview(utilizando el valor del index).


Bueno, hasta aqui lo sencillo, vayamos ahora a algo un poquito mas interesante, la parte webservice.
NO tuve buenas experiencias con el pseudo-webservice que me define por defecto el modal cuando le digo que voy a querer contenido dinamico, y después de muchas pruebas y errores decidi agregar el webservice yo mismo "Old Fashion Way", utilizando "Agregar Nuevo elemento" en ASP. (Nota al pie: No usen el nombre webservice que viene por defecto, porque es solo para confusiones, trust me en esto).
Asi que agregamos el webservice, pero es IMPORTANTE que agregue un par de cosas al ya definido webservice:
Ademas de las referencias que de por sí van a necesitar si pretenden utilizar sql (data, data.sqlclient) DEBEN agregar una referencia a System.Web.Script.Services, y agregar las clases [ScriptService] y [ScriptMethod] al servicio y al método del servicio respectivamente, sino no podrán referenciarlo desde el javascript.
Les va a quedar algo asi:
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ScriptService]
public class CargarDatos : System.Web.Services.WebService {

public CargarDatos () {

//Uncomment the following line if using designed components
//InitializeComponent();
}

[WebMethod]
[ScriptMethod]
public string CargarDatosClientes(string ID_Cliente)
{

string sql = "select * from Clientes where ID_Cliente=" + ID_Cliente;
SqlDataAdapter da = new SqlDataAdapter(sql, “connection string”);
DataSet ds = new DataSet();

da.Fill(ds);

if (ds.Tables.Count > 0)
{
return ds.GetXml();
}
else
{
return null;
}

}
}

El siguiente paso es agregar las referencias pertinentes al webservice al proyecto, utilizando "...Add web reference", y otra cosa más que muy importante es agregar la referencia al webservice dentro del scriptmanager (en la MasterPage):

<asp:ScriptManager ID="ScriptManager1" runat="server">
<Services>
<asp:ServiceReference Path="CargarDatos.asmx" />;
</Services>
</asp:ScriptManager>



Si no siguen estos pasos no van a lograr "Encontrar" el webservice a la hora de llamarlo desde su codigo de Javascript.
Algo que cabe destacar en el codigo del webservice es que retorno el dataset en formato XML, lo que nos va a permitir "desmembrarlo" luego desde mi funcion en javascript, para obtener los datos necesarios.

Lo siguiente es agregar un panel (para estructura del modalpopup y contenedor de los controles), y el modalpopupextender a la página. Colocamos los controles que necesitemos dentro del panel, en este caso yo uso textboxes (para poder editar la información que recuperamos del webservice) y un hiddenfield (que me almacena el ID_Cliente que voy a necesitar a la hora de actualizar la informacion en la BD cuando presione el boton actualizar en el modalpopup con la información ya alterada en los textboxes).

<asp:Panel ID="Panel1" runat="server" BackColor="LightGray" Height="269px" Width="428px" style="display:none">
<asp:HiddenField ID="hidCusCode" runat="server" />
<asp:TextBox ID="TxBxNombre" runat="server"&lt;/asp:TextBox>
<asp:TextBox ID="TxBxCiudad" runat="server"></asp:TextBox>
<asp:TextBox ID="TxBxLocalidad" runat="server"></asp:TextBox>
<asp:Button ID="Button1" runat="server" Text="Update" OnClick=" Button1_Click" />
<asp:Button ID="Button2" runat="server" Text="Cancel" />
</asp:Panel>;
<cc1:ModalPopupExtender ID="ModalPopupExtender1" runat="server" PopupControlID="Panel1" behaviorID="ModalPopupExtender" TargetControlID="Panel1" BackgroundCssClass="modalBackground" CancelControlID="Button2" OnCancelScript="HideModalPopup()"></cc1:ModalPopupExtender>


Observen que solo defino un boton cancel, que me va a esconder el modal despues, y que tengo un evento click asociado al boton actualizar, y como target control NO utilizo el hyperlink.

Okas hasta ahora todo mas o menos sencillo, ahora viene la parte... no tan pintoresca pero fundamental, el Javascript.

Defino 3 funciones en javascript en un bloque < type="text/javascript">< /script> EN la pagina contenido en la que tengo el panel, la grilla y el modal (probe de varias maneras esto, poniendolo en la masterpage, y en un archivo aparte de jscript, pero en ambos casos fui incapaz de poder referenciar a los controles a traves de su .ClientID, y tenia que usar el nombre que se le asigna cuando se arma el HTML, lo que francamente no es óptimo para nada y te quita escalabilidad, por lo que decidi ponerlo en la pagina contenido).
Observen que utilizo el behaviorID en el modal, hago esto para poder "encontrarlo" efectivamente, puesto que tampoco fui capaz de hacerlo de otra manera(esto tiene una explicacion de fondo que no voy a poner aca... pero sientanse libres de goooooglear your guts off :D).
function MostrarPopup(ID_Cliente)
{
var modal=$find('ModalPopupExtender1'); //haciendo referencia al behavior, y NO al id
modal.show();
CargarDatos.CargarDatosClientes(ID_Cliente,MostrarResultados);
}

//paso ID_Cliente al metodo del WS, y, si todo se desarrolla con exito en el WS, invoco a la funcion MostrarResultados (tambien hay bastante info sobre los parametros del WS cuando lo invoco desde javascript en internet, search my boys, search).

function MostrarResultados(result)
{
var doc;
if (window.ActiveXObject)
{
doc=new ActiveXObject("Microsoft.XMLDOM");
doc.async="false";
doc.loadXML(result);
}
else
{
var parser=new DOMParser();
var doc=parser.parseFromString(result,"text/xml");
}

var root=doc.documentElement.childNodes;
var tags;

for(var i=0;i<root.length;i++)
{
if (root[i].nodeType==1)
{
tags=root[i].childNodes;
}
}

for(var i=0;i<tags.length;i++)
{
if (tags[i].nodeType==1)
{
var xmlvalue=tags[i].childNodes[0].nodeValue;

switch (tags[i].nodeName)
{
case "ID_Cliente":
document.getElementById('<%=HiddenField1.ClientID%>').value=xmlvalue;
break;
case "Nombre":
document.getElementById('<%=TxBxNombre.ClientID%>').value=xmlvalue;
break;
case "Ciudad":
document.getElementById('<%=TxBxCiudad.ClientID%>').value=xmlvalue;
break;
case "Localidad":
document.getElementById('<%=TxBxCiudad.ClientID%>').value=xmlvalue;
break;
}
}
}
}


la parte de.ClientID es necesaria para poder encontrar el control cuando se arma el html, puesto que en el momento que la funcion va a ser llamada, cada control ya no se llama de la manera "TxBxNombre", sino ctl00$ContentPlaceHolder1$TxBxNombre; que es una identidficacion generada automaticamente; al usar CLientID obtenemos el ctl00$ContentPlaceHolder1$TxBxNombre en el momento en el que se genera el html sin la necesidad de ESPECIFICAMENTE escribirlo en tiempo de codificacion, con las desventajas que esto traeria aparejado.
Basicamente la funcion anterior lo que hace es tomar el xml y "desmembrarlo", cargando cada control en funcion al valor del mismo.
Ok, ahora la funcion que esconde el modal:
function HideModalPopup()
{
var modal = $find('ModalPopupExtender1');
modal.hide();
}

Perfecto, so far so good; con esto deberian ser capaces de mostrar el modalpopup con los datos cargados desde el xml(recuerden crear una clase css para modalpopup y modalbackground).
Ahora vamos a proceder a hacer "impactar" los cambios que formulen en los texboxes en la base de datos, para esto lo que hacemos es lo siguiente:
En Page_load agregamos:

Button1.OnClientClick=String.Format("fnClickUpdate('{0}','{1}')", Button1.UniqueID, "");


y dentro del
<script type="text/javascript"></script>
agregamos otra funcion mas:

function fnClickUpdate(sender, e)
{
__doPostBack(sender,e);
}


que basicamente lo que nos va a permitir es hacer un postback DESDE el modal para asi poder realizar la actualizacion de los datos.
Ahora lo que hacemos es agregar la logica sql de actualizacion dentro del evento click del boton 1(el de actualizar)

protected void Button1_Click(object sender, EventArgs e)
{
string sql = "Update Clientes Set Nombre=’”+TxBxNombre.Text+”’,
otros_controles=otros_campos Where ID_Cliente="
+HiddenField1.Value;
SqlConnection conn = new SqlConnection(“Connection String”);
conn.Open();
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.ExecuteNonQuery();
conn.Close();
LlenarGrillaClientes();
}


Y voilá, tienen una linda ventanita modal que no solo muestra datos de una grilla, sino que ademas les permite actualizarlos.

Creo que no se me olvido nada, en caso de alguna duda pregunten nomas que para eso estamos...

Saludos y happy programming

P.D:Es mi primer post y es mas que factible que tenga algun error, el funcionamiento del ejemplo es completo, lo estoy usando actualmente, asi que si algo no funciona puede ser error mio al armar el post, o de ustedes al interpretarlo, asi que no duden en preguntar o avisarme si ven algo mal.

P.D.2: Grax Dario por el tip de formateo de codigo en el blog

Saludos

No hay comentarios: