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:
- Grouping items in a checkbox list.
- Getting the value for each Attribute (ie: the PK of the Attribute itself)
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:
- Whether the checkbox was selected.
- What the value of the checkbox is - ie: the primary key of the Attribute object.
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:
- Use the DataKeyNames property of the GridView to specify that the AttributeId is our key for this GridView and then extract it from the row.
- Create a new CheckBox control that will include a new property to hold the Checkbox's Value.
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!