This article illustrates how to implement Three (3) Levels of GridView.
 
 
Database
This article uses Microsoft’s Northwind Database. You can download it from here.
 
 
HTML Markup
The HTML Markup consists of:
GridView – For displaying parent records.
Inside a 
GridView there another 
GridView which is used to display child records.
 
Note: The CustomerID and OrderID fields are stored in the DataKeys property of the GridView as that will be required to populate the Child or Nested GridView of Orders as well as Products.
 
The following HTML Markup consists of:
Customers GridView: The 
Customers GridView is the Main 
GridView and it consists of a ImageButton and Panel inside a 
TemplateField Column.
 
The Panel consists of another 
GridView i.e. 
Orders GridView which in turn consists of 
Products GridView.
 
The 
Customers and 
Orders GridView consists of 
ImageButton to hide and show the respective Child GridViews.
 
<asp:GridView ID="gvCustomers" runat="server" AutoGenerateColumns="false" CssClass="Grid"
    DataKeyNames="CustomerID">
    <Columns>
        <asp:TemplateField>
            <ItemTemplate>
                <asp:ImageButton ID="imgOrdersShow" runat="server" OnClick="OnShowHideOrdersGrid"
                    ImageUrl="~/images/plus.png" CommandArgument="Show" />
                <asp:Panel ID="pnlOrders" runat="server" Visible="false" Style="position:relative">
                    <asp:GridView ID="gvOrders" runat="server" AutoGenerateColumns="false" PageSize="5"
                        AllowPaging="true" OnPageIndexChanging="OnOrdersGrid_PageIndexChanging" CssClass="ChildGrid"
                        DataKeyNames="OrderId">
                        <Columns>
                            <asp:TemplateField>
                                <ItemTemplate>
                                    <asp:ImageButton ID="imgProductsShow" runat="server" OnClick="OnShowHideProductsGrid"
                                        ImageUrl="~/images/plus.png" CommandArgument="Show" />
                                    <asp:Panel ID="pnlProducts" runat="server" Visible="false" Style="position:relative">
                                        <asp:GridView ID="gvProducts" runat="server" AutoGenerateColumns="false" PageSize="2"
                                            AllowPaging="true" OnPageIndexChanging="OnProductsGrid_PageIndexChanging" CssClass="Nested_ChildGrid">
                                            <Columns>
                                                <asp:BoundField ItemStyle-Width="150px" DataField="ProductId" HeaderText="Product Id" />
                                                <asp:BoundField ItemStyle-Width="150px" DataField="ProductName" HeaderText="Product Name" />
                                            </Columns>
                                        </asp:GridView>
                                    </asp:Panel>
                                </ItemTemplate>
                            </asp:TemplateField>
                            <asp:BoundField ItemStyle-Width="150px" DataField="OrderId" HeaderText="Order Id" />
                            <asp:BoundField ItemStyle-Width="150px" DataField="OrderDate" HeaderText="Date" />
                        </Columns>
                    </asp:GridView>
                </asp:Panel>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField ItemStyle-Width="150px" DataField="ContactName" HeaderText="Contact Name" />
        <asp:BoundField ItemStyle-Width="150px" DataField="City" HeaderText="City" />
    </Columns>
</asp:GridView>
 
 
 
Namespaces
You will need to import the following namespaces.
C#
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
 
 
VB.Net
Imports System.Data
Imports System.Data.SqlClient
Imports System.Configuration
 
 
 
Binding the Customers records to the Level 1 GridView
Inside the 
Page_Load event, 
GetData method is called and the records of 
Customers table are fetched and displayed in 
GridView.
 
Note: GetData is a generic function and the same function discussed above is used here.
 
C#
protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
         gvCustomers.DataSource = GetData("select top 10 * from Customers");
        gvCustomers.DataBind();
    }
}
 
private static DataTable GetData(string query)
{
    string constr = ConfigurationManager.ConnectionStrings["constr"].ConnectionString;
    using (SqlConnection con = new SqlConnection(constr))
    {
        using (SqlCommand cmd = new SqlCommand())
        {
            cmd.CommandText = query;
            using (SqlDataAdapter sda = new SqlDataAdapter())
            {
                cmd.Connection = con;
                sda.SelectCommand = cmd;
                using (DataSet ds = new DataSet())
                {
                    DataTable dt = new DataTable();
                    sda.Fill(dt);
                    return dt;
                }
            }
        }
    }
}
 
 
VB.Net
Protected Sub Page_Load(sender As Object, e As EventArgs) Handles Me.Load
    If Not Me.IsPostBack Then
         gvCustomers.DataSource = GetData("select top 10 * from Customers")
        gvCustomers.DataBind()
    End If
End Sub
 
Private Shared Function GetData(query As String) As DataTable
    Dim constr As String = ConfigurationManager.ConnectionStrings("constr").ConnectionString
    Using con As New SqlConnection(constr)
        Using cmd As New SqlCommand()
            cmd.CommandText = query
            Using sda As New SqlDataAdapter()
                cmd.Connection = con
                sda.SelectCommand = cmd
                Using ds As New DataSet()
                    Dim dt As New DataTable()
                    sda.Fill(dt)
                    Return dt
                End Using
            End Using
        End Using
    End Using
End Function
 
 
 
Displaying the Child (Level 2) GridView with the Orders for each Customer in the Parent (Level 1) GridView
On the 
Click event of the 
Expand ImageButton of the Parent 
GridView, first the Child 
GridView in the corresponding 
GridView Row is referenced and then populated with the records from the 
Orders table of the Northwind Database based on 
CustomerId stored in the 
DataKey property for the row.
 
The CommandArgument property of the ImageButton is used to determine whether the operation is of Expand or Collapse.
The Paging for the Child 
GridView is done using the 
OnOrdersGrid_PageIndexChanging event.
 
C#
protected void OnShowHideOrdersGrid(object sender, EventArgs e)
{
    ImageButton imgShowHide = (sender as ImageButton);
    GridViewRow row = (imgShowHide.NamingContainer as GridViewRow);
    if (imgShowHide.CommandArgument == "Show")
    {
        row.FindControl("pnlOrders").Visible = true;
        imgShowHide.CommandArgument = "Hide";
        imgShowHide.ImageUrl = "~/images/minus.png";
        string customerId = gvCustomers.DataKeys[row.RowIndex].Value.ToString();
        GridView gvOrders = row.FindControl("gvOrders") as GridView;
        this.BindOrders(customerId,gvOrders);
    }
    else
    {
        row.FindControl("pnlOrders").Visible = false;
        imgShowHide.CommandArgument = "Show";
        imgShowHide.ImageUrl = "~/images/plus.png";
    }
}
 
private void BindOrders(string customerId, GridView gvOrders)
{
    gvOrders.ToolTip = customerId;
    gvOrders.DataSource = GetData(string.Format("select * from Orders where CustomerId='{0}'", customerId));
    gvOrders.DataBind();
}
 
protected void OnOrdersGrid_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    GridView gvOrders = (sender as GridView);
    gvOrders.PageIndex = e.NewPageIndex;
    this.BindOrders(gvOrders.ToolTip, gvOrders);
}
 
 
VB.Net
Protected Sub OnShowHideOrdersGrid(sender As Object, e As EventArgs)
    Dim imgShowHide As ImageButton = TryCast(sender, ImageButton)
    Dim row As GridViewRow = TryCast(imgShowHide.NamingContainer, GridViewRow)
    If imgShowHide.CommandArgument = "Show" Then
        row.FindControl("pnlOrders").Visible = True
        imgShowHide.CommandArgument = "Hide"
        imgShowHide.ImageUrl = "~/images/minus.png"
        Dim customerId As String = gvCustomers.DataKeys(row.RowIndex).Value.ToString()
        Dim gvOrders As GridView =  TryCast(row.FindControl("gvOrders"), GridView)
        Me.BindOrders(customerId,gvOrders)
    Else
        row.FindControl("pnlOrders").Visible = False
        imgShowHide.CommandArgument = "Show"
        imgShowHide.ImageUrl = "~/images/plus.png"
    End If
End Sub
 
Private Sub BindOrders(customerId As String, gvOrders As GridView)
    gvOrders.ToolTip = customerId
    gvOrders.DataSource = GetData(String.Format("select * from Orders where CustomerId='{0}'", customerId))
    gvOrders.DataBind()
End Sub
 
Protected Sub OnOrdersGrid_PageIndexChanging(sender As Object, e As GridViewPageEventArgs)
    Dim gvOrders As GridView = TryCast(sender, GridView)
    gvOrders.PageIndex = e.NewPageIndex
    Me.BindOrders(gvOrders.ToolTip, gvOrders)
End Sub
 
 
 
Displaying the Child (Level 3) GridView with the Products for each Order placed by a Customer in the Products Grid (Level 3) GridView
On the 
Click event of the 
Expand ImageButton of the Parent 
GridView, first the Child 
GridView in the corresponding 
GridView Row is referenced and then populated with the records from the 
Products table of the Northwind Database based on 
OrderID stored in the 
DataKey property for the row.
 
The CommandArgument property of the ImageButton is used to determine whether the operation is of Expand or Collapse.
The Paging for the Child 
GridView is done using the 
OnProductsGrid_PageIndexChanging event.
 
C#
protected void OnShowHideProductsGrid(object sender, EventArgs e)
{
    ImageButton imgShowHide = (sender as ImageButton);
    GridViewRow row = (imgShowHide.NamingContainer as GridViewRow);
    if (imgShowHide.CommandArgument == "Show")
    {
        row.FindControl("pnlProducts").Visible = true;
        imgShowHide.CommandArgument = "Hide";
        imgShowHide.ImageUrl = "~/images/minus.png";
        int orderId = Convert.ToInt32((row.NamingContainer as GridView).DataKeys[row.RowIndex].Value);
        GridView gvProducts = row.FindControl("gvProducts") as GridView;
        this.BindProducts(orderId, gvProducts);
    }
    else
    {
        row.FindControl("pnlProducts").Visible = false;
        imgShowHide.CommandArgument = "Show";
        imgShowHide.ImageUrl = "~/images/plus.png";
    }
}
 
private void BindProducts(int orderId, GridView gvProducts)
{
    gvProducts.ToolTip = orderId.ToString();
    gvProducts.DataSource = GetData(string.Format("SELECT ProductId, ProductName FROM Products WHEREProductId IN (SELECT ProductId FROM [Order Details] WHEREOrderId = '{0}')", orderId));
    gvProducts.DataBind();
}
 
protected void OnProductsGrid_PageIndexChanging(object sender, GridViewPageEventArgs e)
{
    GridView gvProducts = (sender as GridView);
    gvProducts.PageIndex = e.NewPageIndex;
    this.BindProducts(int.Parse(gvProducts.ToolTip), gvProducts);
}
 
 
VB.Net
Protected Sub OnShowHideProductsGrid(sender As Object, e As EventArgs)
    Dim imgShowHide As ImageButton = TryCast(sender, ImageButton)
    Dim row As GridViewRow = TryCast(imgShowHide.NamingContainer, GridViewRow)
    If imgShowHide.CommandArgument = "Show" Then
        row.FindControl("pnlProducts").Visible = True
        imgShowHide.CommandArgument = "Hide"
        imgShowHide.ImageUrl = "~/images/minus.png"
        Dim orderId As Integer = Convert.ToInt32(TryCast(row.NamingContainer, GridView).DataKeys(row.RowIndex).Value)
        Dim gvProducts As GridView = TryCast(row.FindControl("gvProducts"), GridView)
        Me.BindProducts(orderId,gvProducts)
    Else
        row.FindControl("pnlProducts").Visible = False
        imgShowHide.CommandArgument = "Show"
        imgShowHide.ImageUrl = "~/images/plus.png"
    End If
End Sub
 
Private Sub BindProducts(orderId As Integer, gvProducts As GridView)
    gvProducts.ToolTip = orderId.ToString()
    gvProducts.DataSource = GetData(String.Format("SELECT ProductId, ProductName FROM Products WHEREProductId IN (SELECT ProductId FROM [Order Details] WHEREOrderId = '{0}')", orderId))
    gvProducts.DataBind()
End Sub
 
Protected Sub OnProductsGrid_PageIndexChanging(sender As Object, e As GridViewPageEventArgs)
    Dim gvProducts As GridView = TryCast(sender, GridView)
    gvProducts.PageIndex = e.NewPageIndex
    Me.BindProducts(Integer.Parse(gvProducts.ToolTip), gvProducts)
End Sub
 
 
 
Client Side
Inside the HTML Markup, the following 
jQuery JS file is inherited.
 
1. jquery.min.js
 
Inside the 
jQuery document ready event handler, a loop is executed for all the 
Orders and 
Products ImageButton and the corresponding Child GridViews appended next to the Button. 
 
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script type="text/javascript">
    $(function () {
        $("[id*= imgOrdersShow]").each(function () {
            if ($(this)[0].src.indexOf("minus") != -1) {
                $(this).closest("tr").after("<tr><td></td><td colspan = '999'>" + $(this).next().html() + "");
                $(this).next().remove();
            }
        });
        $("[id*= imgProductsShow]").each(function () {
            if ($(this)[0].src.indexOf("minus") != -1) {
                $(this).closest("tr").after("<tr><td></td><td colspan = '999'>" + $(this).next().html() + "");
                $(this).next().remove();
            }
        });
    });
</script>
 
 
 
CSS Style for displaying GridViews
The GridView’s have been assigned with the following CSS.
<style type="text/css">
    body {font-family: Arial; font-size: 10pt; }
    .Grid th {background-color: #6C6C6C; color: White; font-size: 10pt; line-height: 200%; }
    .Grid td {background-color: #eee; color: black; font-size :10pt; line-height: 200%; }
    .ChildGrid th {background-color: #6C6C6C; color: #fff; font-size: 10pt; line-height: 200%; }
    .ChildGrid td {background-color: #fff; color: black; font-size: 10pt; line-height: 200%; }
</style>
 
 
 
Screenshot
 
 
Demo
 
 
Downloads