Criando colunas dinamicamente no ASP.NET DataGrid

por Marcos Santos e Alexandre Santos

Para ler todas as matérias da MSDN Magazine, assine a revista no endereço www.neoficio.com.br/msdn

Este artigo discute

Este artigo usa as seguintes tecnologias:

  • Controle ASP.NET DataGrid

  • Acesso a banco de dados

C#, SQL

Download:
fontesDataGridDinamico.zip

Chapéu
DataGrid

 

Desde o lançamento do ASP.NET o DataGrid é um dos controles mais empregados, principalmente pela sua fácil utilização e quantidade de recursos que auxiliam o desenvolvedor a criar páginas robustas e atrativas. Através do Visual Studio .NET fica ainda mais inspirador o uso de DataGrid, dada à velocidade de desenvolvimento, já que em poucos cliques, têm-se acesso aos Wizards que configuram as colunas (Property Builder) e o formato visual (Auto Format), restando apenas atribuir o seu DataSource, podendo ser um DataTable, Coleções entre outros.

O objetivo deste artigo é mostrar a formatação e criação de colunas no DataGrid de forma dinâmica. É criado um projeto ASP.NET que efetua uma consulta de Produtos com suas respectivas Categorias no banco de dados Northwind do SQL Server. É apresentado ao usuário todos os Produtos agrupados por Categorias, com subtotais. Para isto, é utilizado como DataSource um novo DataTable criado a partir dos dados retornados na consulta.

Talvez você esteja se perguntando: "Por que criar colunas dinamicamente, se é possível criá-las sem uma linha de código e de forma quase que instantânea utilizando o VS.NET?". Certo? A resposta para tal pergunta pode ser melhor analisada através do seguinte questionamento: "Como fazer para que um mesmo DataGrid seja parametrizado de forma tal, que em certas situações apresente X colunas e em outras situações apresente Y colunas?". Resposta: Criar as colunas dinamicamente, podendo também utilizar artifícios de orientação a objetos (OO) - Herança e Polimorfismo.

Para aumentar a compreensão da resposta apresentada, imagine que você queira centralizar seus relatórios em apenas uma página .aspx com apenas um DataGrid e que cada relatório tenha consulta SQL diferente um do outro. Uma forma viável, através de OO é criar uma classe pai RelatorioPadrao com dois métodos: retornaColunas() e retornaDados() e para cada relatório a ser gerado, criar classes filhas (exemplo RelatorioProduto) que irão sobrescrever estes métodos. Desta forma, na página .aspx será instanciada a classe desejada e invocados os métodos que retornam as colunas do DataGrid e seu DataSource.

Neste artigo, para simplificar, não será modelado através de classes e subclasses. A criação das colunas, formatação do DataGrid e agrupamento com subtotais serão feitos diretamente na classe da Interface (.aspx).

Projeto

Crie um projeto ASP.NET Web Application chamado DataGridAgrupadoColunas usando como linguagem o Visual C#, contendo um formulário chamado GridAgrupadoColunas.aspx. Insira 3 controles, 1 DataGrid (id = gridProdutoCategorias), 1 Button (id = btnSelecionados ) e 1 ListBox (id = lstSelecionados) - veja a Figura 1. Apenas observe que são utilizadas tabelas HTML para possibilitar uma melhor organização visual.

Cc518032.DataGridDinamico01(pt-br,MSDN.10).gif
Figura 1 - Layout de Interface sugerido.

Para agilizar o tempo de desenvolvimento, selecione o DataGrid e formate a cor e fonte do cabeçalho, através de suas propriedades HeaderStyle. Em tempo de design não é necessário fazer mais nada.

No projeto sugerido é realizada uma consulta ao Banco de Dados Northwind do SQL Server, retornando a tabela Products com sua respectiva Categories, como na Figura 2. Porém, os produtos serão visualizados agrupados por Categorias no DataGrid, incluindo subtotais e, no final, um Total Geral. Para isto, via programação, a partir da consulta original é criado um novo DataTable no modelo final.

Cc518032.DataGridDinamico02(pt-br,MSDN.10).png
Figura 2 - Query realizada no Northwind.

No DataGrid são criadas 5 colunas dinamicamente:

Nome

Tipo

Obs.

Selecionar

TemplateColumn

TemplateColumnCheckBox

Descrição

BoundColumn

 

Valor Unitário

BoundColumn

 

Quantidade Estoque

BoundColumn

 

Valor Estoque

BoundColumn

 

BoundColumns são colunas que se relacionam com os campos do DataSource do DataGrid em questão. TemplateColumns são colunas complexas, que permitem a inserção de diversos outros controles, por exemplo um CheckBox. Neste caso, a coluna "Selecionar" possuirá um CheckBox em todas as linhas, menos nos grupos e subtotais, permitindo que o internauta informe quais produtos deseja selecionar. Quando o usuário clicar no botão "Mostra Selecionados", os produtos selecionados serão adicionados ao ListBox. Esta funcionalidade é para explicitar que mesmo sendo adicionados controles em tempo de desenvolvimento (runtime), seus valores permanecem entre PostBacks.

Código

Primeira parte a ser desenvolvida é a criação das colunas do DataGrid, para isto no método OnInit deve ser invocada a rotina "criaColunasGrid()". A necessidade da chamada ser no OnInit ocorre devido que neste evento os controles ainda não são populados com seus dados pelo DataView, último estado entre PostBacks.

override protected void OnInit(EventArgs e)
{			
  InitializeComponent();
  base.OnInit(e);
  //dever ser invocado aqui p/ manter a renderização
  //das colunas, através de PostBacks
  this.criaColunasGrid();
}

Digite a rotina criaColunasGrid que cria as colunas dinamicamente no DataGrid.

//Cria as colunas dinamicamente no DataGrid
private void criaColunasGrid()
{		
  //Colunas não geradas automaticamente
  gridProdutoCategorias.AutoGenerateColumns = false;			
  //coluna Template que nos permite adicionar o checkbox
  TemplateColumn colSelecionar = new TemplateColumn();
  colSelecionar.HeaderText = "Selecionar";
  //informa o que o ItemTemplate é do tipo TemplateColumnCheckBox
  colSelecionar.ItemTemplate = new TemplateColumnCheckBox();
			
  //coluna descrição
  BoundColumn colDescricao = new BoundColumn();
  colDescricao.DataField = "Descricao";
  colDescricao.HeaderText = "Descrição";
  colDescricao.ItemStyle.HorizontalAlign = System.Web.UI.WebControls.HorizontalAlign.Left;

  //coluna valor unitário
  BoundColumn colValorUnitario = new BoundColumn();
  colValorUnitario.DataField = "valorUnitario";
  colValorUnitario.HeaderText = "Valor Unitário";
  colValorUnitario.DataFormatString  = "{0:F2}"; 
  colValorUnitario.ItemStyle.HorizontalAlign = System.Web.UI.WebControls.HorizontalAlign.Right;

  //coluna Unidades no Estoque
  BoundColumn colUnidadesEstoque = new BoundColumn();
  colUnidadesEstoque.DataField = "unidadesEstoque";			
  colUnidadesEstoque.HeaderText = "Quantidade Estoque";
  colUnidadesEstoque.ItemStyle.HorizontalAlign = System.Web.UI.WebControls.HorizontalAlign.Right;

  //coluna (valorUnitario * unidadesEstoque)
  BoundColumn colValorEstoque = new BoundColumn();
  colValorEstoque.DataField = "valorEstoque";
  colValorEstoque.HeaderText = "Valor Estoque";
  colValorEstoque.DataFormatString = "{0:F2}";//duas casas decimais
  colValorEstoque.ItemStyle.HorizontalAlign = System.Web.UI.WebControls.HorizontalAlign.Right;

  //adiciona colunas criadas no grid
  gridProdutoCategorias.Columns.Add(colSelecionar);			
  gridProdutoCategorias.Columns.Add(colDescricao);
  gridProdutoCategorias.Columns.Add(colValorUnitario);
   gridProdutoCategorias.Columns.Add(colUnidadesEstoque);
  gridProdutoCategorias.Columns.Add(colValorEstoque);
}

Perceba que para as colunas BoundColumn é necessário informar o campo referente ao seu DataSource, através da propriedade DataField. Possibilita-se também a formatação através de outras propriedades, como HeaderText (nome no cabeçalho), DataFormatString, HorizontalAlign, entre outras.

Para as colunas TemplateColumn, a propriedade ItemTemplate permite indicar quais controles serão adicionados e visualizados. Isto é possível indicando um objeto de uma classe que implementa a interface ITemplate. Aqui será um CheckBox. Para isto, crie uma nova classe TemplateColumnCheckBox, como segue:

//Define um ItemTemplate c/ CheckBox
public class TemplateColumnCheckBox : System.Web.UI.ITemplate
{
  public void InstantiateIn(System.Web.UI.Control container)
  {
    //adiciona um checkbox na coluna template
    System.Web.UI.WebControls.CheckBox chRetorno = new System.Web.UI.WebControls.CheckBox();
    chRetorno.ID = "chSelecionado";
			
    container.Controls.Add(chRetorno);
  }
}

Até aqui as colunas do DataGrid já estão sendo criadas dinamicamente. O próximo passo é realizar a consulta ao banco de dados que será manipulada para gerar o DataTable no formato final, o qual será o DataSource do DataGrid. Para isto, digite o método "retornaCategoriasProdutos()":

//Retorna DataTable que será navegado, para gerar o DataTable agrupado (no formato final).
public DataTable retornaCategoriasProdutos()  
{
  //faz a conexão com o banco de dados e realiza a consulta com os dados 
  //primários que serão manipulados p/ servir de fonte de dados ao grid
  string sConexao = "Initial Catalog=Northwind; Data source=localhost; user id=sa;";

  System.Text.StringBuilder sSql = new System.Text.StringBuilder();
  sSql.Append(" select c.CategoryName as Categoria, p.ProductName as Produto, p.ProductId as IdProduto, ");
  sSql.Append(" UnitPrice as valorUnitario, ");  
  sSql.Append(" UnitsInStock as unidadesEstoque ");
  sSql.Append(" from categories c join products p ");
  sSql.Append(" on c.categoryid = p.categoryid ");
  sSql.Append(" order by c.CategoryName, p.ProductName");			

  SqlConnection conexao = new SqlConnection(sConexao);
  conexao.Open();

  SqlDataAdapter daConsulta = new SqlDataAdapter(sSql.ToString(),conexao);
  DataSet dsConsulta = new DataSet();
  daConsulta.Fill(dsConsulta);
			
  return dsConsulta.Tables[0];
}

Cabe ressaltar que a conexão deve ser configurada de acordo com a instalação do SQL Server no servidor, podendo haver Password, segurança integrada, etc. Atenção: Não esqueça de adicionar o namespace System.Data.SqlClient na lista de using.

Antes de criar o DataTable com os dados agrupados, é necessário declarar alguns atributos na lista de private, que serão utilizados para controle de quebra de categoria, totalizadores e o próprio DataTable (dtDadosAgrupados).

//variável que controla a troca de categoria		
private string deCategoriaAnterior = "";
		
//variáveis setadas com os valores da linha do BD que está sendo manipulada
private string deCategoriaAtual = ""; 		 
private string deProdutoAtual = ""; 		 
private double valorAtual = 0; 		 
private int unidadesAtual = 0; 		 
			
//variáveis Totalizadoras da Categoria Atual
private double valorCategoriaAtual = 0;
private double valorEstoqueCategoriaAtual = 0;
private int quantidadeCategoriaAtual = 0;
			
//variáveis Totalizadoras do "Total Geral"
private double valorTotal = 0;
private double valorEstoqueTotal = 0;
private int quantidadeTotal = 0;

//variável p/ controlar a linha atual
private int indiceLinha = 1; //inicializado com 1
//DataTable que conterá os dados manipulados e servirá de fonte de dados p/ o grid 
private DataTable dtDadosAgrupados;
//enum p/ associar um tipo p/ cada linha. Utilizado p/ pintar cada tipo de linha de uma cor.
private enum TipoLinha {DescricaoCategoria, DescricaoProduto, SubTotal, Total};

Digite o código que instância e cria as colunas no DataTable que conterá os dados agrupados, pelo método "criaDataTableDadosAgrupados()". Atente para as colunas "Descrição" e "tipoLinha". Para permitir que seja feito o agrupamento, a coluna "Descricao" possuirá as descrições das Categorias, Produtos, subtotais e Total Geral e por sua vez a coluna "tipoLinha", informará ao DataTable o tipo da linha conforme o enum TipoLinha, declarado na lista de private.

//Cria DataTable que conterá os dados manipulados em forma de subgrupos.
private void criaDataTableDadosAgrupados() 
{
  this.dtDadosAgrupados = new DataTable();
  this.dtDadosAgrupados.Columns.Add("Descricao",
       System.Type.GetType("System.String"));
  this.dtDadosAgrupados.Columns.Add("valorUnitario",
       System.Type.GetType("System.Double"));
   this.dtDadosAgrupados.Columns.Add("unidadesEstoque",
       System.Type.GetType("System.Int32"));
  this.dtDadosAgrupados.Columns.Add("valorEstoque",
       System.Type.GetType("System.Double"));
  this.dtDadosAgrupados.Columns.Add("tipoLinha",
       System.Type.GetType("System.Int32"));
}

Através do método "retornaCategoriasProdutos()" criado anteriormente, obtêm-se o DataTable com o retorno do banco de dados, possibilitando o preenchimento do DataTable dtDadosAgrupados já no formato a ser mostrado ao usuário, isto é, com grupos de Categorias, subtotais e Total Geral. Digite o método "populaDataTableDadosAgrupados", que então varre o retorno do banco de dados e faz a validação de troca de categorias. Caso o registro atual da iteração seja uma nova categoria, são adicionadas linhas de subtotal (da categoria anterior) e a linha de Descrição da nova categoria. No final, é adicionada a linha de Total Geral. Observe que em cada iteração são atualizados os Totalizadores dos subtotais e Total Geral, sendo que na troca de categoria, os subtotais são zerados.

//Navega no DataTable retornado no SQL e popula o DataTable Agrupado (no formato final)
private void populaDataTableDadosAgrupados()
{
  foreach (DataRow drRetornoBD in this.retornaCategoriasProdutos().Rows)
  {
    //salva os dados da linha atual p/ serem 
    //manipulados nos outros métodos
    deCategoriaAtual = drRetornoBD["Categoria"].ToString();
    deProdutoAtual = drRetornoBD["Produto"].ToString();
    valorAtual = Convert.ToDouble(drRetornoBD["valorUnitario"]);
    unidadesAtual = Convert.ToInt32(drRetornoBD["unidadesEstoque"]);

    //Valida troca de categoria...
    if (this.deCategoriaAnterior != this.deCategoriaAtual) 
                {					
    //Insere subTotal da Categoria anterior, porém somente se
    //índiceLinha != 1, pois neste caso seria a primeira iteração no for
    if (this.indiceLinha != 1)
        dtDadosAgrupados.Rows.Add(retornaLinhaSubTotal());

    //adiciona linha c/ a descrição da nova Categoria.
     dtDadosAgrupados.Rows.Add(retornaLinhaCategoria());

    //"true" p/ zerar os somatórios desta categoria, pois mudou a categoria
    atualizaTotalizadores(true);

    //atualiza deCategoriaAnterior com a Atual
    deCategoriaAnterior = this.deCategoriaAtual;
  }			
	
  //adiciona linha de Produto no DataTable agrupado.
  dtDadosAgrupados.Rows.Add(retornaLinhaProduto());

  //Incrementa Totalizadores e "false" p/ não
  //zerar os somatórios desta categoria.
  atualizaTotalizadores(false);	

  indiceLinha++;
  }//foreach

  //adiciona subtotal da última categoria
        dtDadosAgrupados.Rows.Add(retornaLinhaSubTotal());
  //adiciona Total Geral
  dtDadosAgrupados.Rows.Add(retornaLinhaTotal());
}

A seguir, devem ser adicionados os métodos que retornam as linhas descritivas das Categorias, Produtos, subtotais e Total Geral ao DataTable dtDadosAgrupados:

//Retorna DataRow com a Descrição da Categoria Atual
private DataRow retornaLinhaCategoria()
{
  //linha de subtitulo da categoria.
  DataRow drCategoria = dtDadosAgrupados.NewRow();
  drCategoria = dtDadosAgrupados.NewRow();
  drCategoria["Descricao"] = " >>Categoria: "+ 
                             deCategoriaAtual;
  drCategoria["tipoLinha"] = (int)TipoLinha.DescricaoCategoria;
  //fim linha de subtitulo da categoria
  return drCategoria;
}

//Retorna DataRow com Dados do Produto Atual
private DataRow retornaLinhaProduto()
{
  //retorna a linha com os dados do produto
  DataRow drProduto = dtDadosAgrupados.NewRow();
  drProduto["Descricao"] = deProdutoAtual;
  drProduto["valorUnitario"] = valorAtual;
  drProduto["unidadesEstoque"] = unidadesAtual;
  drProduto["valorEstoque"] = 
           valorAtual * unidadesAtual;
  drProduto["tipoLinha"] = (int)TipoLinha.DescricaoProduto;

  return drProduto;
}


//Retorna DataRow com SubTotal da Categoria
private DataRow retornaLinhaSubTotal()
{
  //retorna a linha de subtotal após cada categoria.
  DataRow drSubTotal = dtDadosAgrupados.NewRow();
  drSubTotal = dtDadosAgrupados.NewRow();
  drSubTotal["Descricao"] = "Sub Total";
  drSubTotal["valorUnitario"] = valorCategoriaAtual;
  drSubTotal["unidadesEstoque"] = quantidadeCategoriaAtual;
  drSubTotal["valorEstoque"] = valorEstoqueCategoriaAtual;
  drSubTotal["tipoLinha"] = (int)TipoLinha.SubTotal;
  return drSubTotal;
}


//Retorna DataRow com Total Geral
private DataRow retornaLinhaTotal()
{
  //retorna a linha final de Total Geral.
  DataRow drTotal = dtDadosAgrupados.NewRow();

  drTotal["Descricao"] = "Total Geral";
  drTotal["valorUnitario"] = valorTotal;
  drTotal["unidadesEstoque"] = quantidadeTotal;
  drTotal["valorEstoque"] = valorEstoqueTotal;
  drTotal["tipoLinha"] = (int)TipoLinha.Total;
  return drTotal;
}

Para finalizar a implementação do preenchimento do DataTable agrupado é necessário adicionar o método "atualizaTotalizadores", que incrementa ou zera os totalizadores da categoria da iteração atual e que também incrementa os totalizadores do Total Geral.

//Atualiza Totalizadores
private void atualizaTotalizadores(bool _zeraTotCampanhaAtual)
{
  if (_zeraTotCampanhaAtual)
  {
     //zera totalizadores da Categoria Atual
     this.valorCategoriaAtual = 0;
     this.quantidadeCategoriaAtual = 0;
     this.valorEstoqueCategoriaAtual = 0;
  }
  else
  {
     //incrementa totalizadores da Categoria Atual
     valorCategoriaAtual += valorAtual;  
     quantidadeCategoriaAtual += unidadesAtual;
     valorEstoqueCategoriaAtual += (valorAtual * unidadesAtual);

     //incrementa totalizadores do Total Geral
     valorTotal += valorAtual;
     quantidadeTotal += unidadesAtual;
     valorEstoqueTotal += (valorAtual * unidadesAtual);
  }
}

Preenchido o DataTable agrupado, altere o evento Page_Load para que efetivamente faça a chamada aos métodos que criam e populam o DataTable, além de fornecê-lo ao DataGrid como seu DataSource. Todos os códigos contidos no (!Page.IsPostBack) serão executados apenas a primeira vez em que página for carregada.

//Executado toda vez que carrega a página.
private void Page_Load(object sender, System.EventArgs e)
{
  if (!Page.IsPostBack)
  {
    //cria DataTable com os DadosAgrupados
    criaDataTableDadosAgrupados();
    //popula DataTable Agrupado, através dos dados provenientes
    //da consulta SQL no Banco de Dados
    populaDataTableDadosAgrupados();

    //seta a fonte de dados e mostra os dados
    gridProdutoCategorias.DataSource = dtDadosAgrupados;
    gridProdutoCategorias.DataBind();
  }
  //remove CheckBox entre PostBacks
  removeCheckBox();
}

Para cada item que for populado ao DataGrid será criado um CheckBox, devido à coluna TemplateColumn criada. Porém, para as linhas de Categorias, subtotais e TotalGeral, não é desejado mostrá-lo ao usuário, justificando o uso da rotina "removeCheckBox()" referenciada no Page_Load().

//Remove CheckBox das linhas Totalizadoras.
public void removeCheckBox()
{
  //remove CheckBox nas linhas de categoria, 
  //subtotais e Total Geral
  foreach (DataGridItem item in gridProdutoCategorias.Items)
  {
    if ((item.Cells[1].Text == "Sub Total") ||
       (item.Cells[1].Text == "Total Geral") ||
        (item.Cells[1].Text.ToString().IndexOf(">>Categoria:") != -1) )
    {
      item.Cells[0].Controls.RemoveAt(0);
    }
  }
}

No momento que é realizado o DataBind() do DataGrid, automaticamente é disparado o evento ItemDataBound, que ocorre como última oportunidade para se acessar o dado de um item do grid, antes que seja exposto ao usuário. Desta forma, o utilizaremos para colorir as linhas totalizadoras. Para criar este evento, selecione o gridProdutoCategorias e na janela de propriedades dê um duplo clique no evento ItemDataBound.

//Evento que colore as linhas de acordo com o seu Tipo.
private void gridProdutoCategorias_ItemDataBound(object sender, System.Web.UI.WebControls.DataGridItemEventArgs e)
{
  if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem)
  {
    //drvItem faz referencia a linha do dataSource do grid do item atual
    DataRowView drvItem = (DataRowView)e.Item.DataItem;
    TipoLinha tipo = (TipoLinha)(drvItem["tipoLinha"]);
    //switch p/ colorir a linha de acordo com o tipo de linha
    switch (tipo)
    {
       case TipoLinha.DescricaoCategoria :
       {
            e.Item.BackColor = Color.Silver;
            break;
       }
       case TipoLinha.SubTotal :
       {
            e.Item.BackColor = Color.Gainsboro;
            break;
       }
       case TipoLinha.Total :
       {
            e.Item.BackColor = Color.Gray;
            break;
       }
    }
  } //fim if
}

Note que verificado o item atual do DataGrid encontra-se através do DataRowView, o valor da coluna "tipoLinha" do dtDadosAgrupados. Através deste valor e do enum TipoLinha, colore-se o item do DataGrid.

Com o objetivo de mostrar que é possível capturar dados do DataGrid entre PostBacks, mesmo que suas colunas tenham sido criadas dinamicamente, dê um duplo clique no botão btnSelecionados e digite o seguinte código, que listará no ListBox todos os itens que o CheckBox estiver selecionado:

//Mostra Todos os itens selecionados dos CheckBoxs da coluna Template
private void btnSelecionados_Click(object sender, System.EventArgs e)
{
  lstSelecionados.Visible = true;
  lstSelecionados.Items.Clear();
  lstSelecionados.Items.Add("Items Selecionados:");
  foreach (DataGridItem item in gridProdutoCategorias.Items)
  {
    CheckBox chSelecionado = (CheckBox)item.FindControl("chSelecionado");
    if ((chSelecionado != null) && (chSelecionado.Checked))
      lstSelecionados.Items.Add(item.Cells[1].Text);
  }
}

Salve o projeto e execute (CTRL + F5) para ver o resultado do gridProdutoCategorias montado dinamicamente (Figura 3). Experimente selecionar alguns CheckBox e clique no botão "Mostrar Selecionados" e veja como os itens permanecem selecionados e também listados no ListBox.

Cc518032.DataGridDinamico03(pt-br,MSDN.10).png
Figura 3 - Versão final da página sendo executada.

Conclusão

A combinação entre DataGrid e DataTable permite ao desenvolvedor manipular dados e mostrá-los aos usuários de uma forma muito dinâmica e eficiente. A criação de colunas dinamicamente viabilizam a solução de problemas quanto a formatação do DataGrid, aumentando em muito sua aplicabilidade, como por exemplo, um sistema de Geração de Relatórios.

Marcos Santos (marcososantos@yahoo.com) e Alexandre Santos (alexandrecpd@hotmail.com) são graduados em Ciências da Computação na UFSC, trabalham como Analistas de Sistemas na Softway Contact Center e com a plataforma .NET desde a versão Beta.

Mostrar: