Real World GridView: Two Headed & Grouping GridViews


By now you may have figured out when I say “soon” I mean relatively soon, and by “relatively” I meant relative to the rise and fall of empires.  I am finally posting part 2 of the grid view articles.  Also, I have posted the source code on Got Dot Net for both part 1 and part 2.


If you missed part 1, you can read it here:
http://blogs.msdn.com/mattdotson/articles/490868.aspx
You can find the source code here:
http://www.codeplex.com/ASPNetRealWorldContr


I got some good feedback from people about part 1, and I wanted to address some questions in part 2.  In this article, we will focus on manipulating the GridView to tweak its appearance.  I have two samples, “TwoHeadedGridView” and “GroupingGridView”.  Several people posted comments about trying to manipulate the GridView to have an extra header row or something like that, so in this example, we will create an extra header row.  In the GroupingGridView, we investigate grouping cells to make commonality in data more apparent.


TwoHeadedGridView


Often you need more than one header on a table, and fortunately this is fairly easy to implement.  We could conceptually do this from the page, but we’ll make this reusable.  Here is what our TwoHeadedGridView looks like:



We allow the page to set the text by exposing a HeaderText property, and we will add the additional row after data binding.  Adding the row after data binding ensures that it does not get wiped out by data binding.  The only real trick is getting access to the GridView’s inner table.


        protected Table InnerTable
        {
            get
            {
                if (this.HasControls())
                {
                    return (Table)this.Controls[0];
                }


                return null;
            }
        }


The inner table is just an ASP.NET Table control, and is the GridView’s only child control.


Now that we know how to get at the inner table, it makes it easy to manipulate it.  Here we just add a row to the table:


GridViewRow row = new GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal);
TableCell th = new TableHeaderCell();


th.HorizontalAlign = HorizontalAlign.Center;
th.ColumnSpan = this.Columns.Count;
th.BackColor = Color.SteelBlue;
th.ForeColor = Color.White;
th.Font.Bold = true;
th.Text = this.HeaderText;
row.Cells.Add(th);


this.InnerTable.Rows.AddAt(0, row);


Now that you know how to modify the inner table, you can do what ever you want to it!!



GroupingGridView


Grouping data together makes it easy to see how data is related.  Taking the same data from the previous example, we have created a GridView to see how the authors are clustered by city and state.



You can easily see that there are many authors from California, and specifically Oakland.  This is starting to look like multi-dimensional data from a cube!!  Maybe someday I will (or someone else will) expand this concept to include drill-down and expandable/collapsible buttons.


All we ask from the page is that it set the GroupingDepth property, which tells us how many columns to try to group.  In the screen shot, it is set to 2.  If I had set it to only 1, then Oakland would not have spanned multiple rows.


The real trick to spanning cells is marking cells “not visible” instead of deleting them.  Both setting a cell’s Visible property to false and deleting the cell will cause the cell not to render any HTML, but only the Visible property is stored in viewstate so our changes are persisted across postbacks.  NOTE: It is important to realize that the server side Visible property is different than the CSS display and visibility attributes.


An important design feature is that the second, third, etc columns contain groups that are subsets of the column on their left.  For instance, we want to make sure that if Kansas also had a city named Rockville that it would not be grouped with Maryland’s Rockville.  We do this though recursion because it seemed like the most efficient implementation.  Here you can see our recursive function:


        private void SpanCellsRecursive(int columnIndex, int startRowIndex, int endRowIndex)
        {
            if (columnIndex >= this.GroupingDepth || columnIndex >= this.Columns.Count )
                return;


            TableCell groupStartCell = null;
            int groupStartRowIndex = startRowIndex;


            for (int i = startRowIndex; i < endRowIndex; i++)
            {
                TableCell currentCell = this.Rows[i].Cells[columnIndex];


                bool isNewGroup = (null == groupStartCell) || (0 != String.CompareOrdinal(currentCell.Text, groupStartCell.Text));
               
                if (isNewGroup)
                {
                    if (null != groupStartCell)
                    {
                        SpanCellsRecursive(columnIndex + 1, groupStartRowIndex, i);
                    }


                    groupStartCell = currentCell;
                    groupStartCell.RowSpan = 1;
                    groupStartRowIndex = i;
                }
                else
                {
                    currentCell.Visible = false;
                    groupStartCell.RowSpan += 1;
                }
            }


            SpanCellsRecursive(columnIndex + 1, groupStartRowIndex, endRowIndex);
        }


And that’s all for today.  Be sure to look at the full source code @ http://www.codeplex.com/ASPNetRealWorldContr

Comments (34)

  1. GridView is a great control, and out of the box it does a lot of wonderful stuff, but I haven’t worked on a project yet where it did everything I needed.  &quot;Real World GridView&quot; is an attempt to share some of the common customizations I end up

  2. Many of us are familiar with frozen cells in Excel, but it is typically quite difficult to implement something like that HTML.  In this “Real World GridViews”, we investigate adding this functionality to GridView to make frozen headers easy to reuse across

  3. Sebastian says:

    Thanks a lot for these tips! They do seem basic and obvious but I’ve been googling for half a day without finding anyone mentioning the innertable…

    Thanks!

    /Sebastian

  4. liptid1 says:

    Where do I download the full source code?

  5. Breck says:

    Where’s the source–or the binary?  The FrozenGridView type is referenced–but where is it?

    Tantalizing post–but tantalize is about all it can do without more understanding of these behind-the-scenes types…

  6. Jamie Laing says:

    Well, you’ve solved a problem that’s been bugging me for quite a long time now.  Thanks!

    A little gotcha though… If you add a row to your grid in this manner, ViewState has no idea what you’ve done, so you can’t access CommandArgument for a button in your grid without doing some work.  Here is a way you can fudge this…

    LinkButton oButton = (LinkButton)sender;

           GridViewRow oViewRow = (GridViewRow)oButton.Parent.Parent;

           int intRealIndex = oViewRow.RowIndex + 1;

           LinkButton realButton = (LinkButton)GridView1.Rows[intRealIndex].FindControl("LinkButton1");

           String strCampaignID = realButton.CommandArgument.ToString();

           this.Session.Contents["CampaignID"] = strCampaignID;

           Response.Redirect("sa_outlets.aspx");

  7. Glenn Maples says:

    I don’t want to whine, but relying on gotdotnet to distribute your code is a terrible idea.  While the site is not down most of the time, I have found it difficut/impowwible to add an account and downlod your code.

    Would it be that difficult to include the zip on this page?

    Thanks

  8. Josh Gough says:

    This is nice stuff! Thanks for sharing…

    One thing I want to do is _replicate_ the existing header row lower down on the page after a logically separate section.

    I tried this:

           protected override void OnPreRender(EventArgs e)

           {

               // Apply the repeated header if needed

               if (-1 != _duplicateHeaderRowAtIndex)

               {

                   GridViewRow header = HeaderRow;

                   header.Parent.Controls.AddAt(_duplicateHeaderRowAtIndex, header);

               }

               base.OnPreRender(e);

           }

    However, that moves the header from the top to the duplicate index instead of copying it.

    I guess I may have to manually clone the elements….

  9. Thanks very much Matt. This code helped in what I wanted to do.

    A note to other guys though, something I found out. If you don’t want to just add the extra header without going through the pain of inheriting the gridview, use this line to add the newly created row (thus access innertable).

    Instead of Matt’s line

    this.InnerTable.Rows.AddAt(0, row);

    use:

    CType(GridView1.Controls(0), Table).Rows.AddAt(0, row) – VB

    Or C# – (Table)GridView1.Controls[0].Rows.AddAt(0,row)

  10. Alex says:

    This is great stuff. Thank you.

    I would like to build a 2 row header manually so I can use rowspan on some of the columns. The problem I’m having is adding the second row. I have tried playing with the new GridViewRow Parameters such as putting ‘1’ for the rowIndex parameter and have also tried adding it to the AddAt(1, row).

    This is a quick sample idea:

           private void CreateSecondHeader()

           {

               

               // First Row

               GridViewRow row0 = new GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal);

               TableCell th0a = new TableHeaderCell();

               TableCell th1a = new TableHeaderCell();

               TableCell th2a = new TableHeaderCell();

               th0a.ColumnSpan = 1;

               th1a.ColumnSpan = 2;

               th2a.ColumnSpan = 1;

               th0a.RowSpan = 2;

               row.Cells.Add(th0a);

               row.Cells.Add(th1a);

               row.Cells.Add(th2a);

               //Second Row

               GridViewRow row1 = new GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal);

               //TableCell th0b = new TableHeaderCell();

               TableCell th1b = new TableHeaderCell();

               TableCell th2b = new TableHeaderCell();

               TableCell th3b = new TableHeaderCell();

               //th0b.ColumnSpan = 1;

               th1b.ColumnSpan = 1;

               th2b.ColumnSpan = 1;

               th3b.ColumnSpan = 1;

               row1.Cells.Add(th0b);

               row1.Cells.Add(th1b);

               row1.Cells.Add(th2b);

               this.InnerTable.Rows.AddAt(0, row0);

               this.InnerTable.Rows.AddAt(1, row1);

           }

       }

    Basically want to do a combination of colspan and rowspan.

    Any ideas??

    For anyone that’s interested, you can still use bulit in sorting by adding a LinkButton to header and setting CommandName to ‘sort’ and CommandArgument to the data column as you would for sort expression.

  11. Alex says:

    Sorry, one other thing:

    Why do you use a -1 for the dataItemIndex on a new GridViewRow?

    Thanks

  12. BitShift says:

    For the grouping example, where do we call SpanCellsRecursive from – RowDataBound or prerender or other ?

  13. Jon Ege Ronnenberg says:

    Can I use the TwoHeadedGridView to add an extra row to the every row in the GridView? I have a GridView set up to an objectSource and need to write some extra description underneath every row the GridView creates.

  14. raju k says:

    If the table is blank, the code is not showing blank row (GridView.HasControls is false).

    Is there any workaround?

  15. Boerge says:

    I’m having trouble getting the values from a DropDownList when inserting a new row.

    Could anyone give me a hint on how to do this?

    I’ve tried to get the last row of the gridview and using findControls to extract the values. But it is almost as if the last row does not exist.

  16. bsraj says:

    Hi iwant to round off the values in gridview how to do it Pls help

  17. Janardhan says:

    Hi,

    Can i have the summary (subtotal) for each group.

    Please can you help me in generating subtotal for each group

  18. Nelle says:

    It does not work with the TemplateItems in the gridview …

    I had to move the CreateSecondHeader() method to OnPreRenderEvent in order to get the second header ..

    It seems that the TemplateItem’s Header somehow overwrites the second header …

  19. Aaron says:

    I’m having problems using the "two-headed" trick with a templated grid view.  Something in the lifecycle of the control always wants to treat the additional header as a datarow.  It binds an empty template row to the header everytime.

    I think this has something to do with view state, but it doesn’t seem to matter where I put the extra header, it initially renders correctly, but does not on the next roundtrip.

  20. Aaron Schurg says:

    I tried adding the dynamic header to the gridview in a templated data bound scenario, the only place I was able to pull it off was in the OnRowCreatedEvent method for the control.  All other places (before and after viewstate applied, before and after row databinding) were causing the header to be treated as a datarow during postback.

    Reading the notes on the page life cycle for the gridview, this made sense because it turns out that when you add databound templates, things don’t occur exactly in the "normal" order.  The databound templates are trying to play catchup and things can get hinky.

    Here is my code:

    Protected Sub gvFacShifts_OnRowCreated(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) Handles gvFacShifts.RowCreated

           If e.Row.RowType = DataControlRowType.Header Then

               Dim row As GridViewRow = New GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal)

               Dim th As New TableHeaderCell()

               th.HorizontalAlign = HorizontalAlign.Left

               th.ColumnSpan = 8

               th.BackColor = Me.gvFacShifts.HeaderRow.BackColor

               th.ForeColor = Me.gvFacShifts.HeaderRow.ForeColor

               th.Font.Bold = True

               th.Text = "Availability By Shift:"

               row.Controls.Add(th)

               CType(Me.gvFacShifts.Controls(0), Table).Rows.AddAt(0, row)

           End If

       End Sub

    Anyways, thanks for the post on two headers!

  21. Cris says:

    The 2 headed gridview code works great but I have a grid where I’m hiding one of the cells and when I try to edit a row, the hidden cell value that I pick up, is the previous records value.  Any ideas on how to get around this?

  22. Svel says:

    Has anyone found a solution to this issue?  I am also trying to add a second "sub-header" row to my gridview using the suggested method, and it works perfectly the first time round.  However when there is a postback, the "sub-header" row is treated as an emtpy datarow and the last "real" datarow is lost.  I am working with row templates.

    If anyone has found a solution, PLEASE post it here !   Thanks…

  23. kevin says:

    Anyone know where I can get the full source code?   The link doesn’t work in codeplex

  24. Andreas Warberg says:

    The source code is still not available…

  25. Santosh says:

    Does anybody know how to group the rows using a subheader. I want to insert a new row before every group of related rows. For example if the rows are grouped according to state, I want the state name to be the header for that group of data.

  26. I am having a GridVew in which I am in need to bind a subheader row depending on some condition. Please help me if you have any idea regarding this.

    Ex:  from a datasource I will be getting all the names of male and female. In the grid depending on male or female I need to bind as below

    // if Group = Male and Group = Female

    the GridView is like

    GoupsTable                 // Header name

    GroupMale                 // sub header name

    1. Steve                              

    2. John

    3. Peter

    GroupFemale             // Sub  header name

    1. Silvia

    2. Alice

    3. Donna

    Waiting in anticipation

    Thanks in advance

    Amarnath. V. Meti

  27. kludgemaster says:

    Has anyone managed to create a bulk edit version of the grouping grid view?

  28. mantell says:

    Problem was that first row became empty and last row was lost after postback.

    I struggled with this for 5 hours straight, and finally I came up with a solution that worked for me.

    Try using the Pager row as the first header row. First enable Allow Paging, then replace the autogenerated pager row with anything you like. You always have to do this after Bind and in every postback event you handle, for example update, edit etc.

    Now my gridview works perfectly!

  29. Paul Vella says:

    A workaround for the missing datarow is to set EnableViewstate=false for the gridview.

    I’d also love to hear how we can add a row and have it render correctly on postback (ie. from viewstate).

  30. Ed Graham says:

    Great stuff, Matt; people like you make my initiation into the world of web development that much more comfortable!  Keep up the excellent work.

    Ed

    P.S.  I loved the gag about the rise and fall of empires at the beginning, too!

  31. Santosh says:

    Great control but i got into a problem. I have different color for the alternate rows. Now if an even number of rows are grouped together, the next row will have the same color as the previous one. Any workaround?

  32. JoeSwan says:

    Just to update what Jamie Lang said about the events getting shifted out of place, there is actually a work around which also makes the table binding work a lot more smoothly too.

    It does require that you extend GridView with a new class, but that’s not too hard to do. Basically override the method CreateChildTable() to add your row before .Net adds the rest of them. This keeps the viewstate in sync with your rows and lets you add as many header rows as you want.

    I have added two properties which allow you to define a header CSS class and text for the header. If the header text is null we skip adding a row.

    Full code is here:

    using System.Web.UI.WebControls;

    namespace MyNamespace.UserControls

    {

    public class GridView : System.Web.UI.WebControls.GridView

    {

           public string HeaderText

           {

               get { return (string) ViewState["HeaderText"]; }

               set { ViewState["HeaderText"] = value; }

           }

           public string HeaderCssClass

           {

               get { return (string)ViewState["HeaderCssClass"]; }

               set { ViewState["HeaderCssClass"] = value; }

           }

    public GridView()

    {

    UseAccessibleHeader = true;

    }

           protected override Table CreateChildTable()

           {

               Table table = base.CreateChildTable();

               if (!string.IsNullOrEmpty(HeaderText))

               {

                   var row = new GridViewRow(0, -1, DataControlRowType.Header, DataControlRowState.Normal);

                   var th = new TableHeaderCell { ColumnSpan = Columns.Count, Text = HeaderText };

                   if (!string.IsNullOrEmpty(HeaderCssClass))

                       th.CssClass = HeaderCssClass;

                   row.Cells.Add(th);

                   row.TableSection = TableRowSection.TableHeader;

                   table.Rows.Add( row);

               }

               return table;

           }

       protected override void OnDataBound(System.EventArgs e)

    {

    base.OnDataBound(e);

               if (HeaderRow != null)

                   HeaderRow.TableSection = TableRowSection.TableHeader;

               if (FooterRow != null)

                   FooterRow.TableSection = TableRowSection.TableFooter;

    }

           protected Table InnerTable

           {

               get

               {

                if (HasControls())

                return (Table) Controls[0];

                   return null;

               }

           }

    }

    }

  33. Andy says:

    Thanks JoeSwan. Your solution works! But with slight modification. I did not extend the GridView to a new class, I just copied the override method CreateChildTable() then modified the contents.

    thanks!

  34. Sri says:

    Is there a way to access the new header row in Render method of the control. I want to do some manipulation in Render on this new header row.