The DataGrid
control has some built-in support for item selection, but lacks an important
capability - letting users select more than one item at a time. The DataGrid's
extreme versatility, however, allows us as developers to make up for this
deficiency pretty easily.
The
single selection is perfect when you want to drill down into the information
associated with a given item, but it's inadequate when you need to select a
bunch of items and process them in a batch on the server. Want an example?
Consider the user interface of the Hotmail.com inbox page (see Figure 1).
Figure 1: Hotmail allows users to select
multiple messages from the inbox.
The
Hotmail.com inbox allows users to select multiple messages and apply a given
action (e.g. move to junk folder, delete, etc.) to all of them. But how can we
implement this in ASP.NET?
Design a Multi-selection DataGrid
Start by
creating a standard DataGrid control, then add a templated column to
provide each row with a checkbox. Next, write all the necessary event handlers
to retrieve the checked items when the page posts back.
Is that
all? It depends on how much of a perfectionist you are. A templated column is
sufficient to post references to a handful of items to the server, but some
client-side script code would make it more attractive and professional-looking.
For example, you can inject some JavaScript code to let users select and
deselect all items with a single click, just like Hotmail does. Let's start
with the checkbox template column. Figure 2 shows the DataGrid at design
time.
Figure 2: Add a template column to the DataGrid
and make it show a checkbox alongside the data cells.
The DataGrid
is configured to show two columns of data (Customer and Country) plus an
additional column to let users select records. The template column is defined
as follows:
BackColor="PowderBlue">
The
element contains two templates in addition to the
definition of the header's alignment and background color. The
block defines the content of the header cell for the
templated column. The section determines the content that
individual cells in the column will show. You can use either the
or control to render the
checkbox on the final page served to the user. I've used the HTML server
control (the tag) in the body of the grid for a reason that I'll
explain later, and that's related to the client-side script code. So if you
aren't willing to add any JavaScript functions, Web and HTML checkbox controls
work just as well.
Remember
that the ID assigned to the tag is not going to be replicated for
each item in the data source. The actual ID of each checkbox is a unique string
obtained by combining the ID with the ID of all the naming containers of the
checkbox. In this case, they are the DataGridItem object (the
element) and the DataGrid itself (the
element).
Now bind
some data from the Northwind Customers table to the DataGrid and run the
page. The result should resemble Figure 3.
Figure 3: Click on the checkbox and select
the entire row.
Detect Checked Items
Unless
you toggle on the AutoPostBack property on the CheckBox control,
clicking on the checkbox has no repercussions on the server. The page won't
post back, and checking and unchecking items remain purely client-side
operations. For the page to post back, you must click on the Process data button. Once back in its native server environment, all controls
in the page are rebuilt, including the DataGrid. Thanks to the
information stored in the viewstate, the DataGrid retains all of its
checked items. As a result, you can get a reference to all selected items
simply by looping on all DataGrid items, extracting the corresponding CheckBox
control, and verifying the value of the Checked property (see Figure 4).
For Each item As DataGridItem In theGrid.Items
Dim itemType As ListItemType
itemType = item.ItemType
If itemType = ListItemType.Item Or _
itemType = ListItemType.AlternatingItem
Then
Dim cb As HtmlInputCheckBox
Dim ctl As Control =
item.FindControl("YourItem")
cb = CType(ctl, HtmlInputCheckBox)
If Not (cb Is Nothing) Then
If cb.Checked Then
' TODO:: process item
End If
End If
End If
Next
Figure
4: Get a reference
to all selected items simply by looping on all DataGrid items,
extracting the corresponding CheckBox control, and verifying the value
of the Checked property.
The DataGrid's
Items collection contains a reference to all items created to render the
grid, including header, footer, and pager, as well as items and alternating
items. The ItemType property on the DataGridItem object being
processed returns the type of the item. You must consider only Items and
AlternatingItems. To locate the checkbox, use the FindControl
method and pass in the relative ID of the control. By design, FindControl
operates in the realm of the current container. This ensures that the code in
Figure 4 makes its search only in the context of the current DataGrid
item. However, FindControl isn't as fast as a direct, position-based
search. Although slightly faster, this code is less flexible, and, for example,
doesn't work if you give the template column a fixed width:
Dim ctl As Control = item.Cells(0).Controls(0)
In this
case, in fact, the DataGrid renders the templated cells as follows:
As a
result, CheckBox isn't at position 0 as one would expect. The first
position in the Controls collection is reserved for a LiteralControl
object rendering the white space. The bottom line is that using an index-based
approach makes your code more sensitive to changes to the page layout.
As you
can see in Figure 3, the selected items have a different background color and
are drawn in boldface. These graphical changes are applied on the client using
JavaScript code.
Simply
clicking on a checkbox displayed in a browser doesn't necessarily cause the
page to post back. This happens only if you implement the checkbox using the
element and set the Boolean AutoPostBack property
to True. Note that the AutoPostBack property isn't
defined on the HtmlInputCheckBox control (the class behind the tag).
Identify the Checked Items
So you
now know a way to check multiple rows of a DataGrid and send them to the
server in a single post. However, a DataGrid item references a
particular data object, which is - after all - what you're really interested
in. So how can you establish a reliable link between a checkbox and an item in
the bound data source? The DataList and the DataGrid controls
have a slick property named DataKeyField. This property takes the name
of a column in the data source, typically the primary key, or another column
with unique values.
In
ASP.NET 1.x, the DataKeyField property must be a string, and
subsequently it can address only a single column. In ASP.NET 2.0, the DataKeyField
property is extended to support an array of columns.
In the
previous example, I used the Customers table in the SQL Server's Northwind
database. So the most obvious setting for DataKeyField is:
DataKeyField="customerid">
...
The
final effect of this code is that the DataGrid stores the value of the
specified column for all data items currently displayed in the DataKeys
property (an array of objects). In other words, the item index can be used to
retrieve the key for each checked item:
' cb is the
CheckBox retrieved as in the snippet above.
' item is the
current DataGridItem object.
If cb.Checked
Then
Dim index As Integer = item.ItemIndex
Dim key As String = theGrid.DataKeys(index)
ProcessItem(key)
End If
The ProcessItem
routine is a placeholder used to indicate any code you may write to process the
checked item. Thanks to the DataKeyField and DataKeys properties,
ProcessItem can easily be passed a key value that uniquely identifies
the underlying data row.
Passing
in the key is particularly suited to situations where you need to drill down
into the data with a further query. If you're using cached data, you can simply
use the item index to identify the data object in the in-memory data source.
Note, however, that the ItemIndex property is a page-specific index. In
case of a multi-page DataGrid, you need an absolute index to retrieve
the corresponding data object from memory.
Modify the Style of a Checked Item
If
you're content with the core item selection feature, you're pretty much done.
Your grid is up and running and you might consider wrapping it up in an
all-encompassing control. However, adding some client-side bells and whistles
isn't that difficult, and will make the whole solution more elegant.
Let's
see how to change the background color and the font weight of a checked row.
These changes will take place entirely on the client by exploiting the DHTML
capabilities of the browser. As a result, style changes will be lost when the page
posts back and might not work on older browsers.
To
change the style of the HTML element, you must be able to detect when the user
clicks to check or uncheck the input element. This is as easy as defining the
onclick attribute for the tag:
The
checkbox is rendered as part of a templated grid column. This means that each
checkbox displays within a
tag in a table row - a
tag. To
select the entire row you must retrieve the parent
tag, and set the
background color and any other graphical attribute (see Figure 5).
Figure 5: Setting the background color and
font weight of selected rows.
The
first argument is a reference to the checkbox itself. The second argument
indicates the background color for the checked item (see Figure 3). You must
place two iterative calls to the parentElement property of the checkbox
to reach the parent table row. The HTML layout of each checkbox is as follows:
How can
you bind the above script code to each checkbox in the DataGrid? First,
register the
Figure
7: This code
selects or deselects all items according to the checkbox on the column header.
The code
in Figure 7 could be simplified if you're targeting IE 5.0 and newer. It also
works as-is with Netscape Navigator 4.x. To retrieve all checkboxes in the
table, I'm afraid there's no better way than checking all the input elements
and filtering out all those that are neither checkboxes nor children of the
grid. This latter condition is verified looking at the name attribute.
By design, in ASP.NET the ID of the contained control is prefixed with the name
of the parent. Once you've found a checkbox element, you set the checked
attribute to the value of the header's checkbox checked attribute, and
then call the same Select function defined earlier to apply graphical
changes. Figure 8 shows this feature.
Figure 8: Click on the header's checkbox and
select all the items in the grid.
Conclusion
Although
the DataGrid control lacks certain built-in capabilities, it can be
tailored to fit your needs. In this article we saw how to add the capability to
let users select more than one item at a time. This is just one way your
applications can be made to order.
The sample code accompanying
this article is available for download.