Thursday, March 4, 2010

Dynamic column in GridView binding an array

Recently I've done some manual reporting using ASP.NET. Well, orginally I developed it easily using SQL Server 2008 Reporting Service, which is not in our production environment yet, but the client was keen to have the application deployed.
Dynamic columns is actually easy in GridView because you can bind the GridView to whatever data source with just a little more work than the simple declarative binding. For example, you can bind it to a DataTable like this. In my case, the report has fixed number of rows each of which represents a transaction type, while the columns are dynamic depending on another database table for zones. To feed this report grid, an OjbectDataSource is created which returns an array of ReportRow.
public class ReportRow
{
    public string CallType { get; set; }
    public int[] Counts { get; set; } // Counts array for each location + the last is total
}
In .aspx file, we must set AutoGenerateColumns to false. The gvImportCounts binds to an ObjectDataSource with 2 parameters for the reporting period. The OnDataBound event is to add a footer in the report.
<ajaxTool:TabPanel runat="server" ID="importTab">
  <HeaderTemplate>Import Counts</HeaderTemplate>
  <ContentTemplate>
    <asp:GridView ID="gvImportCounts" CssClass="datagrid" runat="server" AutoGenerateColumns="False"
         DataSourceID="odsImportCounts" ShowFooter="True" Width="658px"
         OnDataBound="gvImportCounts_DataBound">
      <FooterStyle CssClass="tfoot" />
    </asp:GridView>

    <asp:ObjectDataSource ID="odsImportCounts" runat="server" SelectMethod="GetImportCounts"
        TypeName="DAO.ReportDAO">
      <SelectParameters>
        <asp:ControlParameter ControlID="txtStartDate" Name="start" PropertyName="Text" />
        <asp:ControlParameter ControlID="txtEndDate" Name="end" PropertyName="Text" />
      </SelectParameters>
    </asp:ObjectDataSource>
  </ContentTemplate>
</ajaxTool:TabPanel>
The difficulty of binding to array elements, like Frdrik pointed out in his blog (Ref 1), is that the ASP.NET's standard BoundField does not provide this functionality. So one of the elegant ways is to extend it by ourselves.
In aspx.cs, add this subclass that enables us to evaluate array elements:
/**
* Extend the BoundField to bind column to composite field even array element
*/
public class CompositeBoundField : BoundField
{
    protected override object GetValue(Control controlContainer)
    {
        object item = DataBinder.GetDataItem(controlContainer);
        return DataBinder.Eval(item, this.DataField);
    }
}
The rest important code in aspx.cs:
protected void Page_Load(object sender, EventArgs e)
{
    if (!Page.IsPostBack) // first load
    {   // Dynamically generate columns for Import report:
        BoundField bf = new BoundField();
        bf.DataField = "Import";
        bf.HeaderText = "Transaction Type";
        gvImportCounts.Columns.Add(bf);

        var zones = ReportDAO._zones; // get zones from DAO

        for (int i = 0; i &lt; zones.Count; i++)
        {
            bf = new CompositeBoundField();
            //Initalize the DataField and HeaderText field value:
            bf.DataField = "Counts[" + i + "]"; // bind to array element
            bf.HeaderText = zones[i].Name;

            //Add the newly created bound field to the GridView:
            gvImportCounts.Columns.Add(bf);
        }
        // Add the Total column
        bf = new CompositeBoundField();
        bf.DataField = "Counts[" + zones.Count + "]";
        bf.HeaderText = "Total";
        gvImportCounts.Columns.Add(bf);
    }
}

/**
* Generate gvImportCounts footer
*/
protected void gvImportCounts_DataBound(object sender, EventArgs e)
{
    GridView grid = (GridView)sender;
    GridViewRow footer = grid.FooterRow;
    if (footer != null) // footer defined, so fill it
    {
        footer.Cells[0].Text = "Total";
        for (int i = 1; i &lt; grid.Columns.Count; i++)
        {
            int total = 0;
            foreach (GridViewRow row in grid.Rows)
            {
                if (row.RowType == DataControlRowType.DataRow)
                    total += int.Parse(row.Cells[i].Text);
            }
            footer.Cells[i].Text = total.ToString();
        }
    }
}
Here is the result: Ref:
  1. Fixing BoundField Support for Composite Objects
  2. GridView and dynamic data sources

1 comment:

Anonymous said...

Thank a lot!