Expandable Rows in GridView
Add expandable rows to a GridView for master/detail view.
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
'Configure the datasource for the expanded details
Dim appID As String = Convert.ToString(DataBinder.Eval_
(e.Row.DataItem, "AppointmentID")) 'Get the unique ID for this record
SQLOverLimitDetail.SelectParameters.Clear() 'Remove all select parameters
'from the datasource for the expanded details
SQLOverLimitDetail.SelectParameters.Add("AppID", appID) 'Add the select parameter
'to the datasource for the expanded details using the unique ID of the record
'Create a new GridView for displaying the expanded details
Dim gv As New GridView
gv.DataSource = SQLOverLimitDetail
gv.ID = "grdSQLOverLimitDetail" & e.Row.RowIndex 'Since a gridview is
'being created for each row they each need a unique ID, so append the row index
gv.AutoGenerateColumns = False
gv.CssClass = "subgridview"
AddHandler gv.RowDataBound, AddressOf grdOverLimitDetails_RowDataBound 'Add a
'rowdatabound method for the new GridView
'Add fields to the expanded details GridView
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)
'Create the show/hide button which will be displayed on each row of the main GridView
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) & ")") 'Adds the javascript
'function to the show/hide button, passing the row to be toggled as a parameter
'Add the expanded details row after each record in the main GridView
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) 'Add the expanded details GridView to the newly-created cell
tr.Cells.Add(tc) 'Add the newly-created cell to the newly-created row
tbl.Rows.Add(tr) ' Add the newly-ccreated row to the main GridView
e.Row.Cells(0).Controls.Add(btn) 'Add the show/hide button to the main GridView row
gv.DataBind() 'Bind the expanded details GridView to its datasource
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 andRowDataBound
method. This will be our detailsGridView
. - 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 newGridView
, 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; //row to be hidden
ctl_row = row - 1; //row where show/hide button was clicked
rows = document.getElementById('<%= grdOverLimitList.ClientID %>').rows;
rowElement = rows[ctl_row]; //elements in row where show/hide button was clicked
img = rowElement.cells[0].firstChild; //the show/hide button
if (rows[row_num].className !== 'hidden') //if the row is not currently hidden
//(default)...
{
rows[row_num].className = 'hidden'; //hide the row
img.src = '../Images/detail.gif'; //change the image for the show/hide button
}
else {
rows[row_num].className = ''; //set the css class of the row to default
//(to make it visible)
img.src = '../Images/close.gif'; //change the image for the show/hide button
}
}
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---------------------------------------------*/
.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------------------------------------------*/
.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