Saturday, November 17, 2007

AggregateBoundField for GridView

I've been working on a reusable solution for adding aggregate totals to a GridView. One issue that I want to address is using page level variables to calculate totals. The second issue I would like to address is using column indexes to place the toals in the footer.

Here is the solution I have often seen for adding totals to a report:


int _iTotal = 0;

void gvReport_RowDataBound(object sender, GridViewRowEventArgs e) {

    if (e.Row.RowType == DataControlRowType.DataRow) {

        DataRow dr = ((DataRowView)e.Row.DataItem).Row;

        _iTotal += Convert.ToInt32(dr["count"]);

    }

    if (e.Row.RowType == DataControlRowType.Footer) {

        e.Row.Cells[0].Text = _iTotal.ToString();

    }

}

The problems I see here are:
a) _iTotal can be modified outside the scope of gvReport_RowDataBound but is only relevant within said scope.
b) If a cell is added in front of this column, we need to go back in and ajust our indexes because it has no logical link to the column. We could improve this slightly by using an enum to store the column indexes but the maintenance issue still remails.
c) Each column needs to implement the same logic to calculate totals.

For issue a) I suggest that it is possible to use the DataTable.Compute() method to easily obtain totals and averages.


e.Row.Cells[0].Text = _dt.Compute("SUM(count)");

For issue b) I believe that the aggregate function needs to be declared within the column. For my solution, I have implemented an AggregateBoundField that inherits from the BoundField control.


public class AggregateBoundField : BoundField {
    private AggregateType _eAggregateFunction;
 
    [Description("The function to use in DataTable.Compute()"),
    Category("Behavior"),
    DefaultValue(typeof(AggregateType), "SUM")]
    public AggregateType AggregateFunction {
        set { _eAggregateFunction = value; }
        get { return _eAggregateFunction; }
    }
}


For issue c) I have written a function that will itterate through all the AggregateBoundField controls in the GridView to assign the footer text.




private static void SetFooter(GridView grid, DataTable dataSource){
    string sExpressionFunction;
    object oComputedValue;
    AggregateBoundField af;

    foreach (DataControlField fcf in grid.Columns) {
        if (fcf is AggregateBoundField) {
            af = (AggregateBoundField)fcf;
            if (af.DataField.Length != 0) {
                sExpressionFunction = string.Format("{0}({1})", Enum.GetName(typeof(AggregateType), af.AggregateFunction), af.DataField);
                oComputedValue = dataSource.Compute(sExpressionFunction, "");
 
                if (af.DataFormatString.Length != 0) {
                    af.FooterText = string.Format(af.DataFormatString, oComputedValue);
                }
                else {
                    af.FooterText = oComputedValue.ToString();
                }
            }
        }
    }
}