Adding (or Inserting) Subheader Rows into a Gridview

By | April 20, 2008

Ever come across a situation where you needed to manually add (or insert) rows to a Gridview, but weren’t quite sure how to do it? A few weeks ago I came across this exact problem, where I needed to create sub headers based on the category for each row. Here’s how I did it.

So here’s the problem that I’m trying to solve. As you can see in the first image, I have a table of restaurants with the second column containing the category that the restaurant falls under (Fast Food, Italian, Pizza, Japanese, ….).

Gridview - Before image

This second image contains the exact same, unmodified listing of restaurants. The difference being that the restaurants are grouped under a Restaurant Category sub heading.
Gridview - After image

When I first tried to solve this problem, I thought that it would be pretty easy. The Gridview.Rows must have an Add() (or Insert()) method that I could call and pass in a GridviewRow object, right?

Wrong. Unfortunately to my surprise, the Gridview doesn’t support this ability.

After a bit of trial and error, and some Googling, I came up with a somewhat simple way to accomplish this problem, and I’m going to show you how.

So let’s begin. First off, we’ll need a DataSet of some data to display in the Gridview. This can be anything you’d like, as long as there is some sort of category field. For me, I manually created a DataSet of restaurants which includes a “category” field. This can be named anything you’d like.

Below is a partial screenshot of my DataSet.
DataSet of Restaurants

Next, we’ll need a Gridview. Here is what I have on my page. Nothing too fancy, just a regular Gridview with a few columns and some simple styling.

<h3>Restaurant Listing</h3>
<asp:gridview id="gdvRestaurants" autogeneratecolumns="false" 
              emptydatatext="No Restaurants were found."
              onrowdatabound="gdvRowDatabound" cellpadding="5"
              width="860" runat="server">
    <headerstyle backcolor="#507CD1" forecolor="white" />
    <rowstyle font-size="Small" />
    <alternatingrowstyle backcolor="#DCE4F9" /> 
        <asp:boundfield datafield="RestaurantId" headertext="Id" />
        <asp:boundfield datafield="Category" headertext="Category" />
        <asp:boundfield datafield="Name" headertext="Name" />
        <asp:boundfield datafield="Phone" headertext="Phone" />
        <asp:boundfield datafield="Address" headertext="Address" />
        <asp:boundfield datafield="Zip" headertext="Zip" />

Pretty harmless. If you’re confused about anything in the above code, let me briefly explain. AutoGenerateColumns has been set to false because I want to add my own columns to display. I’ve also added a EmptyDataText just in case there’s nothing to display.

Next I’ve added a OnRowDatabound attribute and set it to gdvRowDataBound, which is the name of the method that we will create shortly.

After that, there’s some simple styling, such as cellpadding, forecolor, backcolor, width and font-size. These should all be pretty self explanatory.

Then I’ve added several columns to be displayed, and this is why I set AutoGenerateColumns to false.

Since we already specified the method to be called when the OnRowDatabound event is raised, we’ll need to add that event to our code behind/beside page (*.aspx.cs for C# or *.aspx.vb for Visual basic).

protected void gdvRowDatabound(object sender, GridViewRowEventArgs e)

Unlike MTV’s show “Cribs”, this is were the magic will happen, and not in the bedroom. Okay, that was bad, but I tried.

Still following? Good.

What good is a Gridview without data? We’ll need to some how give our Gridview data. This can be done anyway that you prefer, whether it be through a DataSource such as a ObjectDataSource, SqlDataSource or AccessDataSource. Or you can call a method from your code behind/beside.

For this example, since I didn’t have any data to use, I manually created a DataSet inside of a method, and called this method in the Page_Load().

protected void Page_Load(object sender, EventArgs e)
    if (!Page.IsPostBack)
        // GetRestaurants() returns a DataSet of Restaurants.
        this.gdvRestaurants.DataSource = GetRestaurants();

Pretty simple. All that I’m doing is checking if a PostBack caused the Page_Load(), and if not then load the Gridview with the data from the GetRestaurants() method. If it is a PostBack(), then the data will be kept in the viewstate, so we don’t need to fetch it.

If you run your project, you should get something similar to the first image above.

Now onto the “magic” that I spoke of.

Above the Page_Load(), add the following line.

// Used to store the last Category Name between each RowDataBound
private string tmpCategoryName = "";

We will use this variable to check whether the current row’s category is the same as the previous row’s category. If it’s not the same, then we will need to create the sub header row. If it is the same as the previous row, then we can skip creating the sub header.

If we were to declare this variable within the OnRowDatabound, it would always be start off being empty, and therefore we wouldn’t be able to compare it’s value to the current row’s value.

Now to that OnRowDatabound that I keep speaking of.

First, we’ll need to check to see if the current row is a DataRow, and not something else like a HeaderRow. If it is a DataRow, then we will continue doing our stuff to create the sub header row.

To check if it’s a DataRow, add the following to your OnRowDatabound() method.

// If the current row is a DataRow (and not a Header or Footer row), then do stuff.
if (e.Row.RowType == DataControlRowType.DataRow)

Inside of that, we will need to retrieve the value for the category field of the current row. Do to this we will need to cast the current row as a DataRowView, and then compare it’s “category” field to the value in the tmpCategoryName variable. If the two value are not the same, then we will need to set the value of tmpCategoryName to the new value.

// If the current row is a DataRow (and not a Header or Footer row), then do stuff.
if (e.Row.RowType == DataControlRowType.DataRow)
    DataRowView drv = (DataRowView)e.Row.DataItem;
    if (tmpCategoryName != drv["Category"].ToString())
        tmpCategoryName = drv["Category"].ToString();

As I mentioned earlier, the GridView doesn’t have a means of adding a row to it. So what we will do is create a reference to the current row’s Parent, which is a Table object, and happens to be the GridView itself. Once we do that, we will be safe and check to make sure that the reference isn’t null.

// Get a reference to the current row's Parent, which is the Gridview (which happens to be a table)
Table tbl = e.Row.Parent as Table;
if (tbl != null)

Now we can create the sub header row, and add it to the table (GridView). To do this we will need to create a GridViewRow object which will be the sub header row, and we will need to create a TableCell to place the category name into.

I’ve also applied some styling such as font-weight, background-color and the font color to the cell.

At the end, I’ll add the category name, which is wrapped in a span tag, into the TableCell, and then add the TableCell to the GridViewRow, and then add the GridViewRow to the table.

if (tbl != null)
    GridViewRow row = new GridViewRow(-1, -1, DataControlRowType.DataRow, DataControlRowState.Normal);
    TableCell cell = new TableCell();

    // Span the row across all of the columns in the Gridview
    cell.ColumnSpan = this.gdvRestaurants.Columns.Count;

    cell.Width = Unit.Percentage(100);
    cell.Style.Add("font-weight", "bold");
    cell.Style.Add("background-color", "#c0c0c0");
    cell.Style.Add("color", "white");  

    HtmlGenericControl span = new HtmlGenericControl("span");
    span.InnerHtml = tmpCategoryName;


    tbl.Rows.AddAt(tbl.Rows.Count - 1, row);

Now if you were to run the project again, you should have something that looks like the following.

Gridview - After image

And there we have it.

Just because I’m a nice guy, I’ve decided to include the code as a zip file which can be downloaded here.

58 thoughts on “Adding (or Inserting) Subheader Rows into a Gridview

  1. Pingback:

  2. carlj Post author

    OOoops, you’re right. Thanks for pointing that out. Not only is it incomplete, it wasn’t even the correct folder that I zipped.

    I’ll upload the correct one shortly.

    Thanks again

  3. carlj Post author

    I’ve re-uploaded the zip, and it’s the correct one this time 🙂

    Thanks again

  4. Mike P.

    Very nice and well presented! This was the simplest solution I found and helped me a great deal. Thanks.

  5. carlj Post author

    Hey Mike P, no problem, glad I could help.

    Thanks for the comments 🙂

  6. Silvano

    I have problem witrh the pager.
    The cell of the pager set columnspan ever at 1.
    thanks for the code

  7. Raj

    Hi, This is very good and understanding , if do i want one more sub header , how to do, can u help me carlj.

  8. Malli SG

    Thanks a bunch buddy!
    I have been struggling to add a row in the grid for the last 10 days.
    you made my day!

    you are awesome!

    thanks once again:)!

  9. Txomin

    I’ve got a problem with the paging footer too, with this code the pager’s location is the 1st cell with colspan =1 , instead of being in the center of the table.
    Can anybody help me with it?

  10. Carl J

    Hey Txomin,

    Do you have the pager inside of the gridview’s footer? Can you send me your code, and a description of what you’re trying to do, to

  11. JeffD

    This worked perfectly for me from a presentation perspective. I think I have an issue though when submitting. I have a Continue button where I want to loop through the rows in the table to get out entered data (some are template columns with text boxes). The count of the rows is thrown off because it doesn’t include the added category rows, just the number of actual records. Is there a workaround for this? For example:

    for (int i = 0; i < gvTests.Rows.Count; i++)
    if (gvTests.Rows[i].RowType == DataControlRowType.DataRow)
    TextBox txtQty = (TextBox)gvTests.Rows[i].FindControl("txtQuantity");

  12. carlj Post author

    Hey Jeff, it’s been a while since I did this. Let me try to look at it tonight, and I’ll get back to you. I know that when I originally did this (before posting), I had to go through each row.

    What if you did something like the following:

    for (int i = 0; i < gvTests.Rows.Count; i++) { if(gvTests.Rows[i].RowType == DataControlRowType.DataRow) { TextBox txtQty = (TextBox)gvTests.Rows[i].FindControl(”txtQuantity”); if(txtQty != null) { // Do something with txtQty } } } The only difference, is that you check to see if the txtQty is null or not. When you get to a Sub Header row, the Textbox doesn't exist, so it'll be null.

  13. JeffD

    I’ve been playing around a bit more, but still haven’t had any luck. I put two labels on the form and update them with gridview.Rows.Count and tbl.Rows.Count in the RowDataBound event just for testing purposes. The gridview reports 43 rows, and the table 53 when the page is fully loaded (even though I only see 52 rows even in page source). In the button click event I check the counts right away. The gridview count is still 43, but the table count drops to just 45. This is where it’s “broken” because I can’t reference all of the rows.

    If I just let the postback run through, the grid redraws without the new rows (which I guess makes sense because they were drawn after the databind which is only done if not a postback in the page load). Where the new rows were originally added, there is no data–that is, the data remained in the original row positions, but some of the data I entered in fields moved up. Kinda messy, and I’m not sure how to work around this yet.

  14. Velmurugan

    your coding is pretty good and understandable… but i have problem in paging where the paging displays in first cell. moreover i added one link button in gridview column which will redirect to another page.. for the first link its not working and i cant get the id.. help me here is the coding…

    <asp:HiddenField ID=”Hidparentid” runat=”server” Value=” />

    <asp:LinkButton ID=”LBEdit” Text=” runat=”server” CommandArgument=” OnCommand=”Btnedit_Command” >

    <a id=”A1″ href=”javascript:fnEdit(”);” runat=”server”> <asp:Label runat=”server” ID=”LblName” Text=”>

    <asp:Label ID=”Lblaccount” text=” runat=”server”>

     <asp:Label ID=”lblJob” runat=”server” Text=”>

    <asp:TextBox ID=”hidAmount” runat=”server” Text=” />
    <asp:HiddenField ID=”hidAccType” runat=”server” Value=” />
    <asp:TextBox ID=”hidmemo” runat=”server” Text=”>

    in .cs file

    protected void Btnedit_Command(object sender, CommandEventArgs e)
    String IdStr = System.Convert.ToString(e.CommandArgument);
    Response.Cookies[“purchaseid”].Value = IdStr;

  15. Carl J Post author

    @Velmurugan: I’m going out on a limb here, and kind of guessing, since I don’t have the rest of your code.

    In Btnedit_Command(), you’re saving the value of e.CommandArgument into IdStr, however, in your aspx page, you’re not setting the value of the CommandArgument property, unless that’s being done in the DataBound.

  16. Carl J Post author

    @JeffD: Have you had any luck? A few weeks ago I was playing around with the GridView, and had a similar problem. The last few weeks have been super busy with Xmas and all, so I haven’t had a chance to look into it some more.

  17. Velmurugan

    thank u sir… i rectified the error.. thanks for ur coding

  18. Carl J Post author

    @Velmurugan: What did you do to fix it? Was the problem because of the empty CommandArgument?

  19. Ron Liu

    This is a very clever way to add sub-headers to the gridview. However, I ran into a problem. When I click on some button which causing a postback. On postback, it seems the GridView gets rebind to the ViewState without calling calling the RowDataBound event. As a result, the subheaders lost on postback. Is there any fix to this problem?



  20. jax olofsson

    Hi Carl .Thanks for the solution. Im doing training and inspection forms so that code u provided was a big help.. i just made one change,, instead of referring to the gridview by name u reffrreed it using the sender object.. i rewrote in VB so here

    Protected Sub GridView1_DataBound(ByVal sender As Object, ByVal e As GridViewRowEventArgs)
    ‘ this will add a row header every time the itemgroup changes!
    If (e.Row.RowType = DataControlRowType.DataRow) Then
    Dim drv As DataRowView = CType(e.Row.DataItem, DataRowView)
    If (tmpItemGroup drv(“Itemgroup”).ToString) Then
    tmpItemGroup = drv(“ItemGroup”)
    Dim tbl As Table = CType(e.Row.Parent, Table)
    Dim row As GridViewRow = New GridViewRow(-1, -1, DataControlRowType.DataRow, DataControlRowState.Normal)
    Dim cell As TableCell = New TableCell()
    cell.ColumnSpan = sender.Columns.Count
    cell.Width = Unit.Percentage(100)
    cell.Style.Add(“font-weight”, “bold”)
    Dim span As HtmlGenericControl = New HtmlGenericControl(“span”)
    span.InnerHtml = tmpItemGroup
    tbl.Rows.AddAt(tbl.Rows.Count – 1, row)
    End If
    End If
    End Sub

  21. Jo Kvalvaag

    Hello! Thank you for this post. I have used a similar technique to add row spans on cells where data is repeated in a user control I’ve made. However, this control will not render the added sub-header rows.

    I have a bit of a complicated structure with a user control containing the grid being added to a ajaxToolkit TabContainer in an other user control. And due to a bug (I believe) in the TabContainer control, I have to databind the grid during the init event of the tabcontainer, and not during the pageload event. I think this would be the reason that the rows are never rendered.

    Databinding the gridview on pageload will not work with the tab control. You wouldn’t have a workaround to suggest for this, would you?

  22. Phil Coons

    Did anyone find a solution to the rebind problem after postback described by JeffD above? I am having that exact problem and I have spent several days trying to figure out a way around this. Any body have any ideas, Help!!

  23. Chris Spahn

    Carl, Phil, JeffD, anyone: Has anyone solved the rebind problem?? I need a fix ASAP. I’ve been at this for 12 hours straight. PLEASE let me know if you know of a work-around for this.


  24. Anitha

    I have used the above code and facing some problem in data retrieval.I am getting wrong values from the rows.Could you please send me the full code with retrieval .

  25. Sathieshkumar.R

    hi i am using the above code, working well. I am using LinkButton in the gridview through this i will popup a window. My problem is the links in the bottom n number is not working. where n is the number of sub headings in the gridview. Please help me to solve this.

    Thanks in advance.

    Sathieshkumar .R

  26. Miroslav

    I found the solution for pager problem, and somebody else can explain why is it happening (I don’t know). Anyway instead of
    GridViewRow row = new GridViewRow(-1, -1, DataControlRowType.DataRow, DataControlRowState.Normal);
    just define as
    GridViewRow row = new GridViewRow(-1, -1, DataControlRowType.Header, DataControlRowState.Normal);
    and that should do the trick

  27. Hubert

    The solution for the rebind problem after postback, and for the loss of the e.CommandArgument content in the LinkButton, is : set EnableViewState=”False” for the GridView. It worked for me.

  28. pankaj kumar

    hello sir ur code is very good 4 filling gridview bt i hv textboxes in gv 2 insert data in database bt it is not retreiving correct rows from gridview plz tell me way to solve this problem
    itz urgent
    reply will b highly appreciated

  29. Justin

    I had the same issue as JeffD. The problem is, the GridView count stays based on the number of DataSource rows. So, when looping back through on a postback, the rows in the HTML table of the GridView are out-of-sync with the DataItems.

    Does anyone have a fix for this?

    Carl, your solution doesn’t seem to work because the GridView count is still off.

  30. Carl Saiyed

    I had the same problem above as Jeff and Justin. I was able to solve it today and will post the code needed shortly.

  31. Carl Saiyed

    As promised:

    //A database record has been created for each Category with a blank Item and Intials. It has the category.
    protected void GridView1_RowDataBound(object sender, GridViewRowEventArgs e)
    //Ensure we’re on a DataRow
    if (e.Row.RowType == DataControlRowType.DataRow)
    //Get the value of the current row
    TextBox Item = (TextBox)e.Row.FindControl(“txtItem”);
    TextBox Initials = (TextBox)e.Row.FindControl(“txtInitials”);

    //If this row does not have an item or text, it must be a subheader row
    if (Item.Text == “” && Initials.Text==””)
    //Hide the (blank) textboxes currently in this row.
    Item.Visible = false;
    Initials.Visible = false;

    //Create a new cell to hold the data to be inserted
    TableCell cell = new TableCell();

    //Span the row across all columns in Gridview
    cell.ColumnSpan = this.GridView1.Columns.Count;

    //Full width cell
    cell.Width = Unit.Percentage(100);
    //Style attributes
    cell.Style.Add(“font-weight”, “bold”);
    cell.Style.Add(“background-color”, “#819D2A”);
    cell.Style.Add(“Color”, “black”);
    //cell.Style.Add(“text-align”, “left”);

    //Create a new HTML control to span the control
    HtmlGenericControl span = new HtmlGenericControl(“span”);

    //Add the text from the category textbox to the inner HTML property of the control
    span.InnerHtml = ((DataRowView)e.Row.DataItem)[“Category”].ToString();

    //Add this control to the cell

    //Add this cell to the current row
    e.Row.Cells.AddAt(0, cell);

    Basically, the problem is when you iterate through your gridview after adding subheaders, you increase the rows to iterate through, but the count property is not increased, so you are not able to iterate through all your rows.

    The best solution I could find was to add another database record that had the subheader I wanted (this was easy because all my records include the category, which is the subheader) so I added a new record for each category with null for the item and initials. If the initials and Item are null, we hide the textboxes which display the the data and instead write our span control.

    Database record example:
    RecordID Category Item Initials

  32. Stew

    What was the solution Carl? I’ve been racking my brain over this.

  33. Simflex

    Greetings. Fanstac coding techniques.

    I don’t know if anyone is still responding to this post; I certainly hope so. I tried using this code to edit clien record with 7 rows.

    The first row is edited and updated successfully.

    Subsequent rows don’t update.

    Does anyone know what has to change in the code for this to happen?

    Without this added code, the edit and update work pretty good.

    Hoping to hear from someone.

    Thanks alot in advance.

  34. Pierre Lefrancois

    Hi Carl, thanks for your code, very useful for me. How to add the number of categories on each respective line. EX:
    FastFood = 4, Italian = 3, etc.
    Thank you for taking the time to answer me, it would be greatly useful for me.

  35. Vibhu Banga

    Thank you so much for the help!!! Its a great article.

  36. Rameez

    Hi ,
    I had the same problem Carl can you share the solution of Gridview Rows Coount issue on postback

  37. K Snyder

    Get rid of the !IsPostBack and the indexing works perfectly, even when row editing with template columns.

    protected void Page_Load(object sender, EventArgs e)
    //if (!Page.IsPostBack)
    gdvRestaurants.DataSource = GetRestaurants();

  38. jam

    Thanks you !! you’re the best, thanks for sharing.
    Hello from France!

  39. chanel

    Hi Carl,

    I have the pager inside of the gridview’s footer. How do I make the footer in the center of table?


  40. Kamna

    hi everyone
    i hv little bit diff prblm i have to design invoice for my application in which i have heading like accomodation charges in front of which i want the total amount then i hv to show the list of hotels used after that i need a subheading of transporttaion charges in front of which i required the amount al these values are coming from database bt i dnt knw how to design this structure as like above example i dnt have category like field in my database can anybody help me in this thanks a lot in advance for ur kind help

  41. Carl J Post author

    Hi Kamna,

    From your message, I’m going to assume that you want the layout to look something like this (hopefully the formatting shows up):

    accomodation charges $xxxxx.xx
    – Hotel 1 $xxx.xx
    – Hotel 2 $xxx.xx
    – Hotel 3 $xxx.xx

    transporttaion charges $xxxxxx.xx
    – Charge 1 $xxx.xx
    – Charge 2 $xxx.xx
    – Charge 3 $xxx.xx

    If so, I’d suggest looking into maybe using nested repeaters.

    First repeater calls the DB to get “Accommodation Charges” and “Transportation Charges”, and whatever other groupings. It also returns the an Id, Total charges, and maybe an Sort column. Each time the first repeater is databounded, you set the data source of the second (inner) repeater to show the charges for that section.

    This can all be done in one call to the DB. If using procs, your proc can return 2 sets of data. Set the first repeater’s datasource to be the first set of data, and then when bounded look in the second set of data for a specific Id (mentioned in the first paragraph).

  42. Carl J Post author

    By the way, you can still do it as it’s done in the post. “accommodation charges” and “transportation charges” are the categories/groups 🙂

  43. Neha Veny

    You are awesome! I was struggling with my requirement and this post solved all my problems.

  44. David

    I needed something like this and after searching for ever I finally have a clean sub header. Great work.

  45. Bond

    Great work. Please how do I add sub totals at the bottom of each category?

  46. sowlov

    thank Carl J.
    but I have a prob:
    – my datasource’s 5 record -> I created 3 groups -> gridview’s 8 rows:
    row1: 1 group row
    row2,3,4: 3 rows (3 records)
    row5: 1 group row
    row6: 1 row (1 record)
    row7: 1 group row
    row8: 1 row (1 record)
    – when I click in LinkButton at row: 2,3,4 -> great work.
    – else at row: 6,8 -> NOT WORKING… why? <= I known problem about dataitemindex in datasource, but how i fix it?

    ps: my English's bad…

  47. ateek

    the same code used with is throwing error as Viewstate is not loaded correctly, kindly help on this.

  48. Lorna S

    Thank you for this code. It has helped me find an easy way to add sub-headers but it doesn’t add a sub-header after the last row in my GridView. Could you please tell me how to do this?

  49. Christian Tapia


    Do you know why my cell text shows empty (span.InnerHtml ), even if obviously I set a text?

    TableCell cell = new TableCell();

    // Span the row across all of the columns in the Gridview
    cell.ColumnSpan = this.gridBulletin.Columns.Count;

    cell.Width = Unit.Percentage(100);
    cell.Style.Add(“font-weight”, “bold”);
    cell.Style.Add(“background-color”, “#c0c0c0”);
    cell.Style.Add(“color”, “white”);

    HtmlGenericControl span = new HtmlGenericControl(“span”);
    span.InnerHtml = “Test”; //tmpCategoryName;


    Thanks in advance!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.