Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Expandable Rows in GridView

0.00/5 (No votes)
4 Dec 2012 3  
Add expandable rows to a GridView for master/detail view.
expandablegridviewrows/contracted.PNG

expandablegridviewrows/expanded.PNG

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:

  1. Clear the select parameters from the datasource for the details view and add a new one using the unique ID for the current record.
  2. Create a new GridView instance and specify its datasource, ID, CSS class and RowDataBound method. This will be our details GridView.
  3. Add bound fields to the new GridView.
  4. 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.
  5. 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.
  6. 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 GridViews, 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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here