Introduction
This article shows how to add functionality to the ASP.NET GridView
control to allow the display of master/detail records with expanding rows. It probably isn't suited to scenarios where large number of records will be returned at a time.
This example is based on work I did for an equine hospital showing appointments where the cost of treatments had exceeded the limit agreed with the client. Expanding the details lists the transactions which make up the cost.
Background
I was asked for this functionality by a client and found a few articles online presenting different solutions, but I found all of them to be overly-complex. However, some of those other solutions, such as this one, are more suitable for large record sets as my solution renders all of the details for each record when the page loads, rather than only when they are requested.
Using the Code
This solution doesn't require any special classes or custom controls, just a bit of JavaScript. It simply adds a new row after each existing row in the main (master) GridView
in the RowDataBound
event and creates an expanded details (detail) GridView
inside it.
The code for the ASP page looks like this:
<asp:GridView ID="grdOverLimitList" runat="server" AutoGenerateColumns="False"
DataSourceID="SQLOverLimitList" CssClass="gridview" AllowSorting="True"
AlternatingRowStyle-CssClass="alternating"
SortedAscendingHeaderStyle-CssClass="sortedasc"
SortedDescendingHeaderStyle-CssClass="sorteddesc"
FooterStyle-CssClass="footer" >
<AlternatingRowStyle CssClass="alternating"></AlternatingRowStyle>
<Columns>
<asp:TemplateField>
<ItemTemplate>
<%--This is a placeholder for the details GridView--%>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="PetID" HeaderText="Pet ID" SortExpression="Pet ID" />
<asp:BoundField DataField="AppointmentID" HeaderText="Appointment ID"
SortExpression="AppointmentID" />
<asp:BoundField DataField="Horse Name" HeaderText="Horse Name"
SortExpression="Horse Name" />
<asp:BoundField DataField="Client Surname" HeaderText="Client Surname"
SortExpression="Client Surname" />
<asp:BoundField DataField="Senior Clinician" HeaderText="Senior Clinician"
SortExpression="Senior Clinician" />
<asp:BoundField DataField="Warning Limit"
DataFormatString="{0:£#,##0.00;(£#,##0.00);''}"
HeaderText="Warning Limit" SortExpression="Warning Limit" />
<asp:BoundField DataField="Cost"
DataFormatString="{0:£#,##0.00;(£#,##0.00);''}"
HeaderText="Cost" SortExpression="Cost" />
</Columns>
<EmptyDataTemplate>
No data to display
</EmptyDataTemplate>
<FooterStyle CssClass="footer"></FooterStyle>
<SortedAscendingHeaderStyle CssClass="sortedasc"></SortedAscendingHeaderStyle>
<SortedDescendingHeaderStyle CssClass="sorteddesc"></SortedDescendingHeaderStyle>
</asp:GridView>
<asp:ToolkitScriptManager ID="ToolkitScriptManager1" runat="server">
</asp:ToolkitScriptManager>
<asp:SqlDataSource ID="SQLOverLimitList" runat="server"
ConnectionString="<%$ ConnectionStrings:DatabaseConnectionString %>"
SelectCommand="sp_Equine_OverLimitList" SelectCommandType="StoredProcedure">
</asp:SqlDataSource>
<asp:SqlDataSource ID="SQLOverLimitDetail" runat="server"
ConnectionString="<%$ ConnectionStrings:DatabaseConnectionString %>"
SelectCommand="sp_Equine_OverLimitDetail" SelectCommandType="StoredProcedure">
</asp:SqlDataSource>
That just sets up a simple GridView
, which is the master table. Note that it includes an empty ItemTemplate
column where the 'Show/Hide' button will go.
Next is the code behind for the RowDataBound
event of this GridView
:
Protected Sub grdOverLimitList_RowDataBound(ByVal sender As Object, _
ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
Handles grdOverLimitList.RowDataBound
If e.Row.RowType = DataControlRowType.DataRow Then
Dim appID As String = Convert.ToString(DataBinder.Eval_
(e.Row.DataItem, "AppointmentID")) SQLOverLimitDetail.SelectParameters.Clear() SQLOverLimitDetail.SelectParameters.Add("AppID", appID)
Dim gv As New GridView
gv.DataSource = SQLOverLimitDetail
gv.ID = "grdSQLOverLimitDetail" & e.Row.RowIndex gv.AutoGenerateColumns = False
gv.CssClass = "subgridview"
AddHandler gv.RowDataBound, AddressOf grdOverLimitDetails_RowDataBound
Dim bf1 As New BoundField
bf1.DataField = "Date Added"
bf1.DataFormatString = "{0:d}"
bf1.HeaderText = "Date Added"
gv.Columns.Add(bf1)
Dim bf2 As New BoundField
bf2.DataField = "Treatment"
bf2.HeaderText = "Treatment"
gv.Columns.Add(bf2)
Dim bf3 As New BoundField
bf3.DataField = "Total Cost"
bf3.HeaderText = "Total Cost"
bf3.DataFormatString = "{0:c}"
gv.Columns.Add(bf3)
Dim btn As Web.UI.WebControls.Image = New Web.UI.WebControls.Image
btn.ID = "btnDetail"
btn.ImageUrl = "~/Images/detail.gif"
btn.Attributes.Add("onclick", "javascript: gvrowtoggle_
(" & e.Row.RowIndex + (e.Row.RowIndex + 2) & ")")
Dim tbl As Table = DirectCast(e.Row.Parent, Table)
Dim tr As New GridViewRow(e.Row.RowIndex + 1, -1, _
DataControlRowType.EmptyDataRow, DataControlRowState.Normal)
tr.CssClass = "hidden"
Dim tc As New TableCell()
tc.ColumnSpan = grdOverLimitList.Columns.Count
tc.BorderStyle = BorderStyle.None
tc.BackColor = Drawing.Color.AliceBlue
tc.Controls.Add(gv) tr.Cells.Add(tc) tbl.Rows.Add(tr) e.Row.Cells(0).Controls.Add(btn)
gv.DataBind()
End If
End Sub
This is heavily annotated so hopefully it explains itself but it contains these basic steps:
- Clear the select parameters from the datasource for the details view and add a new one using the unique ID for the current record.
- Create a new
GridView
instance and specify its datasource, ID, CSS class and RowDataBound
method. This will be our details GridView
.
- Add bound fields to the new
GridView
.
- Create the show/hide button in the master record row, adding the '
OnClick
' attribute to point to the JavaScript coming up below. Note that it is actually an image, not a button, to remove any question of postback.
- Create a new, empty row after the current row in the master
GridView
, then populate it with the new GridView
, and add the show/hide button to the master record row.
- Bind the new
GridView
to its datasource.
You will notice that in creating the show/hide button, this code passes the index of the relevant details row (I know the formula looks a bit crazy, but trust me!) as the variable rows in the JavaScript below. This script is at the top of my ASP.NET page in the usual way.
<script type="text/javascript">
function gvrowtoggle(row) {
try {
row_num = row; ctl_row = row - 1; rows = document.getElementById('<%= grdOverLimitList.ClientID %>').rows;
rowElement = rows[ctl_row]; img = rowElement.cells[0].firstChild;
if (rows[row_num].className !== 'hidden') {
rows[row_num].className = 'hidden'; img.src = '../Images/detail.gif'; }
else {
rows[row_num].className = ''; img.src = '../Images/close.gif'; }
}
catch (ex) {alert(ex) }
}
</script>
Some pretty simple code, when the show/hide button is clicked this just checks the current class of the details row, setting it to 'hidden' if it's visible and setting it to the default class if it's hidden.
Finally, we just need to create a CSS class called 'hidden
' which will hide the detail row until it's requested. I've also included my CSS for styling the GridView
s, in case you're interested:
.hidden
{
display: none;
}
.gridview
{
width: 100%;
border: 1px solid black;
background: white;
text-align: center;
}
.gridview th
{
text-align: center;
background: #013b82;
color: white;
}
.gridview .pager
{
text-align: center;
background: #013b82;
color: White;
font-weight: bold;
border: 1px solid #013b82;
}
.gridview .pager a
{
color: #666;
}
.gridview a
{
text-decoration: none;
color: White;
}
.gridview a:hover
{
color: Silver;
}
.gridview .sortedasc
{
background-color: #336699;
}
.gridview .sortedasc a
{
padding-right: 15px;
background-image :url(../images/up_arrow.png);
background-repeat: no-repeat;
background-position: right center;
}
.gridview .sortedasc a:hover
{
color:White;
}
.gridview .sorteddesc
{
background-color: #336699;
}
.gridview .sorteddesc a
{
padding-right: 15px;
background-image :url(../images/down_arrow.png);
background-repeat: no-repeat;
background-position: right center;
}
.gridview .sorteddesc a:hover
{
color:White;
}
.gridview .alternating
{
background: #d8d8d8;
}
.subgridview
{
width:80%;
border: none;
text-align:left;
margin: 0px 0px 5px 25px;
background: whitesmoke;
}
.subgridview th
{
background: silver;
color: Black;
}
Points of Interest
There isn't anything particularly difficult about this, it just took some thinking through. The one thing that did drive me a bit mad was that, whereas in Firefox/Chrome the default style for a GridViewRow
is 'table-row', in Internet Explorer it's 'block'. That's why in the JavaScript when the details row is being made visible, the className
is left blank rather, causing the browser to use whatever is default.
History
- 22nd February, 2011: Initial post