Sunday, March 7, 2010

Control and access the binding ObjectDataSource instance programatically

The declarative data source provides a simple and transparent way of data binding for GridView. However, sometimes we need finer control of the ObjectDataSource instance that a GridView binds to. For example, it's usually easier to generate the footer in the data source rather than doing it in data bound event, like I did in this example. By accessing the ObjectDataSource instance directly, we can control when and how the binding happens as we desire.
In .aspx, we are not specifing the ObjectDataSource in GridView
<ajaxTool:TabPanel runat="server" HeaderText="Transaction Time" ID="serviceTab">
<ContentTemplate>
<asp:GridView ID="gvServiceTime" CssClass="datagrid" runat="server" ShowFooter="True"
          OnRowDataBound="gvServiceTime_RowDataBound" FooterStyle-CssClass="tfoot">
</asp:GridView>
</ContentTemplate>
</ajaxTool:TabPanel>
In .aspx.cs Page_Load, instantiate the DAO object, make the query and then assign it to GridView's DataSource. Then use this DAO object in the OnRowDataBound event to feed the footer. Prefer OnRowDataBound over OnDataBound because I want to customize the data row display and highlight some cells.
public partial class Report : System.Web.UI.Page
{
    ReportDAO _reportDao;

    protected void Page_Load(object sender, EventArgs e)
    {
        ...
        if (reportTabs.ActiveTab == serviceTab)
        {
            _reportDao = _reportDao ?? new ReportDAO();
            gvServiceTime.DataSource = _reportDao.GetServiceTimes(txtStartDate.Text, txtEndDate.Text);
            gvServiceTime.DataBind();
        }
        ...
    }

    /**
     * 1. Format each cell to mm:ss using Util.DurationAsString() rather than raw seconds
     * 2. Generate gvServiceTime footer
     */
    protected void gvServiceTime_RowDataBound(object sender, GridViewRowEventArgs e)
    {
        e.Row.HorizontalAlign = HorizontalAlign.Right;
        if (e.Row.RowType == DataControlRowType.DataRow)
        {
            int max = 0, maxsIndex = 0;
            for (int i = 1; i < e.Row.Cells.Count; i++) // i from 1 to skip the 1st col
            {   
                int duration = (int)_reportDao._report.Rows[e.Row.DataItemIndex][i];
                if (max < duration)
                {
                    max = duration;
                    maxsIndex = i;
                }
                e.Row.Cells[i].Text = Util.DurationAsString(duration);
            }
            if (maxsIndex > 0) // highlight the max cell in the row
                e.Row.Cells[maxsIndex].BackColor = System.Drawing.Color.Beige;
        }
        else if (e.Row.RowType == DataControlRowType.Footer)
        {
            e.Row.Cells[0].Text = "Average";
            for (int i = 1; i < _reportDao._footer.Length + 1; i++)
            {
                e.Row.Cells[i].Text = Util.DurationAsString(_reportDao._footer[i - 1]);
            }
        }
    }

    ...
}
This article also shows another way of adding dynamic columns in a table by using DataTable, which is far more flexible than arrays. Here is some snippet from DAO:
DataTable table = new DataTable();
table.Columns.Add(new DataColumn("TransactionType", typeof(string)));
foreach (string staff in staffs)
{
    table.Columns.Add(new DataColumn(staff, typeof(int)));
}
table.Columns.Add(new DataColumn("Average", typeof(int)));

foreach (TransactionType transactionType in _transactionTypes) // each row
{
    DataRow row = table.NewRow();
    row["TransactionType"] = transactionType.Name;
    int total = 0;
    int n = 0;
    foreach (string staff in staffs) // each col
    {
        ServiceTime serviceTime = counts[transactionType.Name]
            .Where(r => r.Staff == staff).SingleOrDefault();
        row[staff] = serviceTime == null ? 0 : serviceTime.Count;
        total += serviceTime == null ? 0 : serviceTime.Count;
        n += serviceTime == null ? 0 : 1;
    }
    row["Average"] = (int)(total / (n == 0 ? 1 : n)); // last col is average
    table.Rows.Add(row);
}