DataGrid com somatório no rodapé – Parte I
Um requisito comum em toda aplicação ASP.Net é a exibição da soma / média dos valores de uma coluna no Footer do DataGrid. Neste artigo compararemos os métodos normais e mais tarde criaremos nossa própria coluna "SumColumn" de DataGrid personalizado, derivado de BoundColumn para evitar a repetição do mesmo código.
Vamos considerar o exemplo de um DataGrid que mostra a lista dos empregados. As colunas são Name, Salary e Bonus. A soluções corriqueiras são:
Consultar o Banco de dados para obter a soma
Uma forma é computar a soma dos dados com uma consulta SQL. Por exemplo, para computar a soma de todos os salários na Tabela Employee, a seguinte declaração SQL pode ser utilizada:
SELECT SUM(Salary)
FROM Employees
Depois que disparamos a consulta utilizando o ADO, armazenamos o resultado em uma variável local internalSum, por exemplo. A divisão do internalSum pela quantidade de Itens no DataGrid, resultará na média. A seguir, temos que varrer a Items Collection do DataGrid para encontrar o Footer e configurar o valor.
For Each dgItem In Me.dgEmployee.Items
If dgItem.ItemType = ListItemType.Footer Then
values = "Sum : " & internalSum & "
"
values += "Average : " & (internalSum / Me.dgEmployee.Items.Count)
dgItem.Cells(0).Text = values
End If
Next
Desvantagens:
· Duas viagens de ida-e-volta ao servidor do banco de dados: uma para os registros e uma para a soma.
· Caso quisermos computar também a soma dos Bonus, precisaremos então, mais uma consulta SQL, isto é, a quantidade de consultas será n+1, onde n é o número de colunas que desejamos somar.
· Para mostrar a soma no Footer, temos que iterar através da coleção Items do DataGrid, para achar o Footer e atribuir o valor.
Utilizar o Método DataTable Compute():
Uma outra maneira é utilizar o método DataTable Compute(), isto é, para obter a mesma saída que na primeira abordagem, temos que fazer o seguinte:
internalSum = dtEmployee.Compute("SUM(Salary)", String.Empty).ToString
internalAvg = dtEmployee.Compute("AVG(Salary)", String.Empty).ToString
Agora aplicamos a técnica antes descrita: iterar através da coleção Items do DataGrid, para achar o Footer e atribuir o valor.
Desvantagens:
· os resultados da consulta SQL que está sendo vinculada ao DataGrid, deve ser recuperada em um DataTable; nenhuma coleção, Array ou ArrayList personalizada irá funcionar.
· para mostrar a soma no Footer, temos que iterar através da coleção Items do DataGrid.
Utilizar o Evento ItemDataBound do DataGrid
A idéia é utilizar o evento o ItemDataBound do DataGrid, recuperando os valores correspondentes dos itens de todas as linhas do DataGrid e somando-as. Devemos também guardar a contagem de Items para calcular a média. Então poderíamos exibir os valores no Footer.
Private Sub dgEmployee_ItemDataBound(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.DataGridItemEventArgs)
Handles dgEmployee.ItemDataBound
Select Case e.Item.ItemType
Case ListItemType.Item, ListItemType.AlternatingItem
internalSum += CType(e.Item.Cells(1).Text, Double)
internalCount += 1
Case ListItemType.Footer
e.Item.Cells(0).Text = "Sum : " & Me.FormatDataValue(internalSum) & "
"
e.Item.Cells(0).Text += "Count : " & internalCount & "
"
e.Item.Cells(0).Text += "Average : " & _
Me.FormatDataValue(internalSum / internalCount)
End Select
End Sub
Vantagens:
· o DataSource do DataGrid pode ser de qualquer tipo. Não está restrito a um DataTable como na segunda solução.
· somente uma consulta em comparação às n+1 consultas da primeira solução.
· nenhuma iteração extra da Item Collection do DataGrid para encontrar o Footer como na primeira e segunda soluções.
Problema
Neste momento imaginamos que se a última solução não tiver problemas, o que iremos fazer. Se não está quebrado, porque consertá-lo? Realmente estou cansado de repetir o mesmo velho código no code behind das páginas que contêm o DataGrid. É você não está cansado também? Queremos fazer isto de uma maneira mais fácil; uma maneira que seja também elegante. Que tal se não escrevermos uma única linha de código e que tudo seja feito automaticamente. Estou certo que irá apreciar isto. Pode ser que já tenha escutado demais esta citação:
A preguiça é a mãe da invenção.
SumColumn ao Resgate - Introduzindo a Solução
Este é o momento de tirar o gato do saco. A solução para este problema consiste em construir a funcionalidade desejada correta em um DataGridColumn personalizado que possamos então utilizar em toda página! Confuso? Permitam-me explicar. Iremos criar nossa própria coluna DataGrid personalizada, à qual nos referiremos como "SumColumn", herdada da BoundColumn do DataGrid. Devido à sua classe base, a nova classe SumColumn terá todas as funcionalidades internas já presentes em BoundColumn. Necessitamos apenas implementar nossa funcionalidade de soma. Herdar controles poderosos como a classe BoundColumn e adicionar-lhes novas funcionalidades é uma das características OO do .NET Framework.
Antes de criarmos a nossa classe de coluna personalizada, vamos observar um exemplo que utiliza simplesmente um controle normal BoundColumn para exibir os dados. Para isto, configuramos um DataGrid simples que exibe o Name, o Salary e o Bonus da Tabela Employee.
E no code behind, no evento de carga da página:
dgEmployee.DataSource = GetEmployees()
dgEmployee.DataBind()
Onde GetEmployees() retornará um DataTable que contem os registros do empregado. A saída normal será (dependendo dos dados):
Figura 2.
Primeiro Passo : criando o Rascunho Inicial (Initial Draft)
Se tiver o Visual Studio .NET, crie um novo projeto VB.Net do tipo Class Library chamado CustomWebControls. Teremos que adicionar a referência do assembly System.Web.dll no projeto, caso contrário criar um arquivo SumColumn.vb. Faremos a implementação da classe passo a passo.
Imports System
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.ComponentModel
Public Class SumColumn
Inherits BoundColumn
End Class
A classe BoundColumn tem dois métodos que são normalmente sobreescritos para fornecer alguma funcionalidade personalizada. Um é o FormatDataValue() e outro é o InitializeCell(). O método FormatDataValue é utilizado normalmente junto com a propriedade DataFormatString para formatar a informação numérica e de data. Conseqüentemente iremos utilizar o segundo isto é o InitializeCell. Este método é muito parecido com o método ItemCreated do DataGrid. A assinatura do método é:
Public Overrides Sub InitializeCell(ByVal cell As System.Web.UI.WebControls.TableCell, _
ByVal columnIndex As Integer, ByVal itemType As System.Web.UI.WebControls.ListItemType)
Ainda não temos o valor da célula porque a mesma foi recém criada. Devemos de algum modo ligar-nos à célula vinculada. Isto pode ser feito anexando um tratador de eventos à célula DataBound:
Public Overrides Sub InitializeCell(ByVal cell As System.Web.UI.WebControls.TableCell, _
ByVal columnIndex As Integer, ByVal itemType As System.Web.UI.WebControls.ListItemType)
MyBase.InitializeCell(cell, columnIndex, itemType)
Select Case itemType
Case ListItemType.AlternatingItem, ListItemType.Item, ListItemType.Footer
AddHandler cell.DataBinding, AddressOf CellItemDataBound
End Select
End Sub
Chamamos o método InitializeCell da classe base de modo que podemos fazer o trabalho rotineiro. Filtramos também o ItemType utilizando o select case, porque queremos somente lidar com Item, AlternalteItem e Footer. Nosso método, CellItemDataBound, foi anexado ao evento DataBinding da célula. Neste método faremos nossa mágica. Lembremos a terceira solução previamente descrita.
Private Sub CellItemDataBound(ByVal sender As Object, ByVal e As EventArgs)
Dim cell As TableCell = CType(sender, TableCell)
Dim DGI As DataGridItem = CType(cell.NamingContainer, DataGridItem)
Dim dValue As Decimal
Dim dataItem As Object = DGI.DataItem
Select Case DGI.ItemType
Case ListItemType.AlternatingItem, ListItemType.Item
dValue = DGI.DataItem(DataField)
internalSum += dValue
internalCount += 1
cell.Text = Me.FormatDataValue(dValue)
Case ListItemType.Footer
cell.Text = "Sum : " & Me.FormatDataValue(internalSum) & "
"
cell.Text += "Count : " & internalCount & "
"
cell.Text += "Average : " & Me.FormatDataValue(internalSum / internalCount)
End Select
End Sub
O código é bastante direto. Recuperamos apenas o DataItem do objeto sender, e verificamos o ItemType. Se for Item ou AlternateItem, configuramos a célula do texto; adicionamos ao internalSum e incrementamos o internalCount. Se for Footer, apenas concatenamos os valores e configuramos o texto da célula. Notar, por favor, que chamamos o método FormatDataValue da classe base para formatar a saída. Nenhuma complicação por enquanto.
Compilamos o projeto da biblioteca da classe. Adicionamos a referência da sua saída ao projeto Web. Registramos o Tag no topo da página .aspx.
Use the new SumColumn instead of BoundColumn for Salary in the aspx.
Rodando nosso projeto Web, a saída será mais ou menos a seguinte:
Figura 3.