Twitter Facebook RSS Feed

lunes, 03 de mayo de 2010 a las 14:28hs por Dario Krapp

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:


El control GridView ha renderizando una grilla y ha generando, como anticipamos, una fila por cada elemento enlazado y una columna por cada propiedad pública del mismo.

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.


Grilla con campos BoundField
Para los casos donde se personalizarán las columnas, como en este ejemplo, deberá recordarse establecer la propiedad AutoGenerateColumns en falso ya que en caso contrario el control añadirá luego de las columnas definidas, todas las columnas autogeneradas. Podrá establecerse un titulo para las columnas definidas a través de la propiedad HeaderText:

<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:


Columnas de diversos tipos

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:


Columna plantilla
El valor de la propiedad Valor1 del ítem ha sido embebido en la ubicación de de la expresión:

<%# 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”.


Comando Seleccionar
Al presionar el botón “Select” en la primer fila, si inspeccionamos las propiedades de la grilla (por ejemplo en el evento SelectedIndexChanged) veremos que las mismas se han establecido con el valor de la fila seleccionada, como puede verse a continuación:


Depuración
En conclusión, el comando se ha ejecutado.
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.

Depuración DataKeys
Volviendo las propiedades SelectedDataKey y SelectedValue, con lo que ya hemos visto, diremos que cuando un item de la grilla es seleccionado, la propiedad SelectedDataKey contendrá el DataKey correspondiente a la fila seleccionada y la propiedad SelectedValue contendra el valor (del primer campo clave de la lista establecida en la propiedad DataKeyNames) del item seleccionado.
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:

Error en evento RowDeleting

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:

Edición de datos
Como puede observarse, las columnas del tipo HyperLinkField y TemplateField no se han modificado en modo edición, para el primer caso no habrá nada que podamos hacer, pero en el caso de las columnas del tipo TemplateField, podremos hacer y bastante. Si analizamos la situación notaremos no sería sensato esperar que en modo edición una columna del tipo TemplateField se ajustase automáticamente, ya que este tipo de columnas permitirá agregar los controles deseados a voluntad.

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:

Edición de template
Cabe mencionar que existirán además un HeaderTemplate, un FooterTemplate y un AlternatingItemTemplate. No entraremos en estos detalles, pero creo sus nombres son todo un indicio de su utilidad.

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:

Error Actualizando o Cancelando
Obviamente si la grilla no sabía cómo eliminar un ítem, es de esperar que tampoco sepa como actualizarlo, en realidad tampoco sabrá como cancelar una edición. Seremos nosotros nuevamente los encargados de resolver tanta ignorancia.

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" />

ButtonField en control GridView
En la imagen previa puede observarse como el campo Valor1 ha sido renderizado en la grilla, la propiedad ButtonType admitirá las opciones: Button, Image y Link, mientras que en la propiedad DataTextField deberá indicarse el nombre de la propiedad que elemento enlazado a mostrará en el botón. Opcionalmente podrá emplearse la propiedad Text para utilizar una cadena de texto constante.

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:

Comando en ButtonField
Mientras que al presionarse el segundo (también de la primera fila) se obtendrá lo siguiente:

Comando en ButtonField
De esta forma podremos diferenciar entre dos o “n” botones o incluso, si lo deseamos por algún motivo, podremos agrupar a varios bajo un mismo nombre de comando.

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:

Edición de columnas 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:

Tabla en SQL Server
Donde hemos tenido el cuidado de hacer que el nombre de los campos coincida con los que habíamos definido en la clase como propiedades, además hemos establecido a la clave primaria con los campos Valor1 y Valor2, De esta forma, la propiedad DataKeyNames (que era Valor2, Valor1) de la grilla reflejará esta definición. Si ajustamos un poco el código, como se muestra en el siguiente ejemplo:

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.

Ordenamiento de grillas
Si presionamos el enlace obtendremos un error indicando que el evento Sorting no ha sido manejado, si manejamos dicho evento notaremos que la función destinada a tal fin poseerá un parámetro denominado “e” del tipo GridViewSortEventArgs cuya propiedad SortExpression devolverá el valor establecido en la propiedad con el mismo nombre de la columna cliqueada. Para nuestro ejemplo obtendremos el valor “Valor1”.
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">

Paginación de grillas
En el mismo puede observarse que los datos se muestran de a cuatro filas. También puede observarse el paginador que ha renderizado la grilla y en el mismo se distingue que la página seleccionada es la segunda. Si presionamos algún enlace del paginador obtendremos un error, dado que deberemos manejar el evento PageIndexChanging, si manejamos dicho evento veremos que la función destinada a tal fin poseerá un parámetro denominado “e” del tipo GridViewPageEventArgs, el mismo contará con la propiedad NewPageIndex donde el número de página cliqueado será pasado. Para que la grilla pase a la página solicitada simplemente deberemos establecer la propiedad PageIndex de la grilla con el valor pasado en la propiedad NewPageIndex del parámetro “e” y la paginación estará funcionando. A continuación se muestra un ejemplo:

(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.

2 comentarios »

  1. slnit dice:

    Muy buen aporte, gracias, me sirvió mucho tu idea. Saludos.

  2. Angelo dice:

    Acá les dejo un enlace, donde encontré la forma de cambiar las cabeceras de las columnas en tiempo de ejecución.

    http://www.systemdeveloper.info/2014/01/change-header-text-of-columns-in.html

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.