
Итоговая строка GridView: суммы в подвале через DataTable.Compute
Таблица продаж без строки «Итого» заставляет пользователя складывать числа в уме. Метод DataTable.Compute считает сумму одной строкой, а подвал GridView показывает её под нужной колонкой.
Где считать итог
Есть три очевидных способа получить сумму для подвала таблицы. Можно добавить в SQL отдельный запрос с SUM. Можно накапливать сумму в обработчике RowDataBound, прибавляя значение каждой строки. А можно воспользоваться тем, что данные уже лежат в DataTable, и попросить её посчитать агрегат.
Накопление в RowDataBound выглядит просто, но при включённом пейджинге даёт итог только по видимой странице: строки остальных страниц не привязываются и в обработчик не попадают. Метод DataTable.Compute работает со всеми строками таблицы, независимо от того, какая страница сейчас на экране. Для отчётов на сотни и тысячи строк это самый короткий путь.
Как устроен DataTable.Compute
Метод принимает два аргумента: выражение агрегата и фильтр. Выражение пишется на том же мини-языке, что и вычисляемые колонки DataColumn.Expression: Sum, Avg, Count, Min, Max, StDev, Var от имени колонки. Фильтр задаётся как условие RowFilter у DataView, пустая строка или Nothing означает «все строки».
Результат возвращается как Object. Если в таблице нет строк или все значения NULL, вернётся DBNull.Value, и прямое приведение CDec упадёт с InvalidCastException. Поэтому результат всегда проверяем перед приведением.
' Сумма по всей колонке
Dim total As Object = dt.Compute("Sum(Amount)", "")
' Сумма только по оплаченным заказам за квартал
Dim paid As Object = dt.Compute("Sum(Amount)", _
"Status = 'Paid' AND OrderDate >= #2009-07-01#")
' Безопасное приведение: DBNull превращается в ноль
Private Function ToDecimal(ByVal value As Object) As Decimal
If value Is Nothing OrElse value Is DBNull.Value Then Return 0D
Return Convert.ToDecimal(value)
End FunctionВключаем подвал и выводим значения
У GridView подвал по умолчанию выключен, включаем его свойством ShowFooter. Текст для подвала колонки можно задать через FooterText, но у BoundField он статический. Удобнее заполнить ячейки подвала в обработчике RowDataBound, когда тип строки равен Footer: к этому моменту все строки данных уже привязаны.
Индексы ячеек совпадают с порядком колонок. Чтобы не ломать код при перестановке колонок, номера удобно вынести в константы или искать колонку по HeaderText.
Private _data As DataTable
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs) Handles Me.Load
If Not IsPostBack Then BindGrid()
End Sub
Private Sub BindGrid()
_data = SalesRepository.GetSales() ' DataTable с колонками Region, Qty, Amount
gvSales.DataSource = _data
gvSales.DataBind()
End Sub
Protected Sub gvSales_RowDataBound(ByVal sender As Object, ByVal e As GridViewRowEventArgs) _
Handles gvSales.RowDataBound
If e.Row.RowType <> DataControlRowType.Footer OrElse _data Is Nothing Then Return
Dim qty As Decimal = ToDecimal(_data.Compute("Sum(Qty)", ""))
Dim amount As Decimal = ToDecimal(_data.Compute("Sum(Amount)", ""))
Dim avg As Decimal = ToDecimal(_data.Compute("Avg(Amount)", ""))
e.Row.Cells(0).Text = "Итого"
e.Row.Cells(1).Text = qty.ToString("N0")
e.Row.Cells(2).Text = amount.ToString("N2")
e.Row.Cells(3).Text = "среднее " & avg.ToString("N2")
e.Row.Font.Bold = True
End SubПейджинг: таблица должна жить между запросами
В примере выше DataTable хранится в поле страницы и существует только в пределах одного запроса. Когда пользователь переходит на другую страницу, мы снова вызываем BindGrid, таблица заполняется заново, и подвал считается правильно. Если же данные привязываются один раз, а при смене страницы используется только ViewState, поле _data будет пустым, и итог не выведется.
Есть несколько вариантов: перечитывать данные из базы при каждой смене страницы, хранить DataTable в кэше с ключом пользователя или отчёта, либо считать итог один раз и класть готовые числа во ViewState. Последний вариант самый лёгкий по памяти: в состоянии страницы лежат три числа, а не вся таблица.
- перечитывание из базы: просто и всегда актуально, но лишний запрос на каждый щелчок;
- Cache с SqlCacheDependency: быстро, но требует настройки уведомлений в SQL Server;
- итоги во ViewState: минимум памяти, но итоги не обновятся, пока не будет новой привязки.
Итоги с учётом фильтра
Если над таблицей есть фильтр по региону или статусу, итог должен совпадать с тем, что видит пользователь. Здесь удобно применить одно и то же условие дважды: в DataView.RowFilter для привязки и вторым аргументом Compute для подвала. Так сумма не разойдётся с таблицей, даже если фильтр сложный.
Хорошая практика: показывать в подвале и количество строк, попавших под фильтр. Его даёт выражение Count(Id). Пользователь сразу видит, что цифра «Итого» относится к 37 заказам, а не ко всем.
' Один фильтр для таблицы и для итогов
Dim filter As String = String.Empty
If ddlRegion.SelectedValue <> "" Then
filter = "Region = '" & ddlRegion.SelectedValue.Replace("'", "''") & "'"
End If
Dim view As New DataView(_data, filter, "Amount DESC", DataViewRowState.CurrentRows)
gvSales.DataSource = view
gvSales.DataBind()
Dim rows As Integer = CInt(_data.Compute("Count(Region)", filter))
lblSummary.Text = String.Format("Строк: {0}", rows)Оформление итоговой строки
Подвал получает стиль FooterStyle, и его стоит визуально отделить от данных: верхняя граница потолще, полужирный шрифт, тот же фон, что и у шапки. Числа в подвале, как и в колонке, выравнивайте по правому краю и форматируйте с разделителями разрядов, иначе суммы разной длины не сравнить взглядом.
Если таблица широкая и прокручивается по горизонтали, подумайте о закреплении подвала через position: sticky, чтобы итог был виден без прокрутки до конца списка.
Как это делают сегодня
Вопросы и ответы
Почему Compute выдаёт ошибку «Invalid usage of aggregate function Sum() and Type: String»?
Колонка в DataTable имеет строковый тип, например после загрузки из Excel. Укажите тип колонки Decimal при создании таблицы или добавьте вычисляемую колонку с Convert(Amount, 'System.Decimal').
Подвал пустой, хотя ShowFooter включён. Что проверить?
Скорее всего, при постбэке DataTable не заполнена, и обработчик выходит по условию _data Is Nothing. Перепривязывайте данные или храните итоги во ViewState.
Можно ли посчитать итог по видимой странице, а не по всем строкам?
Да, накапливайте сумму в RowDataBound для строк DataRow и выводите её в подвале. Это корректно именно для промежуточного итога страницы.