Criando grupos e totalizações no GridView com apenas 2 linhas de código

Site sobre desenvolvimento de sites multi-linguagem lançado: www.SitesMultilinguagem.com

O GridView, sem dúvida alguma, representa um grande avanço frente ao DataGrid do ASP.Net 1.1. Porém nossos clientes e usuários sempre necessitam de funcionalidades que vão além das fornecidas nativamente pelo GridView. Um exemplo frequente é a criação de totalizações e agrupamentos. No primeiro caso é relativamente fácil e rápido codificar o evento RowDataBound para obtenção de totais. A criação de grupos no GridView envolve um pouco mais de implementação e depuração. Há dezenas de artigos orientando tais implementações, porém eles induzem os desenvolvedores menos avisados à má prática da recodificação ao invés do reuso.

Mas é apenas quando precisamos combinar os dois recursos, grupos e sumarizações por grupo, que vemos o quanto é fácil se perder em código e depuração, caso não haja uma implementação consistente. Com o objetivo de simplificar essas tarefas desenvolvi o GridViewHelper, que permite a utilização rápida e confiável de tais recursos.

[Ver exemplo online]

[Baixar código fonte]

Uso do GridViewHelper

Veremos abaixo alguns exemplo de uso do GridViewHelper. Primeiramente exibimos o grid de referência, para o qual serão criados os grupos e totalizações. Os dados dos exemplos são do banco Northwind:

ShipRegion ShipName OrderId ProductName Quantity UnitPrice ItemTotal
RJ Hanari Carnes 10922 Alice Mutton 15 R$ 39,00 R$ 585,00
RJ Hanari Carnes 10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
RJ Hanari Carnes 10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
RJ Hanari Carnes 10925 Filo Mix 12 R$ 7,00 R$ 84,00
SP Wellington Importadora 10935 Chai 21 R$ 18,00 R$ 378,00
SP Wellington Importadora 10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
SP Wellington Importadora 10935 Tunnbröd 8 R$ 9,00 R$ 72,00
SP Gourmet Lanchonetes 10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
SP Queen Cozinha 10961 Filo Mix 6 R$ 7,00 R$ 42,00
SP Queen Cozinha 10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
SP Comércio Mineiro 10969 Spegesild 9 R$ 12,00 R$ 108,00
RJ Hanari Carnes 10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
RJ Que Delícia 10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
RJ Que Delícia 10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
RJ Que Delícia 10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60


Para criar um total para a coluna ItemTotal são necessárias apenas as prometidas 2 linhas de código:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum);
}


Primeiro criamos a classe GridViewHelper especificando no constructor o grid no qual ela irá atuar. Em seguida registramos o sumário informando o nome da coluna e a operação de sumarização desejada. O resultado pode ser visto abaixo:

ShipRegion ShipName OrderId ProductName Quantity UnitPrice ItemTotal
RJ Hanari Carnes 10922 Alice Mutton 15 R$ 39,00 R$ 585,00
RJ Hanari Carnes 10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
RJ Hanari Carnes 10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
RJ Hanari Carnes 10925 Filo Mix 12 R$ 7,00 R$ 84,00
SP Wellington Importadora 10935 Chai 21 R$ 18,00 R$ 378,00
SP Wellington Importadora 10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
SP Wellington Importadora 10935 Tunnbröd 8 R$ 9,00 R$ 72,00
SP Gourmet Lanchonetes 10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
SP Queen Cozinha 10961 Filo Mix 6 R$ 7,00 R$ 42,00
SP Queen Cozinha 10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
SP Comércio Mineiro 10969 Spegesild 9 R$ 12,00 R$ 108,00
RJ Hanari Carnes 10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
RJ Que Delícia 10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
RJ Que Delícia 10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
RJ Que Delícia 10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
  R$ 20.550,10

 

No exemplo acima foi adicionada uma nova linha ao grid para o sumário. Uma outra opção é utilizar a própria linha de rodapé (footer) para exibição do sumário ao invés de adicionar uma nova linha. Quando uma nova linha é adicionada ao grid, apenas as células necessárias para exibição dos colunas sumarizadas são criadas. Com a utilização do rodapé todas as células são criadas. No caso de sumários de grupo, a geração de todas as colunas ou apenas das colunas de sumário é um atributo do grupo.

Vamos agora criar um grupo no GridView. O código necessário para o primeiro agrupamento é exibido abaixo:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterGroup("ShipRegion"truetrue);
    
helper.ApplyGroupSort();
}


O primeiro parâmetro do método RegisterGroup informa a coluna para a qual deve ser criado o grupo. É possível também criar um grupo composto por um conjunto de colunas, bastando para isso passar um array com os nomes das colunas que compõem o grupo. O segundo parâmetro informa se o grupo é automático. Nesse caso é criada automaticamente a linha de cabeçalho do grupo. O terceiro parâmetro informa se as colunas que compõem o grupo devem ser ocultadas. O método ApplyGroupSort() define que o grid deve ser ordenado pela(s) coluna(s) do grupo, no caso acima ShipRegion. Isso é requerido para que o agrupamento funcione adequadamente, exceto caso os dados já venham ordenados do banco pela(s) coluna(s) do grupo.

No exemplo abaixo a coluna do grupo (ShipRegion) foi automaticamente ocultada do grid:

ShipName OrderId ProductName Quantity UnitPrice ItemTotal
RJ
Hanari Carnes 10922 Alice Mutton 15 R$ 39,00 R$ 585,00
Hanari Carnes 10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
Hanari Carnes 10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
Hanari Carnes 10925 Filo Mix 12 R$ 7,00 R$ 84,00
Hanari Carnes 10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
Que Delícia 10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
Que Delícia 10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
Que Delícia 10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
SP
Wellington Importadora 10935 Chai 21 R$ 18,00 R$ 378,00
Wellington Importadora 10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
Wellington Importadora 10935 Tunnbröd 8 R$ 9,00 R$ 72,00
Gourmet Lanchonetes 10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
Queen Cozinha 10961 Filo Mix 6 R$ 7,00 R$ 42,00
Queen Cozinha 10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
Comércio Mineiro 10969 Spegesild 9 R$ 12,00 R$ 108,00


Vamos tornar as coisas um pouco mais interessantes, adicionando um sumário para o grupo criado. Precisamos incluir uma linha para registrar um sumário para o grupo criado anteriormente:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterGroup("ShipRegion"truetrue);
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum, "ShipRegion");
    
helper.ApplyGroupSort();
}


Dessa vez há um parâmetro adicional para o método RegisterSummary. Ele especifica o nome do grupo para o qual o sumário deve ser gerado. O nome do grupo é gerado automáticamente a partir dos nomes das colunas que compõem o grupo. Caso o grupo seja composto por apenas uma coluna, o nome do grupo é o nome da própria coluna. Caso seja composto por mais de uma coluna, o nome do grupo é a concatenação ordenada das colunas que compõem o grupo, separadas por um sinal de "+". Ex: "ShipRegion+ShipName".

Abaixo vemos nosso grid com agrupamento e sumário para o grupo:

ShipName OrderId ProductName Quantity UnitPrice ItemTotal
RJ
Hanari Carnes 10922 Alice Mutton 15 R$ 39,00 R$ 585,00
Hanari Carnes 10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
Hanari Carnes 10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
Hanari Carnes 10925 Filo Mix 12 R$ 7,00 R$ 84,00
Hanari Carnes 10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
Que Delícia 10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
Que Delícia 10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
Que Delícia 10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
  R$ 18.465,10
SP
Wellington Importadora 10935 Chai 21 R$ 18,00 R$ 378,00
Wellington Importadora 10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
Wellington Importadora 10935 Tunnbröd 8 R$ 9,00 R$ 72,00
Gourmet Lanchonetes 10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
Queen Cozinha 10961 Filo Mix 6 R$ 7,00 R$ 42,00
Queen Cozinha 10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
Comércio Mineiro 10969 Spegesild 9 R$ 12,00 R$ 108,00
  R$ 2.085,00


Podemos incluir um sumário geral (grand total) para o grid acima simplesmente utilizando a sobrecarga adequada do método RegisterSummary, como foi mostrado no primeiro exemplo.

É possível criar mais de um grupo no grid, simulando um agrupamento hierárquico, como vemos abaixo:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterGroup("ShipRegion"truetrue);
    
helper.RegisterGroup("ShipName"truetrue);
    
helper.ApplyGroupSort();
}


Resultado:

OrderId ProductName Quantity UnitPrice ItemTotal
RJ
Hanari Carnes
10922 Alice Mutton 15 R$ 39,00 R$ 585,00
10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
10925 Filo Mix 12 R$ 7,00 R$ 84,00
10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
Que Delícia
10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
SP
Comércio Mineiro
10969 Spegesild 9 R$ 12,00 R$ 108,00
Gourmet Lanchonetes
10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
Queen Cozinha
10961 Filo Mix 6 R$ 7,00 R$ 42,00
10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
Wellington Importadora
10935 Chai 21 R$ 18,00 R$ 378,00
10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
10935 Tunnbröd 8 R$ 9,00 R$ 72,00


A visualização fica um pouco prejudicada quando há mais de um grupo. O GridViewHelper disponibiliza eventos para simplificar ajustes visuais ou funcionais no grid. Abaixo vemos a lista de eventos disponíveis:

GroupStart Ocorre quando um novo grupo se inicia, ou seja, quando novos valores são encontrados na(s) coluna(s) do grupo
GroupEnd Ocorre na última linha encontrada para o grupo
GroupHeader Ocorre quando uma linha automática de header é adicionada ao grupo. O evento não é disparado caso o grupo não seja automática pois o header não será adicionado automaticamente.
GroupSummary Ocorre quando é gerada a linha de sumário do grupo. Também só é disparado caso exista algum sumário automático que force a geração da linha, ou caso haja um grupo de supressão (será visto em seguida).
GeneralSummary Ocorre após os sumários gerais serem calculados. Se o sumário for automático o evento ocorrerá após a linha de sumário ser incluída e os valores serem definidos.
FooterDataBound Ocorre no databinding do footer.


Com mais algumas (poucas) linhas de código conseguimos melhorar o aspecto visual de nosso grid:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterGroup("ShipRegion"truetrue);
    
helper.RegisterGroup("ShipName"truetrue);
    
helper.GroupHeader += new GroupEvent(helper_GroupHeader);
    
helper.ApplyGroupSort();
}
    
private void helper_GroupHeader(string groupName, object[] values, GridViewRow row)
{
    
if ( groupName == "ShipRegion" )
    {
        row.BackColor 
Color.LightGray;
        
row.Cells[0].Text "  " + row.Cells[0].Text;
    
}
    
else if (groupName == "ShipName")
    {
        row.BackColor 
Color.FromArgb(236236236);
        
row.Cells[0].Text "     " + row.Cells[0].Text;
    
}
}


Nosso grid após os cosméticos:

OrderId ProductName Quantity UnitPrice ItemTotal
  RJ
     Hanari Carnes
10922 Alice Mutton 15 R$ 39,00 R$ 585,00
10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
10925 Filo Mix 12 R$ 7,00 R$ 84,00
10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
     Que Delícia
10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
  SP
     Comércio Mineiro
10969 Spegesild 9 R$ 12,00 R$ 108,00
     Gourmet Lanchonetes
10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
     Queen Cozinha
10961 Filo Mix 6 R$ 7,00 R$ 42,00
10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
     Wellington Importadora
10935 Chai 21 R$ 18,00 R$ 378,00
10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
10935 Tunnbröd 8 R$ 9,00 R$ 72,00

 

Mais opções de agrupamento

É interessate mostrar mais dois exemplos de agrupamento. O primeiro apresenta um grupo composto por duas colunas. O segundo define um grupo de supressão, que tem um comportamento semelhante ao da cláusula group by do SQL. Ele suprime os valores repetidos no grupo, realizando uma operação de sumarização nas demais colunas.

Abaixo vemos a listagem e a aparência do grid com grupo composto:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    string
[] cols = new string[2];
    
cols[0"ShipRegion";
    
cols[1"ShipName";
    
helper.RegisterGroup(cols, truetrue);
    
helper.ApplyGroupSort();
}

 

OrderId ProductName Quantity UnitPrice ItemTotal
RJ - Hanari Carnes
10922 Alice Mutton 15 R$ 39,00 R$ 585,00
10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
10925 Filo Mix 12 R$ 7,00 R$ 84,00
10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
RJ - Que Delícia
10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
SP - Comércio Mineiro
10969 Spegesild 9 R$ 12,00 R$ 108,00
SP - Gourmet Lanchonetes
10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
SP - Queen Cozinha
10961 Filo Mix 6 R$ 7,00 R$ 42,00
10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
SP - Wellington Importadora
10935 Chai 21 R$ 18,00 R$ 378,00
10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
10935 Tunnbröd 8 R$ 9,00 R$ 72,00


Podemos adicionar um sumário para o grupo composto. Dessa vez vamos usar a operação média e acrescentar um rótulo para indicar tal operação

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    string
[] cols = new string[2];
    
cols[0"ShipRegion";
    
cols[1"ShipName";
    
helper.RegisterGroup(cols, truetrue);
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Avg, "ShipRegion+ShipName");
    
helper.GroupSummary += new GroupEvent(helper_GroupSummary);
    
helper.ApplyGroupSort();
}

private void helper_GroupSummary(string groupName, object[] values, GridViewRow row)
{
    row.Cells[
0].HorizontalAlign HorizontalAlign.Right;
    
row.Cells[0].Text "Média";
}

 

OrderId ProductName Quantity UnitPrice ItemTotal
RJ - Hanari Carnes
10922 Alice Mutton 15 R$ 39,00 R$ 585,00
10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
10925 Filo Mix 12 R$ 7,00 R$ 84,00
10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
Média R$ 3.422,30
RJ - Que Delícia
10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
Média R$ 451,20
SP - Comércio Mineiro
10969 Spegesild 9 R$ 12,00 R$ 108,00
Média R$ 108,00
SP - Gourmet Lanchonetes
10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
Média R$ 155,00
SP - Queen Cozinha
10961 Filo Mix 6 R$ 7,00 R$ 42,00
10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
Média R$ 561,00
SP - Wellington Importadora
10935 Chai 21 R$ 18,00 R$ 378,00
10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
10935 Tunnbröd 8 R$ 9,00 R$ 72,00
Média R$ 233,33


Nosso último exemplo de sumarização vai criar um grupo de supressão. É interessante ressaltar que, se for definido um grupo de supressão, nenhum outro grupo pode ser criado. Da mesma maneira, caso haja algum grupo já criado, a tentativa de definir um grupo de supressão irá gerar um exceção.

Abaixo vemos a listagem e a aparência do grid com grupo de supressão:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.SetSuppressGroup("ShipName");
    
helper.RegisterSummary("Quantity", SummaryOperation.Sum, "ShipName");
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum, "ShipName");
    
helper.ApplyGroupSort();
}

 

ShipRegion ShipName OrderId ProductName Quantity UnitPrice ItemTotal
  Comércio Mineiro     9   R$ 108,00
  Gourmet Lanchonetes     20   R$ 155,00
  Hanari Carnes     147   R$ 17.111,50
  Que Delícia     59   R$ 1.353,60
  Queen Cozinha     66   R$ 1.122,00
  Wellington Importadora     33   R$ 700,00


Não é exibido nenhum valor para as colunas que não possuem operação de sumarização. Isso é coerente pois o GridViewHelper não sabe como proceder para sumarizar os valores das n linhas do grupo para um único valor. Da mesma forma, em SQL, se especificarmos no SELECT colunas sem função de agregação que não estejam na cláusula group by receberemos uma mensagem parecida com a exibida abaixo:

"Column 'column_name' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause."

É interessante ocultar essas colunas já que elas não fazem sentido por não possuírem função de sumarização nem pertencerem ao grupo de supressão. Para isso precisamos chamar mais um método:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    helper.SetSuppressGroup(rdBtnLstGroup.SelectedValue);
    
helper.RegisterSummary("Quantity", SummaryOperation.Sum, "ShipName");
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum, "ShipName");
    
helper.SetInvisibleColumnsWithoutGroupSummary();
    
helper.ApplyGroupSort();
}


Realmente SetInvisibleColumnsWithoutGroupSummary é um nome muito extenso, mas a intenção foi torná-lo o mais descritivo possível. O resultado pode ser visto abaixo:

ShipName Quantity ItemTotal
Comércio Mineiro 9 R$ 108,00
Gourmet Lanchonetes 20 R$ 155,00
Hanari Carnes 147 R$ 17.111,50
Que Delícia 59 R$ 1.353,60
Queen Cozinha 66 R$ 1.122,00
Wellington Importadora 33 R$ 700,00

 

Operações de sumarização

O GridViewHelper suporta "nativamente" 3 operações de sumarização: soma, média e contagem de linhas. Uma característica muito interessante é a possibilidade de se especificar operações de sumarização customizadas. Para isso devemos fornecer ao GridViewHelper duas funções: uma que será chamada para cada linha encontrada no grid (ou grupo) e outra para ser consultada e informar o resultado da operação. Abaixo vemos um exemplo de sumarização com operação customizada. A operação semi-dummy criada para esse exemplo retorna o item de menor valor da coluna especificada:

private List<int> mQuantities = new List<int>();

protected void 
Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterSummary("Quantity", SaveQuantity, GetMinQuantity);
}

private void SaveQuantity(string column, string group, object value)
{
    mQuantities.Add(Convert.ToInt32(
value));
}

private object GetMinQuantity(string column, string group)
{
    
int[] qArray = new int[mQuantities.Count];
    
mQuantities.CopyTo(qArray);
    
Array.Sort(qArray);
    return 
qArray[0];
}


No código acima pode-se ver a assinatura requerida dos métodos. Ambos recebem o nome da coluna e do grupo sendo sumarizados. Caso o sumário não seja relativo a um grupo, o parâmetro group terá valor nulo. O método que é chamado para cada linha encontrada no grid (ou grupo) recebe, além do nome da coluna e do nome do grupo, o valor da coluna na linha corrente.

O resultado é visto abaixo:

ShipRegion ShipName OrderId ProductName Quantity UnitPrice ItemTotal
RJ Hanari Carnes 10922 Alice Mutton 15 R$ 39,00 R$ 585,00
RJ Hanari Carnes 10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
RJ Hanari Carnes 10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
RJ Hanari Carnes 10925 Filo Mix 12 R$ 7,00 R$ 84,00
SP Wellington Importadora 10935 Chai 21 R$ 18,00 R$ 378,00
SP Wellington Importadora 10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
SP Wellington Importadora 10935 Tunnbröd 8 R$ 9,00 R$ 72,00
SP Gourmet Lanchonetes 10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
SP Queen Cozinha 10961 Filo Mix 6 R$ 7,00 R$ 42,00
SP Queen Cozinha 10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
SP Comércio Mineiro 10969 Spegesild 9 R$ 12,00 R$ 108,00
RJ Hanari Carnes 10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
RJ Que Delícia 10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
RJ Que Delícia 10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
RJ Que Delícia 10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
  4  

 

Limitações

Pouco acima, no exemplo de criação de mais de um grupo no mesmo grid, um detalhe pode ter chamado a atenção de alguns:

"É possível criar mais de um grupo no grid, simulando um agrupamento hierárquico"

Apesar da impressão visual ser de um agrupamento hierárquico, a implementação atual não é hierárquica. Não existe grupo e subgrupo, apenas grupos que são registrados sequencialmente. Isso só se torna um problema caso queiramos criar um sumário para um grupo interno, ou melhor, um grupo que não tenha sido o primeiro grupo a ser registrado. Abaixo vemos o que acontece nessa situação:

protected void Page_Load(object sender, EventArgs e)
{
    GridViewHelper helper 
= new GridViewHelper(this.GridView1);
    
helper.RegisterGroup("ShipRegion"truetrue);
    
helper.RegisterGroup("ShipName"truetrue);
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum, "ShipName");
    
helper.RegisterSummary("ItemTotal", SummaryOperation.Sum);
    
helper.GroupSummary += new GroupEvent(helper_Bug);
    
helper.ApplyGroupSort();
}

private void helper_Bug(string groupName, object[] values, GridViewRow row)
{
    
if (groupName == nullreturn;

    
row.BackColor Color.Bisque;
    
row.Cells[0].HorizontalAlign HorizontalAlign.Center;
    
row.Cells[0].Text "[ Summary for " + groupName + " " + values[0] + " ]";
}

 

OrderId ProductName Quantity UnitPrice ItemTotal
RJ
Hanari Carnes
10922 Alice Mutton 15 R$ 39,00 R$ 585,00
10922 Guaraná Fantástica 35 R$ 4,50 R$ 157,50
10925 Inlagd Sill 25 R$ 19,00 R$ 475,00
10925 Filo Mix 12 R$ 7,00 R$ 84,00
10981 Côte de Blaye 60 R$ 263,50 R$ 15.810,00
[ Summary for ShipName Hanari Carnes ] R$ 17.111,50
Que Delícia
10989 Grandma's Boysenberry Spread 40 R$ 25,00 R$ 1.000,00
10989 Queso Cabrales 15 R$ 21,00 R$ 315,00
10989 Jack's New England Clam Chowder 4 R$ 9,65 R$ 38,60
SP
[ Summary for ShipName Que Delícia ] R$ 1.353,60
Comércio Mineiro
10969 Spegesild 9 R$ 12,00 R$ 108,00
[ Summary for ShipName Comércio Mineiro ] R$ 108,00
Gourmet Lanchonetes
10959 Rhönbräu Klosterbier 20 R$ 7,75 R$ 155,00
[ Summary for ShipName Gourmet Lanchonetes ] R$ 155,00
Queen Cozinha
10961 Filo Mix 6 R$ 7,00 R$ 42,00
10961 Lakkalikööri 60 R$ 18,00 R$ 1.080,00
[ Summary for ShipName Queen Cozinha ] R$ 1.122,00
Wellington Importadora
10935 Chai 21 R$ 18,00 R$ 378,00
10935 Carnarvon Tigers 4 R$ 62,50 R$ 250,00
10935 Tunnbröd 8 R$ 9,00 R$ 72,00
[ Summary for ShipName Wellington Importadora ] R$ 700,00
  R$ 20.550,10


Perceba o que ocorre com o sumário em destaque. O sumário do grupo é criado depois do cabeçalho do grupo SP. Isso ocorre pois a sequência de eventos é:

Group1_Start
Group1_End
Group2_Start
Group2_End

E para um funcionamento adequado a sequência deveria ser:

Group1_Start
Group2_Start
Group2_End
Group1_End

 

Implementação

Na implementação do GridViewHelper optei por definir uma classe independente para que as funcionalidades pudessem ser utilizadas com qualquer GridView, ao invés de criar um controle customizado herdado de GridView. Essa opção mostra-se adequada por não "amarrar" o desenvolvedor a nenhuma classe base estranha ao seu projeto. Além da classe GridViewHelper há mais quatro classes auxiliares:

GridViewSummary e GridViewGroup, que representam os elementos sumário e grupo;
GridViewSummaryList e GridViewGroupList, que foram criadas para permitir o acesso aos sumários e grupos com um indexador string: helper.GeneralSummaries["ItemTotal"].Value
.

Na criação do GridViewHelper é mantida uma referência do GridView e também associa-se o evento RowDataBound do GridView para o método do GridViewHelper responsável pelo trabalho:

public GridViewHelper(GridView grd, bool useFooterForGeneralSummaries, SortDirection groupSortDirection)
{
    
this.mGrid grd;
    this
.useFooter useFooterForGeneralSummaries;
    this
.groupSortDir groupSortDirection;
    this
.mGeneralSummaries = new GridViewSummaryList();
    this
.mGroups = new GridViewGroupList();
    this
.mGrid.RowDataBound += new GridViewRowEventHandler(RowDataBoundHandler);
}


O constructor listado é o que possui todos os parâmetros, incluindo a opção de sentido de ordenação. Como vemos, o método RowDataBoundHandler é o responsável por processar o evento do grid e fazer todo o trabalho para geração de grupos e sumários.

Alguns métodos utilizados internamente no GridViewHelper foram mantidos públicos pois provêem algumas funcionalidades interessantes que podem precisar ser acessadas externamente para alguma customização visual ou funcional. Há algumas outras opções que não foram exibidas aqui, mas que podem ser facilmente verificadas e compreendidas com o intellisense do Visual Studio.

 

Aspectos relevantes

A performance das operações deve ser prejudicada em função do "uso abusivo" de boxing e unboxing de value types.Uma possibilidade para tentar minimizar esse problema é implementar as operações nativas do GridViewHelper com Generics, mas isso não é trivial como seria desejável, como podemos ver em Using Generics for Calculations. Outra possibilidade pode ser vista em http://www.codeproject.com/csharp/genericoperators.asp. Na prática isso não será perceptível, exceto caso haja milhões de linhas no grid ou milhões de usuários agrupando e sumarizando grids de forma concorrente.

No exemplo online, o EnableViewState do grid está definido como False. Isso pois, caso contrário, no PostBack o grid iria recuperar a informação necessária para renderizar seu conteúdo do ViewState, não disparando o evento RowDataBound que é essencial ao funcionamento do GridViewHelper. Considero desabilitar o EnableViewState uma prática recomendável, exceto caso você tenha certeza de que ele é necessário, ou caso o custo da consulta ao banco de dados seja alto. No ASP.Net 1.1 desabilitar o EnableViewState podia afetar o comportamento dos controles em função de não haver a separação entre ViewState (estado visual) e ControlState (estado do controle). Essa divisão foi incluída no ASP.Net 2.0.

Todo o código e inclusive comentários foram em inglês por dois motivos:

1 - Permitir a divulgação em comunidades internacionais, tais com o Code Project
2 - Ninguém merece uma classe de nome AjudanteDoGridView !!

O exemplo e os fontes podem ser baixados aqui. Sintam-se à vontade para comentar, sugerir e, principalmente, ampliar a funcionalidade do GridViewHelper. Enviem as adaptações para que eu possa atualizar o artigo e compartilhar com a comunidade.

Site sobre desenvolvimento de sites multi-linguagem lançado: www.SitesMultilinguagem.com