Home | About Me | Developer PFE Blog | Become a Developer PFE
Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
Sign In
I was helping a co-worker today with one of his screens and realized that it may make a useful little tutorial for all those other folks out there. This was somewhat interesting because it follows up pretty well on my previous post as I talk about saving records with a 1 to many relationship. Basically, he needed to develop a screen similar to the following:
This is actually a pretty simple page, but there are a few places that might cause a hiccup or two:
For the first item - grouping inside of a checkbox list. This actually won't work - there is no way to specify groups of checkboxes or additional properties to define checkboxes or any of that...so, the only real option is to put the checkboxes into a GridView control. Now, I've done this type of grouping many times previously and it never really felt right. After Googling the problem with him - "grouping items in a gridview" - the first result is a great series of classes posted here. The only problem with this method is that it is focused on grouping within reports. The difference in this situation is that we wanted the checkboxes to maintain their state between postbacks. If you perform a postback using the article's method, then not only will your checkboxes lose their state (kinda sorta) but the Header row for each group will disappear.
Then, I happened to recall something similar written by Matt Dotson and his use of cell-spanning to accomplish a similar feat. He has a great blog entry posted here to illustrate the concept. In-fact, he also has a great CodePlex project here that has a bunch of useful "Real World" ASP.NET webcontrols in it. One of these useful controls is a GroupingGridView. This is essentially a GridView to perform the actual Grid Grouping. By default, using Matt's sample, the GridView renders like this:
And the XHTML to output this simple page is:
<rwg:GroupingGridView ID="GroupGrid" DataSourceID="PubsDataSource" AutoGenerateColumns="False" GroupingDepth="2" DataKeyNames="au_id" runat="server"> <Columns> <asp:BoundField HeaderText="State" DataField="state" /> <asp:BoundField HeaderText="City" DataField="city" /> <asp:BoundField HeaderText="Last Name" DataField="au_lname" /> <asp:BoundField HeaderText="First Name" DataField="au_fname" /> <asp:BoundField HeaderText="Phone" DataField="phone" /> <asp:BoundField HeaderText="Address" DataField="address" /> <asp:BoundField HeaderText="Zip Code" DataField="zip" /> <asp:CheckBoxField HeaderText="Contract" DataField="contract" /> </Columns> </rwg:GroupingGridView>
Nice, clean and simple - and it gets us most of the way there. Instead of actually displaying the grouped items in the center of the cell, though, we needed the Group text to display at the top of the cell. This is a very simple change to his source code (available on the CodePlex site). In fact, it's only 1 additional line of code in his SpanCellsRecursive method (see the marked line below):
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.Cells[columnIndex]; bool isNewGroup = (null == groupStartCell) || (0 != String.CompareOrdinal(currentCell.Text, groupStartCell.Text)); currentCell.VerticalAlign = VerticalAlign.Top; 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); }
Now that gets us the Grouped items in a GridView for our pretty display. Just using that wonderful GridView gets us a page that looks like this:
With only this small amount of code in the XHTML:
<RWC:GroupingGridView runat="server" ID="GroupingGridView1" AutoGenerateColumns="False" GroupingDepth="1" ShowHeader="False"> <Columns> <asp:BoundField DataField="GroupName" ShowHeader="False" /> <asp:TemplateField> <ItemTemplate> <asp:checkbox runat="server" id="checkbox1" Text='<%# Eval("AttributeName") %>' Checked='<%# DirectCast(Eval("ParentId"),Int32) > 0 %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </RWC:GroupingGridView>
And then you just bind this grid behind the scenes:
Protected Sub Page_Load(ByVal sender As Object, _ ByVal e As System.EventArgs) _ Handles Me.Load If Not IsPostBack Then Dim attributeList As _ System.Collections.Generic.IList(Of Attribute) attributeList = Me.GetAttributes() With Me.GroupingGridView1 .DataSource = attributeList .DataBind() End With End If End Sub
Now, from a look and feel perspective - we're just about done. But what about getting the values out of the checkbox - so that we know:
The checked property is relatively simple to get out of the page. In the Submit button's event, you loop through the rows in the GridView and extract out the instance of the Checkbox for each row and then check the "Checked" property of the checkbox to determine if the checkbox is selected:
For Each row As GridViewRow In Me.gvGrouping.Rows If row.RowType = DataControlRowType.DataRow Then Dim obj As CheckBox = _ TryCast(row.FindControl("chkbxPermissions"), _ CheckBox) If obj IsNot Nothing Then Response.Write(String.Format("Row {0}: checked value is {1} <br>", _ row.RowIndex.ToString, obj.Checked.ToString)) End If End If Next
Now this will write to the page the RowIndex and a value indicating whether the checkbox was checked. This will not, though, give me the AttributeId for these checkboxes because the ASP.NET 2.0 Checkbox Control does not have actually have a "Value" property like it does in a CheckBoxList control. So, what do we do? Well, there are two solutions:
Given the two options mentioned above - there are no real differences. The actual size of the page is EXACTLY the same and the speed to access the GridView's DataKey property versus a new property on a new CheckBox control is no different. I'm choosing to go with the latter as I know of a few other situations where a control like this might come in handy.
So, since it is not already there - let's create a new control that derives from the existing CheckBox control. We'll call this new control a ValueCheckBox control and add a new property:
Namespace WebControls Public Class ValueCheckBox Inherits CheckBox Public Property CheckboxValue() As String Get Return DirectCast(ViewState("checkboxvalue"), String) End Get Set(ByVal value As String) ViewState("checkboxvalue") = value End Set End Property End Class End Namespace
Then, we add the new Register tag to the top of our page:
<%@ Register TagPrefix="sp" Namespace="WebControls" %>
And then slightly change our XHTML to include a reference to the new checkbox in our GridView:
<RWC:GroupingGridView runat="server" ID="gvGrouping" AutoGenerateColumns="False" GroupingDepth="1" ShowHeader="False"> <Columns> <asp:BoundField DataField="GroupName" ShowHeader="False" /> <asp:TemplateField> <ItemTemplate> <sp:ValueCheckBox runat="server" ID="chkbxPermissions" Text='<%# Eval("AttributeName") %>' Checked='<%# DirectCast(Eval("ParentId"),Int32) > 0 %>' CheckboxValue='<%# Eval("AttributeId") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </RWC:GroupingGridView>
Lastly, you tweak your submit button code-behind to get at the new control:
For Each row As GridViewRow In Me.gvGrouping.Rows If row.RowType = DataControlRowType.DataRow Then Dim obj As WebControls.ValueCheckBox = _ TryCast(row.FindControl("chkbxPermissions"), _ WebControls.ValueCheckBox) If obj IsNot Nothing Then Dim checked As Boolean = obj.Checked Dim checkboxValue As String = obj.CheckboxValue End If End If Next
And now you're done. My previous post discusses the best way to store these values, so I won't go into that level.
Enjoy!