Prólogo e inicios
En un artículo previo denominado Enlace de elementos en controles de lista en ASP.NET vimos las posibilidades que ASP.NET ofrecía para enlazar orígenes de datos a controles de lista y realizamos enlaces vía código y vía los elementos SqlDataSource y ObjectDataSource sin codificar ni una línea en los dos últimos casos.
En este artículo, daremos un próximo paso y no solo mostraremos información de un origen de datos en un control ASP.NET, sino que también veremos la forma de transferir los cambios realizados por el usuario sobre dichos datos nuevamente hacia el origen de datos. Para realizar estas tareas la funcionalidad proporcionada por los controles de lista no nos será suficiente, por lo que aprovecharemos la oportunidad para conocer uno de los controles más populares de ASP.NET, el control GridView el cual renderizará, como no debería sorprendernos, una grilla con datos.
Debido a que el control GridView es extremadamente flexible y ofrece una considerable variedad de posibilidades, como no queremos que el artículo se extienda demasiado, nos ocuparemos ver todos estos temas mencionados realizando el enlace vía código, ya que no hay tantos ejemplos (o al menos eso me parece), como los que pueden encontrarse contra enlaces a los objetos SqlDataSource y ObjectDataSource.
Para comenzar, dado que el artículo trata sobre enlace, diremos que en nuestro escenario contaremos con:
- Un origen de datos (datos a enlazar).
- Un control ASP.NET (GridView) destinado a enlazar y mostrar en la interfaz de usuario a los elementos del origen de datos.
Nuestro interés será en esta etapa simplemente enlazar el origen de datos al control GridView.
Para poder realizar esta tarea, el control GridView ofrecerá una propiedad denominada DataSource en la cual deberá establecerse el origen de datos y todo quedará casi listo, un detalle a considerar es que podrán enlazarse elementos que implementen algunas de las siguientes interfaces: ICollection, IEnumerable o IListSource.
Seguramente una duda razonable será como se renderizará el control al efectuarse el enlace. En el caso de enlazar un objeto que implemente ICollection generará una fila por cada elemento de la colección y una columna por cada propiedad pública que el elemento posea.
A continuación y a modo de ejemplo enlazaremos una lista de elementos personalizados a un control GridView, utilizaremos como origen de datos un elemento del tipo List
<asp:GridView runat="server" ID="GV1" />
public class MiElem { public string Valor1 { set; get; } public int Valor2 { set; get; } public DateTime Valor3 { set; get; } public bool Valor4 { set; get; } public string Valor5 { set; get; } public string Valor6 { set; get; } } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); BindGrid(GV1); } private void BindGrid(GridView Grd) { List Lst = new List(); Lst.Add(new MiElem() { Valor1 = "V1", Valor2 = 1, Valor3 = DateTime.Now.AddDays(1), Valor4 = true, Valor5 = "imagen1.gif", Valor6 = @"~\Imagenes\imagen1.gif" }); Lst.Add(new MiElem() { Valor1 = "V2", Valor2 = 2, Valor3 = DateTime.Now.AddDays(2), Valor4 = true, Valor5 = "imagen1.gif", Valor6 = @"~\Imagenes\imagen1.gif" }); Lst.Add(new MiElem() { Valor1 = "V3", Valor2 = 3, Valor3 = DateTime.Now.AddDays(3), Valor4 = true, Valor5 = "imagen1.gif", Valor6 = @"~\Imagenes\imagen1.gif" }); Grd.DataSource = Lst; Grd.DataBind(); }
Como puede verse:
Un detalle a mencionar es que el método DataBind() será quien realizará el traspaso de datos desde el origen de datos hacia el control, si el mismo es obviado los datos no serán transferidos y la grilla quedará vacía. Por otra parte cualquier modificación que se realice sobre la lista “Lst” posterior a la invocación al método DataBind() no será transferida al control y no será reflejada en el mismo.
Volviendo al ejemplo, cabe mencionar que lo que ha hecho el control es utilizar reflection para conocer las propiedades públicas de los elementos que componen la lista Lst (en realidad del primer elemento de la lista) y ha autogenerado una columna para cada una de ellas, además ha enlazado a cada elemento en una fila (ubicando el valor de cada propiedad en la columna correspondiente).
Luego de la impresión inicial, es claro que este comportamiento no nos será demasiado útil en la mayoría de los casos, al menos eso me parece, ya que posiblemente si deseamos enlazar un grupo de elementos, en muchos casos se deseará visualizar solo algunas de sus propiedades (públicas).
En un artículo previo vimos que los controles de lista poseían la propiedad DataTextField para indicar que propiedad (o campos en el caso de enlazar el control contra un DataSet, un DataTable o un DataReader) mostraría el control. Pero el caso del GridView, que como ya hemos visto, permitirá enlazar varias propiedades o campos en forma simultánea, el esquema será más complejo y más aún si consideramos otros factores como por ejemplo, el orden de las columnas. Es entonces esperable que exista una manera de definir cómo será la visualización de las propiedades a mostrar en las columnas de la grilla que considere todos estos factores.
Personalización de columnas
El tag Columns permitirá llevar a cabo esta definición, en el siguiente ejemplo se define una grilla que mostrará solamente las propiedades Valor2 y Valor3 en ese orden, para cada elemento MiElem enlazado.
<asp:GridView runat="server" ID="GV1" AutoGenerateColumns="false"> <Columns> <asp:BoundField DataField="Valor2"/> <asp:BoundField DataField="Valor3" DataFormatString="{0:yyyy/dd/MM}"/> </Columns> </asp:GridView>
En el ejemplo se han definido elementos BoundField indicando el nombre de las propiedades a enlazar en la primera y segunda columna y se ha utilizado la propiedad DataFormatString para darle un formato personalizado a la propiedad Valor3.
<asp:BoundField DataField="Valor2" HeaderText="Col1" />
Aunque en el ejemplo hemos definido columnas del tipo BoundField, se podrán definir columnas de los siguientes tipos:
- BoundField: Mostrará el valor de la propiedad como texto.
- CheckBoxField: Permitirá mostrará el valor de una propiedad booleana como un control CheckBox (y ajustará el valor automáticamente al valor de la propiedad).
- HyperLinkField: Mostrará el valor una propiedad (o un texto estático) como un hipervínculo.
- ImageField: Permitirá mostrar una imagen a partir de una propiedad.
- TemplateField: Permitirá crear una columna personalizada.
- ButtonField: Permitirá crear un botón a partir del valor del una propiedad.
- CommandField: Creará botones de selección, eliminación o edición.
A continuación modificaremos un poco el ejemplo y utilizaremos algunos de estos tipos de columnas mencionados:
<table style="border: solid 2px red;"> <tbody> <tr> <td style="border: solid 2px green;">Este es un item Pantilla</td> </tr> <tr> <td style="border: solid 2px blue;">en una columna</td> </tr> </tbody> </table>
Al ejecutar el código se obtendrá un resultado similar al siguiente:
ImageField
Para la segunda columna, se ha ejemplificado el caso frecuente donde puede verse como se han utilizado las propiedades DataImageUrlFormatString y DataImageUrlField para indicar la parte fija de la url a la imagen (en la propiedad DataImageUrlFormatString) y el nombre del campo que posee el valor la parte variable de la misma (en la propiedad DataImageUrlField).
<asp:ImageField NullDisplayText="Sin imagen" DataImageUrlFormatString="~\Imagenes\{0}" DataImageUrlField="Valor5"/>
En el momento del enlace el valor {0} será reemplazado por el valor de la propiedad Valor5 en cada ítem a enlazar, generando de esa manera la dirección final a la imagen, esta opción es bastante útil para escenarios donde las imágenes de encuentran dentro de una misma carpeta.
Será posible omitir la propiedad DataImageUrlFormatString, pero en tal caso debería establecerse en la propiedad DataImageUrlField un valor que posea la url completa a la imagen, El valor de la propiedad Valor6 del ejemplo nos podría haber servido en ese caso, tal como se muestra a continuación:
<asp:ImageField NullDisplayText="Sin imagen" DataImageUrlField="Valor6"/>
CheckBoxField
En el caso de la tercer columna, la misma se ha enlazado directamente a un campo booleano, el control ha renderizado un checkbox, el mismo se mostrará seleccionado si la propiedad Valor4 toma el valor “verdadero”.
<asp:CheckBoxField DataField="Valor4" />
HyperLinkField
En la cuarta columna se generado un enlace a la misma imagen que se mostró en la segunda columna, se ha utilizado la propiedad DataNavigateUrlFields para indicar la url completa a la dirección del enlace almacenada en el campo Valor6 en contraposición al ejemplo que habíamos realizado en la segunda columna.
Podríamos haber incorporado y establecido la propiedad DataNavigateUrlFormatString con el valor «~\Imagenes\{0}» y haber utilizado la propiedad Valor5 (en forma similar al ejemplo de la segunda columna) para obtener el mismo resultado, de más está decir que la implementación definirá cual opción será la conveniente.
La propiedad DataTextField indicará el valor que mostrará el enlace y la propiedad Target indicará el target del enlace (_blank, _self, _top, etc.).
Será también posible generar columnas que utilicen una constante como enlace, en tal caso deberá emplearse la propiedad NavigateUrl.
TemplateField
En la última columna se ha utilizado un campo plantilla, el cual nos permitirá agregar los controles deseados a voluntad dentro de un ItemTemplate que será el contenedor de los mismos (ItemTemplate será en realidad un tipo especial de contenedor denominado “data container”). Si le prestamos atención al ejemplo previo, seguramente nos generará una gran decepción. Si aún la decepción no ha llegado, veamos el ejemplo detalladamente:
<table style="border: solid 2px red;"> <tbody> <tr> <td style="border: solid 2px green;">Este es un item Pantilla</td> </tr> <tr> <td style="border: solid 2px blue;">en una columna</td> </tr> </tbody> </table>
Es indiscutible que podemos agregar controles a gusto y eso es muy bueno, pero hay algo imprescindible que falta y son, sin dar más rodeos, los datos. De poco serviría poder crear columnas personalizadas si no tuviésemos la posibilidad de embeber datos (o sea propiedades públicas del elemento a enlazar) en dichas columnas.
Este será el momento indicado para presentar a las denominadas “expresiones de enlace a datos” o data-binding expressions.
Las expresiones de enlace a datos serán expresiones ubicadas entre los delimitadores <%# y %> que utilizarán a las funciones Eval y Bind para definir enlaces dentro de un data container.
La expresión Eval se utilizará para los llamados enlaces en un sentido, o sea enlace que van desde el origen de datos hacia el destino, mientras que la expresión Bind se utilizará en los llamados enlaces en dos sentidos, o sea desde y hacia el origen de datos. Utilizaremos la funcion Eval y a continuación mostraremos un ejemplo de lo que terminamos de comentar:
<table style="border: solid 2px red;"> <tbody> <tr> <td style="border: solid 2px green;">Este es un item Pantilla</td> </tr> <tr> <td style="border: solid 2px blue;">Donde hemos enlazado la propiedad V1 y vale <%# Eval("Valor1")%></td> </tr> </tbody> </table> <pre>
Si ejecutamos el código veremos que la columna se ha renderizado de la siguiente forma:
<%# Eval("Valor1")%>
La función Eval tomará por parámetro el nombre de la propiedad a embeber y contará con una sobrecarga para además incluir un formato para la expresión.
Los delimitadores <%# y %> permitirán también incluir además de Eval y Bind otras funciones y expresiones, por ejemplo:
<%# ((MiFunc(Eval("Valor1")) + MI_CONST) + "123").Substring(2)%>
Siendo:
public const string MI_CONST = "MiConstante"; public string MiFunc(object ParamValor1) { return "MiFunc " + ParamValor1.ToString(); }
Representará una expresíon válida donde como puede verse se han incluido una constante y un método personalizado definidos en el código de la pagina como públicos, para que puedan accederse desde el markup aspx.
Cabe mencionar que en realidad este tipo de expresiones van más allá de de los data containers y podrán definirse en cualquier sección de la página, será la funcion Eval y Bind las cuales, por lógica, solo tendrán sentido y podrán invocarse dentro de un data container.
CommandField
Otro tipo de columna posible es la del tipo CommandField, una columna de este tipo permitirá renderizar un botón por comando disponible para cada item de la grilla, y antes de preguntarnos que es un comando, diremos que es una acción y en este contexto las acciones posibles son: Seleccionar, Editar, Eliminar o Insertar. Dichas acciones serán ejecutar al presionarse el botón renderizado por la grilla
Comando Seleccionar
Comenzaremos por ver el comando seleccionar. El comando seleccionar permitirá, valga la redundancia, seleccionar una fila de la grilla. Para ver de que se trata esto modificaremos nuestro último ejemplo e incorporaremos una columna de este tipo:
<asp:CommandField ButtonType="Button" ... />;
Hemos definido a la columna y que la misma mostrará los comandos como botones (en la propiedad ButtonType). Las opciones posibles son: Button, Image y Link.
Para concluir el ejemplo incluiremos un comando seleccionar :
<asp:CommandField ButtonType="Button" ShowSelectButton="true"/>
Como puede verse lo único que hemos tenido qu hacer es establecer la propiedad ShowSelectButton en verdadero.
Al ejecutar el código veremos que la columna es generada y la misma ha incorporado el botón “Select”.
Si necesitamos efectuar alguna acción cuando una fila es seleccionada podremos utilizar los eventos SelectedIndexChanging (que se disparará cuando la fila está siendo seleccionada) y SelectedIndexChanged (que se disparará cuando la fila ya ha sido seleccionada).
Un caso muy frecuente donde se utilizará la selección de filas es cuando se necesite incorporar un formulario de detalle de un item mostrado en la grilla, pero seguramente existirán unos cuantos contextos más donde será útil contar con la posibilidad de seleccionar una fila para realizar algún tipo de operación.
Existirán además las propiedades SelectedDataKey y SelectedValue.
Pero antes de hablar de ellas, será lo mejor tomarnos un tiempo para presentar a las propiedades DataKeyNames y DataKeys. La propiedad DataKeyNames permitirá establecer u obtener los nombres de las propiedades que serán clave primaria en los items que mostrará la grilla.
Quizás el concepto de clave primaria parezca inesperado y muy relacionado con persistencia y base de datos, y aunque en el uso practico existirá frecuentemente una relación muy estrecha entre persistencia y el uso de la propiedad DataKeyNames, el concepto es genérico y más abarcativo, y la idea de esta propiedad (más allá del uso práctico que le demos) es la de poder indicar cuales propiedades permitirán reconocer a un item del conjunto completo de items que se enlazarán en la grilla.
Si por ejemplo sabemos que para nuestra clase, las propiedades Valor1 y Valor2 son suficientes para identificar univocamente a un elemento, podremos establecer la propiedad DataKeyNames de la siguiente forma:
<asp:GridView runat="server" ID="GV1" AutoGenerateColumns="false" DataKeyNames="Valor2,Valor1">
Por otra parte, propiedad DataKeys nos permitirá conocer, en base al indice de una fila de la grilla los valores de las propiedades clave primaria del elemento enlazado en la posicion indicada. La propiedad devolvera un objeto del tipo DataKeyArray el cual implementa la interfaz IEnumerable, por lo que será posible acceder via indice a los elementos que componene el array, en codigo:
GV1.DataKeys[0]
El ejemplo devolverá las propiedades que son clave primaria del elemento enlazado en la primer fila de la grilla, las mismas serán devueltas en un objeto del tipo DataKey. El objeto DataKey contendrá una propiedad llamada Values que contendrá un diccionario donde la clave/valor serán el “Nombre de la propiedad”/”Valor de la propiedad”.
El objeto del tipo DataKey también contará con la propiedad Value, la cual contendrá el valor del primer campo clave de la lista establecida en la propiedad DataKeyNames. A continuación se muestra un ejemplo de lo mencionado previamente.
De esta forma, al conocer las propiedades que son claves primarias del elemento seleccionado podremos encontrarlo en el grupo de elementos del origen de datos para efectuar algun tipo de operación.
Comando Eliminar
Hasta el momento, no habíamos considerado la posibilidad de efectuar cambios en el origen de datos, al menos hasta ahora. A continuación veremos de qué manera es posible modificar el origen de datos a partir modificar información en el control GridView, ya sea elimininando un ítem o modificando los valores en la grilla.
Antes que nada, si esperamos poder eliminar un ítem y que la eliminación sea reflejada, deberemos mantener el estado de la lista (Lst) que estábamos utilizando, ya que si la recargamos todo el tiempo nunca veremos cómo la misma ha sido modificada. A continuación ajustaremos un poco el ejemplo inicial para poder mantener el estado de la lista Lst en el ViewState y que la eliminación y modificación de los ítems sobre la misma pueda reflejarse.
[Serializable] public class MiElem { public string Valor1 { set; get; } public int Valor2 .... } protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { List Lst = new List(); Lst.Add(new MiElem() { Valor1 = "V1", Valor2 = 1, Valor3 = DateTime.Now.AddDays(1), Valor4 = true, Valor5 = "imagen1.gif", Valor6 = @"~\Imagenes\imagen1.gif" }); Lst.Add(new MiElem() { Valor1 = "V2", Valor2 ..... ViewState["Lst"] = Lst; } } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); BindGrid(GV1); } private void BindGrid(GridView Grd) { List Lst = ViewState["Lst"] as List; Grd.DataSource = Lst; Grd.DataBind(); }
Si observamos el ejemplo podemos ver que sigue casi tan simple como antes, simplemente en este nuevo código estamos guardando la lista en el ViewState.
Volviendo al markup, estableceremos la propiedad ShowDeleteButton en verdadero en el CommandField como se muestra en el ejemplo:
<asp:CommandField ButtonType="Button" ShowSelectButton="true" ShowDeleteButton="true" />
Al ejecutarlo veremos que un nuevo botón es renderizado.
Si lo presionamos obtendremos inmediatamente un error similar al siguiente:
El cual nos indicará que la grilla ha disparado un evento RowDeleting pero el mismo no se ha manejado.
Este comportamiento no debería sorprendernos ya que la grilla (a diferencia del caso de selección de un ítem) no tiene forma de saber cómo debe eliminar un ítem por lo que simplemente disparará el evento RowDeleting esperando que una función lo atienda y resuelva el problema de la eliminación, pero hasta el momento dicha función no existe. Como nosotros somos los únicos que sabemos de qué manera eliminar un ítem, deberemos ser los creadores de la función y deberemos además asignársela al evento RowDeleting, tal como se muestra en el ejemplo:
<asp:GridView runat="server" ID="GV1" AutoGenerateColumns="false" DataKeyNames="Valor2,Valor1" OnRowDeleting="GV1_RowDeleting" >
protected void GV1_RowDeleting(object sender, GridViewDeleteEventArgs e) { List Lst = ViewState["Lst"] as List; GridView ctlGV = sender as GridView; Lst.Remove(Lst.Single(MiE => MiE.Valor1 == (ctlGV.DataKeys[e.RowIndex].Values["Valor1"] as string) && MiE.Valor2 == (int)ctlGV.DataKeys[e.RowIndex].Values["Valor2"])); ViewState["Lst"] = Lst; }
El parámetro “e” del tipo GridViewDeleteEventArgs indicará entre otras cosas el número de fila en el cual se ha presionado el botón de eliminación en la grilla, podremos encontrar dicho valor en la propiedad RowIndex. Dependerá de nosotros saber que elemento ha sido cliqueado a partir del índice pasado y eliminarlo, para nuestro caso donde intentaremos no agregar complejidad al ejemplo y mantendremos nuestra suposición que Valor2, Valor1 son propiedades primarias, la eliminación se reducirá a recuperar los valores de dichas propiedades a partir del índice (para lo que se ha recurrido a consultar la propiedad DataKeys de la grilla que hemos visto hace apenas algunos parrafos), buscar el ítem con dichos valores y eliminarlo, como estamos enlazando la grilla a último momento el cambio será reflejado. En unas pocas líneas de código hemos realizado esta tarea.
Comando Editar
De forma similar al comando de eliminación, será posible incluir un botón de edición estableciendo la propiedad ShowEditButton en verdadero y atendiendo al evento RowEditing.
Cuando el botón de edición sea presionado el evento RowEditing será invocado:
protected void GV1_RowEditing(object sender, GridViewEditEventArgs e) { (sender as GridView).EditIndex = e.NewEditIndex; }
Con este código, la fila indicada pasará a modo edición. Podríamos preguntarnos
¿Qué pasará con las columnas cuando la fila pase a modo edición? y la respuesta es que dependerá del tipo de columna, las columnas del tipo BoundField renderizarán un cuadro de texto para que el usuario ingrese un nuevo valor y mostrará el valor original de la propiedad DataField de la columna.
Las columnas del tipo ImageField también renderizarán un cuadro de texto para que el usuario ingrese el nuevo valor para la propiedad DataField mostrando el valor original de la misma.
Las columnas del tipo CheckBoxField habilitarán el control CheckBox renderizado para que su valor pueda modificarse.
Estos tres tipos de columnas contarán con la propiedad ReadOnly cuyo valor por defecto es false, si la misma se establece en”verdadero”, la columna no pasará a modo edición aunque la fila lo haya hecho.
Las columnas del tipo CommandField en modo edición renderizarán los botones “Actualizar” y “Cancelar” para que el usuario pueda concluir la operación sobre la fila que había decidido editar, en unos párrafos retomaremos este tema y a continuación veremos la apariencia que la grilla del ejemplo toma en modo edición:
Si deseamos que este tipo de columnas pueda editarse deberemos ser nosotros mismos quienes definamos la forma que adoptará la columna en esta situación, por ese motivo las columnas del tipo TemplateField incluirán además del un ItemTemplate un EditItemTemplate donde podrá definirse la plantilla que se utilizará durante la edición.
A continuación agregaremos una plantilla de edición a la columna TemplateField del ejemplo que estamos utilizando:
<table style="border: solid 2px red;"> <tbody> <tr> <td style="border: solid 2px green;">Este es un item Pantilla</td> </tr> <tr> <td style="border: solid 2px blue;">Donde hemos enlazado la propiedad V1 y vale <%# Eval("Valor1")%></td> </tr> </tbody> </table> <table style="border: solid 2px red;"> <tbody> <tr> <td style="border: solid 2px green;">Este es un item Pantilla</td> </tr> <tr> <td style="border: solid 2px blue;">Donde hemos enlazado la propiedad V1 y vale</td> </tr> </tbody> </table>
Luego de esta modificación, al presionarse el botón Editar, notaremos que la columna de dicha fila adoptará la siguiente forma:
Volviendo a la columna del tipo CommandField, que había quedado renderizada con los botones “Actualizar” o “Cancelar”, si el usuario los presiona obtendrá errores similares a los siguientes:
Para el caso del botón “Actualizar” deberemos atender al evento RowUpdating del control GridView tal como se muestra en el ejemplo:
<asp:GridView ... OnRowUpdating="GV1_RowUpdating" ...>
protected void GV1_RowUpdating(object sender, GridViewUpdateEventArgs e){
El parámetro “e” del tipo GridViewUpdateEventArgs contará con la propiedad RowIndex que nos permitirá conocer el número de fila que ha sido cliqueado en la grilla. Si observamos las propiedades del parámetro “e” veremos que además de la propiedad RowIndex también se encuentran las propiedades: Keys,OldValues y NewValues, pero para quienes se sientan afortunados al hacer esta observación y hayan sacado algunas conclusiones, lamento informar que dichas propiedades no serán de utilidad en nuestro caso, ya que cuando el origen de datos es establecido en la propiedad DataSource, dichas propiedades estarán vacías (otra forma de establecer un origen de datos es utilizar la propiedad DataSourceID, pero este detalle quedará fuera del alcance de este artículo).
La forma de acceder a los valores que ha modificado el usuario será a través de la colección de filas y celdas de la grilla, para realizar esta tarea nos valdremos de la propiedad RowIndex tal como se muestra en el ejemplo:
protected void GV1_RowUpdating(object sender, GridViewUpdateEventArgs { GridView ctlGV = sender as GridView; TextBox txtCol0_BoundField = ctlGV.Rows[e.RowIndex].Cells[0].Controls[0] as TextBox; TextBox txtCol1_ImageField = ctlGV.Rows[e.RowIndex].Cells[1].Controls[0] as TextBox; CheckBox txtCol2_CheckBoxField = ctlGV.Rows[e.RowIndex].Cells[2].Controls[0] as CheckBox; TextBox txtCol4_TemplateField_txtVV1 = ctlGV.Rows[e.RowIndex].Cells[4].FindControl("txtVV1") as TextBox; ...
En las últimas líneas de código puede observarse como se accede al control de edición de la fila editada utilizando la propiedad RowIndex, el control accedido dependerá del tipo de columna.
Para el caso de las columnas del tipo TemplateField, como la misma puede contener cualquier cantidad y tipo de controles deberá emplearse el método FindControl tal como se muestra en la última línea del ejemplo, la única precaución que deberá tomarse es asignar un ID a los controles que serán accedidos (tal como hemos hecho en el ejemplo asignando ID=»txtVV1″) pero imagino que esta restricción no traerá ningún tipo de inconveniente.
Una vez que los valores hayan sido recuperados y el elemento se haya actualizado, deberá pasarse explícitamente la grilla a modo visualización estableciendo la propiedad EditIndex con el valor -1, a continuación se muestra la actualización sobre nuestro ejemplo:
protected void GV1_RowUpdating(object sender, GridViewUpdateEventArgs { GridView ctlGV = sender as GridView; TextBox txtCol0_BoundField = ctlGV.Rows[e.RowIndex].Cells[0].Controls[0] as TextBox; TextBox txtCol1_ImageField = ctlGV.Rows[e.RowIndex].Cells[1].Controls[0] as TextBox; CheckBox txtCol2_CheckBoxField = ctlGV.Rows[e.RowIndex].Cells[2].Controls[0] as CheckBox; TextBox txtCol4_TemplateField_txtVV1 = ctlGV.Rows[e.RowIndex].Cells[4].FindControl("txtVV1") as TextBox; List Lst = ViewState["Lst"] as List; MiElem EditedElem = Lst.Single(MiE => MiE.Valor1 == (ctlGV.DataKeys[e.RowIndex].Values["Valor1"] as string) && MiE.Valor2 == (int)ctlGV.DataKeys[e.RowIndex].Values["Valor2"]); EditedElem.Valor5 = txtCol1_ImageField.Text; EditedElem.Valor4 = txtCol2_CheckBoxField.Checked; if (string.Compare(txtCol0_BoundField.Text, txtCol4_TemplateField_txtVV1.Text) == 1) EditedElem.Valor1 = txtCol4_TemplateField_txtVV1.Text; else EditedElem.Valor1 = txtCol0_BoundField.Text; Lst[e.RowIndex] = EditedElem; ctlGV.EditIndex = -1; }
La sentencia string.Compare se ha incluido solo para darle un poco de emoción al código.
De forma similiar a lo ocurrido con el botón “Aceptar”, para el caso del botón “Cancelar” deberá atenderse al evento RowCancelingEdit, de la siguiente forma:
<asp:GridView ... onrowcancelingedit="GV1_RowCancelingEdit" ... >
protected void GV1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e) { (sender as GridView).EditIndex = -1; }
En el código previo simplemente hemos pasado a la grilla a modo visualización nuevamente.
Comando Insertar
Cabe mencionar que aunque los campos del tipo CommandField, poseen una propiedad denominada ShowInsertButton, esta opción no estará disponible para el caso del control GridView, de este hecho podemos concluir que el mismo no ha sido diseñado para esta tarea.
Con este último comentario le hemos dado fin a las columnas del tipo CommandField, quedando pendientes solamente las columnas del tipo ButtonField que veremos a continuación.
ButtonField
Las columnas de este tipo permitirán renderizar una propiedad (o un texto) como un botón el cual ejecutará una acción al ser presionado.
A continuación se muestra un ejemplo de definición de este tipo de campo.
<asp:ButtonField ButtonType="Button" DataTextField="Valor1" />
Para poder responder a la acción de presionar el botón deberá utilizarse el evento RowCommand de la grilla, a continuación se muestra un ejemplo:
<asp:ButtonField ButtonType="Button" DataTextField="Valor1" />
protected void GV1_RowCommand(object sender, GridViewCommandEventArgs e)
El parámetro “e” del tipo GridViewCommandEventArgs contará con las propiedades CommandSource, CommandArgument y CommandName. La propiedad CommandSource contendrá el control que ha sido presionado mientras que la propiedad CommandArgument indicará el número de fila del botón que se ha presionado, el problema es que el numero de fila del botón presionado nos es insuficiente si no conocemos en número de columna.
A continuación veremos cómo resolver esta ambigüedad. Si observamos nuevamente a la columna del tipo ButtonField, notaremos que posee una propiedad llamada CommandName, el valor establecido en dicha propiedad será pasado al evento RowCommand en la propiedad CommandName del parámetro “e” y de esta forma podremos resolver la ambigüedad de los botones. Para que quede claro crearemos dos columnas del tipo ButtonField en nuestro ejemplo de la siguiente forma:
<asp:ButtonField ButtonType="Button" DataTextField="Valor1" CommandName="Comando1" /> <asp:ButtonField ButtonType="Button" DataTextField="Valor2" CommandName="Comando2" />
Al presionarse el primer botón (de la primera fila, como lo indica la propiedad CommandArgument) obtendremos el siguiente resultado:
Un detalle más que importante que debemos saber es que el evento RowCommand en realidad se disparará al presionarse cualquier control de los tipos Button, ImageButton o LinkButton dentro de la grilla, y cuando decimos todos, queremos decir todos sin excepción, incluyéndose a los botones renderizados por las columnas del tipo CommandField o los existentes dentro de una columna del tipo TemplateField (este es un buen tip para cuando se necesite atender a eventos de botones dentro de un tempate).
Por este motivo deberemos antes de ejecutar una acción en el evento RowCommand no olvidar consultar el parámetro e.CommandName.
Otro detalle importante es que los botones renderizados dentro de una columna del tipo CommandField utilizarán nombres de comandos propios y el numero de fila seleccionada será enviado en la propiedad CommandArgument del parámetro “e” tal como ocurre con los botones creados en las columnas del tipo ButtonField.
Con los botones creados dentro de una columna del tipo TemplateField al ser controles convencionales deberemos resolver nosotros mismos el problema de obtener la fila en la que el botón fue cliqueado y hacer llegue hasta el parámetro CommandArgument del evento RowCommand, por suerte esta tarea es sencilla, si observamos los controles Button, ImageButton o LinkButton, veremos que poseen las propiedades CommandName y CommandArgument, en la propiedad CommandName deberemos establecer el nombre del comando, tal como lo estábamos haciendo con las columnas del tipo ButtonField. En la propiedad CommandArgument deberemos establecer el número de fila de la grilla, para poder llevar a cabo esta tarea utilizaremos la propiedad DataItemIndex del ItemTemplate, tal como se muestra en el ejemplo:
<asp:TemplateField> <ItemTemplate> ... ... <asp:Button runat="server" Text="Botón" CommandName="BtnInGrid" CommandArgument='<%# Container.DataItemIndex %>' /> <asp:ImageButton ID="Button1" runat="server" ImageUrl="~\Imagenes\Imagen1.gif" CommandName="ImgInGrid" CommandArgument='<%# Container.DataItemIndex %>' /> <asp:LinkButton ID="Button2" runat="server" Text="Botón enlace" CommandName="LnkInGrid" CommandArgument='<%# Container.DataItemIndex %>' />
Con este último comentario, hemos explicado todos los tipos de columnas. Para quien no se sienta muy cómodo editando el markup .aspx a mano le resultará muy útil saber que es posible configurar las columnas y otros detalles más del control GridView desde el diseñador:
Enlace contra DataReaders, DataTables y DataSets
Aunque hemos enlazado el control GridView a un elemento que implementaba la interfaz ICollection, También será posible enlazarlo a un DataTable, a un DataSet y a un DataReader, en tal caso, simplemente deberá utilizarse el nombre de los campos involucrados donde estábamos empleando el nombre de las propiedades y todo el esquema que hemos visto seguirá funcionando de la misma forma. A continuación modificaremos nuestro ejemplo para que utilice estas opciones y para hacerlos más entretenidos desde ahora en adelante optaremos por utilizar datos persistidos en una base de datos, en nuestro ejemplo utilizaremos SQL Server 2008 Enterprise Edition y crearemos no más que la siguiente tabla:
SqlConnection SqlCn = new SqlConnection("Data Source..."); SqlCommand SqlCmd = new SqlCommand("SELECT * FROM GV_T1", SqlCn); SqlCmd.Connection.Open(); GV1.DataSource = SqlCmd.ExecuteReader(System.Data.CommandBehavior.CloseConnection); GV1.DataBind();
Veremos que la el control GridView funcionará correctamente.
De igual forma podríamos haber realizado el enlace contra una DataTable:
... SqlDataAdapter SqlDA = new SqlDataAdapter(SqlCmd); DataTable DT = new DataTable(); SqlDA.Fill(DT); GV1.DataSource = DT; ...
O un DataSet, especificando la tabla a enlazar en la propiedad DataMember (si es que no se desea utilizar el primer DataTable del DataSet)
... SqlDataAdapter SqlDA = new SqlDataAdapter(SqlCmd); DataSet DS = new DataSet(); SqlCmd.CommandText = "SELECT * FROM GV_T1"; SqlDA.Fill(DS,"T1_A"); SqlCmd.CommandText = "SELECT TOP 1 * FROM GV_T1 WHERE Valor2=100"; SqlDA.Fill(DS, "T2_V2EQ100"); GV1.DataSource = DS; GV1.DataMember = "T1_V2EQ100"; ...
Ordenamiento y paginación
La grilla ofrecerá además la posibilidad de ordenar la información mostrada por alguna columna, para activar esta característica deberá establecerse la propiedad AllowSorting del control GridView en verdadero y para cada columna por la que se desee ordenar la grilla deberá establecerse un valor para la propiedad SortExpression. Además deberá establecerse la propiedad HeaderText (si es que no se trata de una columna autogenerada) para establecer el nombre del encabezado, tal como se muestra a continuación:
<asp:GridView ... AllowSorting="true"> <asp:BoundField DataField="Valor1" SortExpression="Valor1" HeaderText="Valor1" />
Luego de estas modificaciones notaremos que el encabezado de la columna modificada posee un enlace.
La propiedad SortDirection no podremos utilizarla cuando establezcamos el origen de datos en la propiedad DataSource (como ya hemos comentado previamente, otra forma de establecer un origen de datos es utilizar la propiedad DataSourceID, aunque no veremos esta posibilidad en este articulo) pero podremos utilizar por ejemplo el Viewstate para almacenar la dirección del ordenamiento sin dificultades adicionales.
Otra de las características de la grilla es que brindará la posibilidad de mostrar los resultados paginados, para activar esta posibilidad, la grilla contará con la propiedad AllowPaging que deberá establecerse en verdadero.
Podrá también definirse el Tamaño de la página (a través de la propiedad PageSize, el valor por defecto es 10), la página actual podrá obtenerse y modificarse a través de la propiedad PageIndex (En base cero). En el siguiente ejemplo so muestra una definición posible:
<asp:GridView ... AllowSorting="true" PageSize="4" PageIndex="1" AllowPaging="true">
(sender as GridView).PageIndex = e.NewPageIndex;
Un detalle interesante está relacionado con el ítem seleccionado en la grilla, la paginación y el ordenamiento y es que cuando una fila es seleccionada la misma quedará seleccionada aunque se modifique el ordenamiento o se cambie de página en la grilla, esto puede ser bastante confuso para el usuario, por lo que al cambiar de pagina u ordenar la grilla deberá controlarse manualmente la fila seleccionada, ya sea deseleccionando cualquier posible fila en un ordenamiento o paginación o buscando alguna metodología para mantener y refrescar el ítem seleccionado, a continuación se muestra un ejemplo donde, como seguramente sucederá en muchos ambientes de mundo real, se ha combinado la posibilidad de ordenar y filtrar una grilla conjuntamente:
protected void Page_Load(object sender, EventArgs { if (!Page.IsPostBack) ViewState["SortDirection"] = "ASC"; } protected override void OnPreRender(EventArgs e) { base.OnPreRender(e); BindGrid(GV1); } ... ...... protected void GV1_Sorting(object sender, GridViewSortEventArgs e) { string strSortExpression = ViewState["SortExpression"] as string; string strSortDirection = ViewState["SortDirection"] as string; if (strSortExpression == e.SortExpression) strSortDirection = strSortDirection == "ASC" ? "DESC" : "ASC"; else strSortDirection = "ASC"; ViewState["SortExpression"] = e.SortExpression; ViewState["SortDirection"] = strSortDirection; //Deselección de item (sender as GridView).SelectedIndex = -1; } protected void GV1_PageIndexChanging(object sender, GridViewPageEventArgs e) { ViewState["PageIndex"] = e.NewPageIndex; //Deselección de item (sender as GridView).SelectedIndex = -1; } ... ....... private void BindGrid(GridView Grd) { string strSortExpression = ViewState["SortExpression"] as string; string strSortDirection = ViewState["SortDirection"] as string; int? intPageIndex = ViewState["PageIndex"] as int?; string strCmdText = "SELECT * FROM GV_T1"; if (!string.IsNullOrEmpty(strSortExpression)) strCmdText = string.Format("SELECT * FROM GV_T1 ORDER BY {0} {1}", strSortExpression, strSortDirection); if (intPageIndex.HasValue) Grd.PageIndex = intPageIndex.Value; using (SqlConnection SqlCn = new SqlConnection("Data Source=...")) { using (SqlCommand SqlCmd = new SqlCommand(strCmdText, SqlCn)) { SqlCmd.Connection.Open(); SqlDataAdapter SqlDA = new SqlDataAdapter(SqlCmd); DataSet DS = new DataSet(); SqlDA.Fill(DS, "T1_A"); Grd.DataSource = DS; Grd.DataMember = "T1_A"; Grd.DataBind(); SqlCmd.Connection.Close(); } } }
Casi finalizado el artículo hemos conseguido enlazar el control GridView a varios orígenes de datos, emplear todos los tipos de columnas disponibles, modificar datos en la grilla y transferirlos al origen de datos, ordenar y paginar los datos en el control.
Un comentario es que será posible realizar estas tareas sin escribir código, para tal fin podrán utilizarse los controles SqlDataSource y ObjectDataSource. Lamentablemente estas posibilidades las dejaremos fuera del alcance este artículo.
Otro breve comentario, es que aunque no lo hemos visto en este artículo (ya que el mismo estaba enfocado específicamente a enlace) el control GridView ofrecerá una amplia funcionalidad para ajustar su apariencia, y contará incluso con formatos predefinidos a los cuales podrá accederse desde el diseñador en la opción “Auto Format”.
Con este último comentario le doy fin al artículo esperando, como siempre, que lo visto haya sido de utilidad. Dejo también un proyecto (en Visual Studio 2010) con algunos ejemplos.