Embedding a Detail GridView Inside a Master GridView...

This article shows how to display a row of a master GridView and then display one or more rows of a detail GridView on an ASP.NET page.


By: John Kilgo Date: March 23, 2009 Download the code.

I was recently asked to demonstrate a way to display a master-detail GridView whereby a the master GridView data was presented as a row of data, with the detail GridView displayed immediately below it as one or more rows of detailed data from the detail datasource. This is my attempt to answer that need.

Since you cannot create an embedded GridView at design-time, it must be done at runtime by dynamically adding the control and setting its properties. The best way to demonstrate the technique is get right to the code. The .aspx file (GridMasterDetail.aspx) is presented first below. The Northwind database provides a nice set of tables to use for our example. The Customers table is tied to the Orders table by the common column "CustomerID", while the Orders table and the Order Details table are tied together by the common column "OrderID". In the .aspx file we have included a DropDownList containing the list of Customers from the Customers table. The DataValueField of the DropDownList is the CustomerID. Upon choosing a Customer and then clicking the "Get Orders Data" button an OnClick event is called to get the Orders and Order Details data for that Customer. The master GridView in this case is the Orders table. All columns are shown in an html table forming our first row of Master data. We have to have a way of triggering the selection of Order Details data for the runtime building of the details GridView. For that we use the OnItemDataBound event. Most of the remainder of the code is just to make the grid look presentable.

<%@ Page Language="vb" Src="GridMasterDetail.aspx.vb" Inherits="DotNetJohn.GridMasterDetail"%>
<html>
<head>
<title>GridMasterDetail</title>
<style rel="StyleSheet">
.tablehead
{
font-family: Verdana, Arial, Helvetica, Sans-Serif;
background-color:Teal;
color:White;
font-size: 10pt;
}
.tablebody
{
font-family: Verdana, Arial, Helvetica, Sans-Serif;
background-color:Gainsboro;
color:Black;
font-size: 10pt;
}
</style>
</head>
<body>
<form id="OrderDetailGridView" method="post" runat="server">
<asp:Label ID="lblCustomer" Text="Choose Customer: " Font-Bold="True" Runat="server" />
<asp:DropDownList ID="ddlCustomers" Runat="server" />
<asp:Button ID="btnGetData" OnClick="Get_Data" Text="Get Orders Data" Runat="server" />
<p></p>
<asp:GridView id="dtgOrders" runat="server"
    AutoGenerateColumns="False"
    CellPadding="4" CellSpacing="0"
    Font-Names="Verdana, Arial, Helvetica, Sans-Serif"
    BorderColor="Black" BorderWidth="1"
    GridLines="Horizontal"
    OnItemDataBound="dtgOrders_OnItemDataBound">
    <HeaderStyle Font-Bold="True" Font-Size="x-small" Font-Name="Verdana"
              BackColor="AliceBlue" ForeColor="Black" />
  <ItemStyle Font-Size="x-small" />
  <AlternatingItemStyle BackColor="Ivory" />
  <Columns>
    <asp:BoundColumn DataField="OrderID" visible="False" />
    <asp:TemplateColumn ItemStyle-VerticalAlign="Top"
                        HeaderText="Order Information"
                        HeaderStyle-HorizontalAlign="Center">
      <ItemTemplate>
        <table width="100%" cellpadding="4" cellspacing="0">
          <tr class="tablehead">
            <th align="left">Order ID</th>
            <th align="left">Customer ID</th>
            <th align="left">Order Date</th>
            <th align="left">Required Date</th>
            <th align="left">Shipped Date</th>
          </tr>
          <tr class="tablebody">
            <td align="left" valign="top">
              <%# DataBinder.Eval(Container.DataItem, "OrderID") %>
            </td>
            <td align="left" valign="top">
              <%# DataBinder.Eval(Container.DataItem, "CustomerID") %>
            </td>
            <td align="left" valign="top">
              <%# DataBinder.Eval(Container.DataItem, "OrderDate", "{0:d}") %>
            </td>
            <td align="left" valign="top">
              <%# DataBinder.Eval(Container.DataItem, "RequiredDate", "{0:d}" ) %>
            </td>
            <td align="left" valign="top">
              <%# DataBinder.Eval(Container.DataItem, "ShippedDate", "{0:d}") %>
            </td>
          </tr>
        </table>
        <!-- Order Details GridView goes here -->
      </ItemTemplate>
    </asp:TemplateColumn>
  </Columns>
</asp:GridView>
</form>
</body>
</html>

As usual, the code-behind file contains most of the interesting code. It is displayed in two sections below. The only thing going on in the Page_Load sub is the loading of the DropDownList of customers from the Customers table. The Get_Data subroutine is our button click event from the .aspx page. It gets both the Orders table for the selected customer and the Order Details data and fills a DataSet with the two tables. Notice that all Order Details data is being retrieved. We will later have to filter that data so that it can be displayed for each Order table data row it belongs to. The master GridView's (dtgOrders) datasource is then set to the Orders table of the DataSet and the GridView is bound.

Imports System.Data
Imports System.Data.SqlClient
Imports System.Drawing
Imports System.Web.UI.WebControls
Imports System.Configuration

Namespace DotNetJohn

  Public Class GridMasterDetail : Inherits System.Web.UI.Page

    Protected dtgOrders As GridView
    Protected ddlCustomers As DropDownList
    Private _dataSet As DataSet = New DataSet()
    Private strCustID As String

    Public Sub Page_Load(sender As System.Object, e As System.EventArgs)
      If Not IsPostBack Then
        Dim objConn As SqlConnection
        Dim objCmd As SqlCommand
        Dim dataReader As SqlDataReader
        Dim strSql As String

        objConn = New SqlConnection(ConfigurationSettings.AppSettings.Get("NorthwindConnection"))
        strSql = "SELECT CustomerID, CompanyName FROM Customers ORDER BY CompanyName"
        objCmd = New SqlCommand(strSql, objConn)
        Try
          objConn.Open()
          dataReader = objCmd.ExecuteReader()
          With ddlCustomers
            .DataSource = dataReader
            .DataTextField = "CompanyName"
            .DataValueField = "CustomerID"
            .DataBind()
          End With
        Catch
        End Try
      End If
    End Sub

    Public Sub Get_Data(sender As object, e As System.EventArgs)
      strCustID = ddlCustomers.SelectedItem.Value
      Dim strSql As String
      strSql = "SELECT * FROM Orders WHERE CustomerID='" & strCustID & "'; " _
          & "SELECT OrderID, ProductID, UnitPrice, " _
          & "Quantity, Discount, UnitPrice * " _
          & "(1 - Discount) * Quantity AS SubTotal " _
          & "FROM [Order Details]" _

      Dim strConn As String = ConfigurationSettings.AppSettings.Get("NorthwindConnection")
      Dim _dataAdapter As SqlDataAdapter = New SqlDataAdapter(strSql, strConn)
      _dataAdapter.Fill(_dataSet)
      _dataSet.Tables(0).TableName = "Orders"
      _dataSet.Tables(1).TableName = "OrderDetails"
      dtgOrders.DataSource = _dataSet.Tables("Orders")
      dtgOrders.DataBind()
    End Sub

The remainder of the code-behind file consists of code to create and embed the details GridView containing the Order Details table data. As stated previously we are using the OnItemDataBound event to create each details GridView. Each time (row) the master GridView is bound, the OnItemDataBound event is called and we create our details grid. In the third line of code below we create the details GridView. The next 15 or so lines of code set properties of the new grid. After that there is a long section of code where we create bound columns for each of the columns in the Order Details table. We then create a DataView using the OrderID as a filter. The OrderID comes from the invisible BoundColumn we included on the .aspx page. After that we set the details GridView's datasource to the dataview and bind it. Lastly we add the the details GridView as the second cell of the master and we are done.

    Protected Sub dtgOrders_OnItemDataBound(sender As Object, e As GridViewItemEventArgs)
      If e.Item.ItemType = ListItemType.Item Or e.Item.ItemType = ListItemType.AlternatingItem Then
        Dim dtgOrderDetails As GridView = New GridView()

        dtgOrderDetails.Width = Unit.Pixel(720)
        dtgOrderDetails.BorderWidth = Unit.Pixel(1)
        dtgOrderDetails.CellPadding = 2
        dtgOrderDetails.CellSpacing = 0
        dtgOrderDetails.GridLines = GridLines.Horizontal
        dtgOrderDetails.BorderColor = Color.FromName("Black")
        dtgOrderDetails.HeaderStyle.BackColor = Color.FromName("Black")
        dtgOrderDetails.HeaderStyle.ForeColor = Color.FromName("White")
        dtgOrderDetails.HeaderStyle.Font.Bold = True
        dtgOrderDetails.HeaderStyle.Font.Size = FontUnit.XSmall
        dtgOrderDetails.ItemStyle.Font.Name = "Verdana"
        dtgOrderDetails.ItemStyle.Font.Size = FontUnit.XSmall
        dtgOrderDetails.AlternatingItemStyle.BackColor = Color.FromName("Gainsboro")
        dtgOrderDetails.AutoGenerateColumns = False

        Dim _boundColumn As BoundColumn = New BoundColumn()
        'Order ID
        _boundColumn.HeaderText = "Order ID"
        _boundColumn.DataField = "OrderID"
        dtgOrderDetails.Columns.Add(_boundColumn)
        'Product ID
        _boundColumn = New BoundColumn()
        _boundColumn.HeaderText = "Product ID"
        _boundColumn.DataField = "ProductID"
        dtgOrderDetails.Columns.Add(_boundColumn)
        'Unit Price
        _boundColumn = New BoundColumn()
        _boundColumn.HeaderText = "Unit Price"
        _boundColumn.DataField = "UnitPrice"
        _boundColumn.DataFormatString="{0:c}"
        dtgOrderDetails.Columns.Add(_boundColumn)
        'Quantity
        _boundColumn = New BoundColumn()
        _boundColumn.HeaderText = "Quantity"
        _boundColumn.DataField = "Quantity"
        dtgOrderDetails.Columns.Add(_boundColumn)
        'Discount
        _boundColumn = New BoundColumn()
        _boundColumn.HeaderText = "Discount"
        _boundColumn.DataField = "Discount"
        _boundColumn.DataFormatString="{0:p}"
        dtgOrderDetails.Columns.Add(_boundColumn)
        'SubTotal
        _boundColumn = New BoundColumn()
        _boundColumn.HeaderText = "SubTotal"
        _boundColumn.DataField = "SubTotal"
        _boundColumn.DataFormatString="{0:c}"
        _boundColumn.HeaderStyle.HorizontalAlign = HorizontalAlign.Right
        _boundColumn.ItemStyle.HorizontalAlign = HorizontalAlign.Right
        dtgOrderDetails.Columns.Add(_boundColumn)

        Dim _dataView As DataView = _dataSet.Tables("OrderDetails").DefaultView
        _dataView.RowFilter = "OrderID='" & e.Item.Cells(0).Text & "'"

        'Bind the GridView.
        dtgOrderDetails.DataSource = _dataView
        dtgOrderDetails.DataBind()

        'Add the dtgOrderDetails GridView
        e.Item.Cells(1).Controls.Add(dtgOrderDetails)
      End If
    End Sub

  End Class

End Namespace

You may download the code here.