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.

 

 

      

      

        

        

      

             < asp:BoundColumn DataField ="Name" HeaderText ="Name">

             < asp:BoundColumn DataField ="Bonus" HeaderText ="Bonus">

             < asp:BoundColumn DataField ="Salary" HeaderText ="Salary">

      

 

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.

 < asp:BoundColumn DataField ="Name" HeaderText ="Name">

 < asp:BoundColumn DataField ="Bonus" HeaderText ="Bonus">

 

 

Rodando nosso projeto Web, a saída será mais ou menos a seguinte:

 

 

Figura 3.