domingo, 8 de agosto de 2010

[GridView] ITemplate – Columnas definidas en runtime

 

Introducción

Suelen darse los escenarios en donde se necesite editar distintas entidades pero haciendo uso de un único control gridview.

Una respuesta rápida a este problema seria hacer uso de la propiedad AutoGenerateColumns en true, para que los datos que se le proporciona al control defina las columnas que debe mostrar, lo malo de esta opción es que se pierde control sobre la grilla.

Otra alternativa interesante podría ser el uso de la clase BoundField con esta seria posible definir columnas en tiempo de ejecución, si bien podría ser la solución en la mayoría de los caso, esta no permite un control total del témplate que se debe usar en al edición de las columnas.

La solución definitiva al problema esta en la implementación de témplates de columnas, estas clases especializadas contendrán el código del témplate que define, para que esto se posible se necesitara implementar la interfaz ITemplate 

El modelo del ejemplo de código planteado hace referencia a dos listados, uno de notebooks y otro de televisores, ambos con distintas columnas por mostrar, pero haciendo uso de un solo control de grilla y la definición de las columnas de forma explicita en runtime.

Uso del BoundField

La definición de las columnas mediante esta clase podrá apreciarse en el formulario de nombre “GridViewBoundField.aspx”

public partial class GridViewBoundField : System.Web.UI.Page
{
    protected void Page_Init(object sender, EventArgs e)
    {
        DefinirColumnasNotebook();
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            GridView1.DataSource = DataAccess.ObtenerListaNotebook();
            GridView1.DataBind();
        }
    }

    private void DefinirColumnasNotebook()
    {
        //
        // Se define el campo dentro de la grilla, 
        // para poder identificar cada item 
        //
        GridView1.DataKeyNames = new string[] { "Id" };

        GridView1.Columns.Clear();

        BoundField tempDesc = new BoundField();
        tempDesc.HeaderText = "Descripcion Producto";
        tempDesc.DataField = "Descripcion";
        GridView1.Columns.Add(tempDesc);

        BoundField tempPrecio = new BoundField();
        tempPrecio.HeaderText = "Precio";
        tempPrecio.DataField = "Precio";
        GridView1.Columns.Add(tempPrecio);

    }
}

Se notara en el código que las columnas son definidas en el evento Page_Init, mientras que los datos son cargadas en el Page_Load

Esto es porque la definición de las columnas al ser dinámicas se deberán crear cada vez que se realice un postback, mientras que los datos pueden volver a bindear a la grilla, o no, eso dependerá de la funcionalidad que se quiera lograr

Definición de ITemplate

La definición de las columnas por medio de la creación de templetes de columnas, podrá apreciarse en el formulario de nombre “GridViewITemplate.aspx”.

Para poder hacer uso de template de columnas en el control GridView, será necesario la implementación de a interfaz ITemplate, a continuación se vera el código de estas clases:

public class GridViewHeaderTemplate : ITemplate
{
    string text;

    public GridViewHeaderTemplate(string text)
    {
        this.text = text;
    }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        Literal lc = new Literal();
        lc.Text = text;

        container.Controls.Add(lc);

    }
}

public class GridViewEditTemplate : ITemplate
{
    private string columnName;

    public GridViewEditTemplate(string columnName)
    {
        this.columnName = columnName;
    }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        TextBox tb = new TextBox();
        tb.ID = string.Format("txt{0}", columnName);
        tb.EnableViewState = false;
        tb.DataBinding += new EventHandler(tb_DataBinding);

        container.Controls.Add(tb);
    }

    void tb_DataBinding(object sender, EventArgs e)
    {
        TextBox t = (TextBox)sender;

        GridViewRow row = (GridViewRow)t.NamingContainer;

        string RawValue = DataBinder.Eval(row.DataItem, columnName).ToString();

        t.Text = RawValue;
    }
}

public class GridViewItemTemplate : ITemplate
{
    private string columnName;

    public GridViewItemTemplate(string columnName)
    {
        this.columnName = columnName;
    }
    
    public void InstantiateIn(System.Web.UI.Control container)
    {
        Literal lc = new Literal();

        lc.DataBinding += new EventHandler(lc_DataBinding);

        container.Controls.Add(lc);

    }

    void lc_DataBinding(object sender, EventArgs e)
    {
        Literal l = (Literal)sender;

        GridViewRow row = (GridViewRow)l.NamingContainer;

        string RawValue = DataBinder.Eval(row.DataItem, columnName).ToString();

        l.Text = RawValue;
    }
}


public class GridViewItemCheckTemplate : ITemplate
{
    private string columnName;

    public GridViewItemCheckTemplate(string columnName)
    {
        this.columnName = columnName;
    }

    public bool CanEdit { get; set; }

    public void InstantiateIn(System.Web.UI.Control container)
    {
        CheckBox check = new CheckBox();
        check.ID = string.Format("chk{0}", columnName);
        check.Enabled = this.CanEdit;
        check.DataBinding += new EventHandler(check_DataBinding);

        container.Controls.Add(check);

    }

    void check_DataBinding(object sender, EventArgs e)
    {
        CheckBox check = (CheckBox)sender;

        GridViewRow row = (GridViewRow)check.NamingContainer;

        string value = DataBinder.Eval(row.DataItem, columnName).ToString();

        check.Checked = bool.Parse(value);
    }

}

Cada una representa un témplate de visualización y edición dentro del Gridview.

El método principal que debe implementarse es InstantiateIn(), dentro de este se definirá el o los control que conformen el témplate de columna para el estado especifico.

Algo que seguramente llamara la atención es el uso del evento DataBinding, el cual es usada para tomar los datos al momento de bindear cada fila de la grilla, este evento será ejecutado tantas veces como filas tenga.

Estas clases serán usadas para definir cada témplate de columna:

private void DefinirColumnasNotebook()
{
    //
    // Se define el campo dentro de la grilla, 
    // para poder identificar cada item 
    //
    GridView1.DataKeyNames = new string[] { "Id" };

    GridView1.Columns.Clear();

    //
    // Columna Descripcion
    //
    TemplateField tempDesc = new TemplateField();
    tempDesc.HeaderTemplate = new GridViewHeaderTemplate("Descripcion Producto");
    tempDesc.ItemTemplate = new GridViewItemTemplate("Descripcion");
    tempDesc.EditItemTemplate = new GridViewEditTemplate("Descripcion");
    GridView1.Columns.Add(tempDesc);

    //
    // Columna Precio
    //
    TemplateField tempPrecio = new TemplateField();
    tempPrecio.HeaderTemplate = new GridViewHeaderTemplate("Precio");
    tempPrecio.ItemTemplate = new GridViewItemTemplate("Precio");
    tempPrecio.EditItemTemplate = new GridViewEditTemplate("Precio");
    GridView1.Columns.Add(tempPrecio);
}

private void DefinirColumnasTelevisores()
{
    GridView1.DataKeyNames = new string[] { "Id" };

    GridView1.Columns.Clear();

    //
    // Columna Descripcion
    //
    TemplateField tempDesc = new TemplateField();
    tempDesc.HeaderTemplate = new GridViewHeaderTemplate("Descripcion Televidor");
    tempDesc.ItemTemplate = new GridViewItemTemplate("Descripcion");
    tempDesc.EditItemTemplate = new GridViewEditTemplate("Descripcion");
    GridView1.Columns.Add(tempDesc);

    //
    // Columna PrecioUnitario
    //
    TemplateField tempPrecio = new TemplateField();
    tempPrecio.HeaderTemplate = new GridViewHeaderTemplate("Precio Unitario");
    tempPrecio.ItemTemplate = new GridViewItemTemplate("PrecioUnitario");
    tempPrecio.EditItemTemplate = new GridViewEditTemplate("PrecioUnitario");
    GridView1.Columns.Add(tempPrecio);

    //
    // Columna EsPlasma
    //
    TemplateField tempEsPlasma = new TemplateField();
    tempEsPlasma.HeaderTemplate = new GridViewHeaderTemplate("Plasma");

    GridViewItemCheckTemplate esPlasmaItem = new GridViewItemCheckTemplate("EsPlasma");
    tempEsPlasma.ItemTemplate = esPlasmaItem;

    GridViewItemCheckTemplate esPlasmaEdit = new GridViewItemCheckTemplate("EsPlasma");
    esPlasmaEdit.CanEdit = true;
    tempEsPlasma.EditItemTemplate = esPlasmaEdit;

    GridView1.Columns.Add(tempEsPlasma);
}

 

Se define tanto el témplate del Ítem, como el de edición y encabezado, usando para ello el témplate que corresponda, se debe tener presente que también el tipo de dato a mostrar influye en la decisión de que témplate utilizar, un ejemplo muy claro lo representa el checkbox que marca si el televisor es de plasma o no, representado por un témplate que justamente dibuja un check en la celda.

También hay que comentar que no hay una forma única de crear las clases de témplate, estas podrían tomar la info mediante propiedades o pasarlas en el constructor. Un ejemplo de esto es la clase “GridViewItemCheckTemplate” la cual asigna el nombre del campo al cual vincula los datos, pero si debe permitir la edición o no, es asignada mediante una propiedad, tomando un valor por defecto en caso de no asignar valor.

La edición de un registro en la grilla implica todo un tema:

protected void GridView1_RowEditing(object sender, GridViewEditEventArgs e)
{
    GridView1.EditIndex = e.NewEditIndex;
    DataBindGrid();
}

protected void GridView1_RowCancelingEdit(object sender, GridViewCancelEditEventArgs e)
{
    GridView1.EditIndex = -1;
    DataBindGrid();
}

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    int Id = Convert.ToInt32(GridView1.DataKeys[e.RowIndex].Value);
    
    GridViewRow row = GridView1.Rows[e.RowIndex];

    if (Session["datos"] is List<Notebook>)
    {
        Notebook notebookActualizada = (from item in (List<Notebook>)Session["datos"]
                                         where item.Id == Id
                                         select item).FirstOrDefault();


        TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;
        notebookActualizada.Descripcion = Convert.ToString(txtDescripcion.Text);

        TextBox txtPrecio = row.FindControl("txtPrecio") as TextBox;
        notebookActualizada.Precio = Convert.ToInt32(txtPrecio.Text);

    }
    else if (Session["datos"] is List<Televisor>)
    {
        Televisor televisorActualizado = (from item in (List<Televisor>)Session["datos"]
                                            where item.Id == Id
                                            select item).FirstOrDefault();


        TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;
        televisorActualizado.Descripcion = Convert.ToString(txtDescripcion.Text);

        TextBox txtPrecio = row.FindControl("txtPrecioUnitario") as TextBox;
        televisorActualizado.PrecioUnitario = Convert.ToInt32(txtPrecio.Text);

        CheckBox chkEsPlasma = row.FindControl("chkEsPlasma") as CheckBox;
        televisorActualizado.EsPlasma = chkEsPlasma.Checked;
    }

    GridView1.EditIndex = -1;
    DataBindGrid();
}

Mediante los evento RowEditing y RowCancelingEdit, se controla que la fila este o no en estado de edición, esto indica a la grilla cuando debe cambiar el témplate de edición que se ha definido.

El evento RowUpdating actuara cuando se acepta la edición, es en este momento donde controla que tipo de lista se esta visualizando, como primer paso se localiza la entidad dentro de la colección que se había usado para bindear la grilla, para esta tarea se hizo uso de Linq.

Luego se toma la información de los controles que genero cada template, hay que remarcar en este punto que los template usan internamente el agregado de un prefijo con respecto al tipo de control que agregan, agregando este al nombre de la columna que se le asigno, por ejemplo:

tb.ID = string.Format("txt{0}", columnName);

Esta línea agrega el prefijo “txt” al nombre del campo, es por esto que luego al buscar el control se uso

TextBox txtDescripcion = row.FindControl("txtDescripcion") as TextBox;

En donde “Descripcion” es el nombre del campo, y “txt” el prefijo.

Al igual que se hizo con el BoundField, en este caso la definición de las columnas se realiza en el Page_Init

protected void Page_Init(object sender, EventArgs e)
{
    if (Session["datos"] == null || Session["datos"] is List<Notebook>)
    {
        DefinirColumnasNotebook();

    }
    else if (Session["datos"] is List<Televisor>)
    {
        DefinirColumnasTelevisores();
    }
}

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        Session["datos"] = DataAccess.ObtenerListaNotebook();
        DataBindGrid();
    }
}

private void DataBindGrid()
{
    GridView1.DataSource = Session["datos"];
    GridView1.DataBind();
}

Hay un método adicional que por ahí no este tan claro, DataBindGrid(), este método simplemente toma la info de session y bindea la grilla, este cache de información en session me pareció importante ya que por cada accion que se realiza la grilla debe ser bindeada a los datos, lo cual podría producir una sobrecarga de comunicación si en todo momento debe ir controla la db para buscar la información.

 

Ejemplos de Código

El ejemplo fue confeccionado con Visual Studio 2008.

 

[C#]
[VB.NET]

22 comentarios:

Augusto dijo...

Hola Leandro

Una consulta.
Como puedo llamar a otro formulario haciendo click en una celda del datagridview de otro formulario.
Me ha salido con una columna del tipo
DataGridViewButtonColumn
en visual .net 2008

algo asi
Private Sub DataGridView1_CellContentClick(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DataGridView1.CellContentClick
If e.ColumnIndex = 0 Then
Dim formulario As New Form2()
formulario.ShowDialog()
End If
End Sub

pero no me sale cuando la columna es del tipo
DataGridViewTextBoxColumn
que codigo podria implementar
para conseguir el mismo efecto

Muchas Gracias
Saludos

Augusto dijo...

Leandro masomenos resolvi este tema con un artículo tuyo.
Para complementar , quería saber como puedo hacer para truncar al form.Osea de un Form1
llamo al Form2, pero no quisiera crear varias instancias hasta que seleccione algo del Form2 para pintarlo en el Form1.
Me guie de este ejemplo tuyo
C# – DataGridView – Parte 3 – Pasaje de información entre grillas en distintos formulario.
Agradezco de antemano tu respuesta.
Saludos

Leandro Tuttini dijo...

hola Augusto

Que bueno que los articulos son de utilidad.

Bien con respecto al planteo me surgen algunas dudas, no veo la relacion entre crear instancias y pintar en un formulario.

Las acciones no me quedan claras, basicamente lo que pasas es informacion de un form al otro, y las instancias no son varias, son siempre las mismas que se pasan de in form al otro para poder comunicarse

Si la pregunta es algo compleja de explicar, te aconsejaria que la realices en los foros de MSDN:

Foro C#

Alli participo activamente, sino soy yo quien la responde seguro habra mucho colaboradores con ganas de ayudar.

saludos

Marlon José dijo...

Buenos dias Leandro. No he tenido mucho tiempo de revisar todo el material que has colocado en este sitio razón por la que te planteo esta pregunta: ¿puedo crear dataadapters asi: select * from productos where IdePro = @IdePro
usando el asistente?
Uso vs2010 express

Leandro Tuttini dijo...

hola Marlon

Aunque no lo has mencionado imagino que por asitente te refieres al que provee la creacion de DataSet tipados.

O sea cuando concluye el asistente obtiene un archivo de extension .xsd, en donde se definen datatable con campos y puede usar los TablaAdapter de cada datatable.

Si es asi podrias comentarte que si puedes hacer esto que mencionas, podrias extrender la funcionalidad, revisa este link:

Building a DAL using Strongly Typed TableAdapters and DataTables in VS 2005 and ASP.NET 2.0


Alli veras como agregar nuevos metodos con parametros en tu consulta para extender la funcionalidad.

De esta forma podrias crear un metodo que toma como parametro IdePro, que usaras luego en la query del TableAdapter.

Bueno espero esto sea de utilidad

saludos

MarlonVillamizar dijo...

Buenas noches Leandro.
Mi planteamiento: resulta que tengo un proyecto en donde si coloco un picturebox y le asigno una imagen no me muestra nada. tuve problemas con un módulo y observé que me falta el archivo Resources.Designer.cs. Estoy seguro que este es el problema. Ahora, no se como recuperarlo o reconstruirlo. Gracias por adelantado...

Leandro Tuttini dijo...

hola MarlonVillamizar

No has probado regenerando el archivo de recurso, como explica este link

Missing auto-generated Resources.Designer.cs file


Veras que este recursos puede encontrarse oculto en la carpeta "Properties" del proyecto:
imagen

saludos

Axel dijo...

Hola Leandro.

Tengo una duda que si bien es algo que parece superficial me gustaría si hay alguna forma practica de resolverla.

Resulta que no he querido usar AutoGenerateEditButton="True" y me he creado mis Clases para poder usar ImageButtons: Public Class GridViewItemCommandTemplate(aqui agregue un ImageButton para Edit y otro para Delete) y en GridViewEditCommandTemplate los ImageButtons para Update y Cancel, a pesar de que todo me funciona OK(auque tal vez no sea la mas adecuada ), me gustaria saber si hay alguna manera de InstantiateIn en ambas clases, crear/agregar algo en medio de ambos imagebuttons de manera que haya espacios en blanco entre los botones(tanto entre edit y delete como entre Update y cancel) porque quedan muy pegados.

ya ves porque te digo q es algo q parece muy superficial, pero me gustaría saber si hay una forma practica de lograrlo antes de intentar salidas no muy practicas. :D

Saludos y gracias de antemano

Leandro Tuttini dijo...

hola Axel

no se si entendi del todo el planteo, pero si estas diseñando tu mismo el template podrias agregar html que le de formato

por ejemplo podrias agregar un   para poner espacios, pero como se que es por codigo esto lo haces con el HtmlGenericControl
aunqru creo que con el LiteralControl tambien funciona

entonces creas una instancia

LiteralControl espacio = new LiteralControl(" ");

contenedor.Controls.Add(espacio);

es mas podrias hasta usar tablas de html si lo necesitas, recuerda que esta el HtmlTable como clase y puedes desde codigo .net armar html con estas

saludos

Edalo dijo...

Buenas Leandro, tengo una duda sobre Linq, si bien es cierto la reducción de código es bastante notoria con Linq, que me podrías decir del rendimiento, es este mayor, menor o igual??

Saludos y gracias

Leandro Tuttini dijo...

hola Edalo

te cuento que la situacion varia segun que buscas si sitieen uan lista con quizas 100 items si aplcias linq puedo asegurater que elr endimiento anda perfecto

ahora si hablamos de imaginemos 10000 items bueno aqui hay que tenee algo asm de cuidado, primero porque son items en memoria, y depsues porque si apcais alguna operacion compleja como ser la union con otra lista, o agrupados, etc
puedes algun problema

pero yo lo he usado con listas de 200000 items y la verdad sin problemas

algio igual que no has mencionado es a que linq haces referencia porque yo apunto a alinq basico, no a linq to sql o a linq to entities

aunque igual estos son muy bueno tambien para mapear las entidades a la tabla de tu db

saludos

Kleyvert dijo...

Hola Leandro, tengo una duda, tengo un gridview con 3 columnas, una de ellas tiene un LABEL, la segunda tiene un Combobox y la ultima un textbox, quisiera poder llenar solamente la primera columna donde estan los label, pasandole una lista de una clase materia: list();

tengo este codigo:

public void Inicio()
{
int i;
GridViewRow row;
Label chequeado = new Label();
for (i = 0; i < GridView1.Rows.Count; i++)
{
row = GridView1.Rows[i];
if (row.RowType == DataControlRowType.DataRow)
{
chequeado = (Label)row.FindControl("LabelMateria");
chequeado.Text ="Calculo II";
row.FindControl("LabelMateria").Controls.Add(chequeado);
GridView1.Controls.Add(row);
;
}
}
}

creo que la linea que dice GridView1.Controls.Add(row); esta mala, como le asigno propiamente a esa columna del Gridview la lista de materias, de antemano muchas gracias!

Leandro Tuttini dijo...

hola Kleyvert

un primero punto, para que haces esto:
Label chequeado = new Label();

si despues haces:
chequeado = (Label)row.FindControl("LabelMateria");

porque directamente no haces:
Label chequeado = (Label)row.FindControl("LabelMateria");

o sea defines y asignas directamente



ademas estas lineas:

row.FindControl("LabelMateria").Controls.Add(chequeado);
GridView1.Controls.Add(row);

no hacen falta para nada
si has usado el FindControl y localizado el label en la row y cambias su valor, con eso alcanza


saludos

Ivan Santiago dijo...

Hola Leandro,

Muchas gracias por la entrada.
Me sirvió mucho para un gridview dinámico que estoy haciendo.
Ahora quería añadirle al checkbox dinámico la propiedad CommandName para controlar qué checkboxes se marcan y actualizarlos en la BD, pero no encuentro la forma.

Muchas Gracias,
Saludos

Leandro Tuttini dijo...

hola Ivan

pero en el articulo ya se trata el tema de crear un template dinamico con checkbox

seria similar a la clase GridViewItemCheckTemplate


quizas en tu caso el tema del CanEdit no lo tendrias
pero la tecnica seria igual

saludos

Ivan Santiago dijo...

Hola Leandro,

Muchas gracias por tu atención.
Me ayudó mucho tu sugerencia.
Al final usé tu ejemplo para generar el gridview dinámico pero para el checkbox utilicé la idea de "http://aspadvice.com/blogs/joteke/archive/2006/10/13/Command_2D00_capable-CheckBox-with-GridView.aspx"

Saludos

Omar dijo...

Hola Leandro, una consulta, las clases me funcionaron bie, muchas gracias; pero al momento de exportar los datos a excel me muestra una tabla vacia. ¿Como puedo solucionarlo y a que se debe?

De antemano muchas gracias.

Leandro Tuttini dijo...

hola Omar

pero al momento de exportar estas recreando la estructura del gridview?

porque recuerda para exportar seguro lance el render con lo cual debes volver a asignar estas columnas que creas desde codigo

saludos

Omar dijo...

Hola Leandro, muchas gracias por la ayuda, eso es lo que me estaba faltando ahora ya se muestran los datos cuando exporto la tabla a excel.

Saludos

Veneno dijo...

Gracias!!! muy buen post!

marco's production dijo...

Excelente post, quería bajar los archivos pero no están disponibles, vuelve a subirlo por favor.

Leandro Tuttini dijo...

hola marco

yo pude descargarlo sin problemas


saludos