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:
https://blogs.msdn.com/mattdotson/articles/490868.aspx
You can find the source code here:
https://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 @ https://www.codeplex.com/ASPNetRealWorldContr
Comments
Anonymous
March 01, 2006
The comment has been removedAnonymous
March 02, 2006
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 acrossAnonymous
March 06, 2006
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!
/SebastianAnonymous
April 08, 2006
Where do I download the full source code?Anonymous
April 12, 2006
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...Anonymous
April 25, 2006
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");Anonymous
May 21, 2006
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?
ThanksAnonymous
June 14, 2006
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....Anonymous
August 29, 2006
The comment has been removedAnonymous
August 30, 2006
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.Anonymous
August 30, 2006
Sorry, one other thing:
Why do you use a -1 for the dataItemIndex on a new GridViewRow?
ThanksAnonymous
September 22, 2006
For the grouping example, where do we call SpanCellsRecursive from - RowDataBound or prerender or other ?Anonymous
October 04, 2006
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.Anonymous
November 22, 2006
If the table is blank, the code is not showing blank row (GridView.HasControls is false). Is there any workaround?Anonymous
November 28, 2006
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.Anonymous
December 10, 2006
Hi iwant to round off the values in gridview how to do it Pls helpAnonymous
January 04, 2007
Hi, Can i have the summary (subtotal) for each group. Please can you help me in generating subtotal for each groupAnonymous
January 19, 2007
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 ...Anonymous
February 10, 2007
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.Anonymous
February 10, 2007
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!Anonymous
February 19, 2007
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?Anonymous
March 15, 2007
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...Anonymous
April 16, 2007
Anyone know where I can get the full source code? The link doesn't work in codeplexAnonymous
May 04, 2007
The source code is still not available...Anonymous
June 23, 2007
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.Anonymous
July 30, 2007
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
- Steve
- John
- Peter GroupFemale // Sub header name
- Silvia
- Alice
- Donna Waiting in anticipation Thanks in advance Amarnath. V. Meti
Anonymous
July 31, 2007
Has anyone managed to create a bulk edit version of the grouping grid view?Anonymous
November 16, 2007
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!Anonymous
December 05, 2007
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).Anonymous
April 23, 2008
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!Anonymous
July 09, 2008
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?Anonymous
December 18, 2008
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; } } } }Anonymous
April 21, 2010
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!Anonymous
October 22, 2010
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.