Captions, scope, and headers Attributes in the GridView Control
The final section of code in the example1.aspx page demonstrates the use of some accessibility-oriented properties of the new GridView control in ASP.NET 2.0. The UseAccessibleHeader property is also available on the Calendar, DataList and DataGrid controls, while the other two properties (AccessibleHeaderText and RowHeaderColumn) are specific to the new GridView control.
The code listing below shows the complete declaration of a GridView control, which is populated by a SqlDataSource control shown at the end of the listing (the ConnectionString property of the SqlDataSource control is set at runtime by code you saw earlier in the Page_Load event handler):
...
<asp:GridView id="MyGrid" runat="server"
DataSourceID="ds1"
DataKeyNames="ProductID"
CaptionAlign="Top"
Caption="<u>G</u>ridView Example"
AccessKey="G"
RowHeaderColumn="ProductName"
UseAccessibleHeader="True"
SummaryViewColumn="ProductName"
BorderWidth="1px"
BorderColor="#E7E7FF"
BorderStyle="None"
BackColor="White"
CellPadding="3"
TabIndex="3"
PagerSettings-Mode="Numeric"
AutoGenerateColumns="False">
<HeaderStyle ForeColor="#F7F7F7" Font-Bold="True"
BackColor="#4A3C8C" />
<RowStyle ForeColor="#4A3C8C" BackColor="#E7E7FF" />
<AlternatingRowStyle BackColor="#F7F7F7" />
<PagerStyle ForeColor="#4A3C8C"
HorizontalAlign="Right" BackColor="#E7E7FF" />
<FooterStyle ForeColor="#4A3C8C" BackColor="#B5C7DE" />
<Columns>
<asp:BoundField DataField="ProductID"
AccessibleHeaderText="Product Identifier"
HeaderText="ID"
ItemStyle-HorizontalAlign="Center">
<ItemStyle HorizontalAlign="Center"></ItemStyle>
</asp:BoundField>
<asp:BoundField DataField="ProductName"
AccessibleHeaderText="Full Product Name"
HeaderText="Product"
ItemStyle-HorizontalAlign="Left">
<ItemStyle HorizontalAlign="Left"></ItemStyle>
</asp:BoundField>
<asp:BoundField DataField="QuantityPerUnit"
AccessibleHeaderText="Quantity per Unit"
HeaderText="Packaging" />
<asp:BoundField DataField="UnitPrice"
AccessibleHeaderText="Price per Unit"
HeaderText="Price"
DataFormatString="$ {0:F2}"
ItemStyle-HorizontalAlign="Right">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundField>
<asp:BoundField DataField="UnitsInStock"
AccessibleHeaderText="Units in Stock"
HeaderText="Stock"
ItemStyle-HorizontalAlign="Right">
<ItemStyle HorizontalAlign="Right"></ItemStyle>
</asp:BoundField>
</Columns>
</asp:GridView>
<asp:SqlDataSource id="ds1" runat="server"
SelectCommand="SELECT ProductID, ProductName, QuantityPerUnit,
UnitPrice, UnitsInStock FROM Products"
FilterExpression="ProductName LIKE '@ProductName%'">
<FilterParameters>
<asp:ControlParameter Name="ProductName" ControlID="txtProduct"
PropertyName="Text" />
</FilterParameters>
</asp:SqlDataSource>
The section of the example page that this code creates is shown in Figure 3.

Figure 3 - The output generated by the GridView control in Internet Explorer
Adding a Table Caption
Most of the declaration is concerned with the mechanics of specifying the columns and the appearance of the control in a normal graphical Web browser. However, there are several sections that are aimed at improving accessibility for users of non-graphical client devices. The HTML table that the GridView control generates has a caption at the top, so that users can immediately understand what the content as a whole represents. This is aligned above the table, and uses a hotkey in the caption to allow the user to jump directly to the table:
CaptionAlign="Top"
Caption="<u>G</u>ridView Example"
AccessKey="G"
Specifying Meaningful Column Names
Displaying a table of values is easy, as you can see from the code above. There are plenty of controls in ASP.NET that can take a source rowset and display its contents. However, we often spend more time worrying about the layout and appearance of the table, and little time thinking about things like what the column heading actually mean. After all, they have to be fairly short to avoid upsetting the layout of the table - especially when it contains, for example, just columns of numbers.
In a non-visual user agent or specialist page reader, it's hard to grasp what a table contains in the same way as a sighted user would (for example, by rapidly scanning over the values to get a feel for what they represent). Therefore, good informative column headings are extremely useful. One way to achieve this is to use an attribute to specify a "long description" of the column contents, which is not visible in an ordinary graphical browser.
Microsoft chose to take advantage of the abbr (abbreviation) attribute that is supported by all visible elements. You can specify the values to be placed in this attribute for any of the column types that are used in the new GridView and DetailsView controls in ASP.NET 2.0. These column types are: BoundField, AutoGeneratedField, ButtonField, CommandField, CheckBoxField, HyperlinkField, ImageField, and TemplateField.
You just turn off auto-generation of the columns in the control, and then specify the columns you require - setting the AccessibleHeaderText property of each one. In our example, we turn off automatic column generation by adding the attribute AutoGenerateColumns="False" to the declaration of the GridView control. Then we specify the columns we want. The code below is the declaration of the first two columns, showing how we can set the visual header text and the associated header text to different values, both of which are different to the name of the column in the source rowset (as identified by the DataField attribute):
<asp:BoundField DataField="ProductID"
AccessibleHeaderText="Product Identifier"
HeaderText="ID"
ItemStyle-HorizontalAlign="Center">
<asp:BoundField DataField="ProductName"
AccessibleHeaderText="Full Product Name"
HeaderText="Product"
ItemStyle-HorizontalAlign="Left">
Look back at Figure 3 to see the output that is generated in Internet Explorer. If you view the source of the page, you'll see the associated header text as in this (abbreviated) listing:
<table accesskey="G" tabindex="3" id="MyGrid">
<caption align="Top"><u>G</u>ridView Example</caption>
<tr>
<th abbr="Product Identifier">ID</th>
<th abbr="Full Product Name">Product</th>
<th abbr="Quantity per Unit">Packaging</th>
<th abbr="Price per Unit">Price</th>
<th abbr="Units in Stock">Stock</th>
</tr>
...
... data rows here ...
...
</table>
Specifying the Row Headers in a GridView Control
In Figure 3, you can also see that the values in the column headed "Product" are displayed in bold text. This column is generated from the values in the ProductName column of the source rowset, and it is the ideal column to provide a "friendly name" identifier for each row.
You can think of the "friendly name" as the value you'd choose to identify a product for a visitor who wasn't familiar with the product range. Obviously, in our example, this is going to be the product name. However, if the output is aimed at users who will already be familiar with all the product identifier numbers (maybe a stock control clerk who is just looking up the price or the quantity in stock) then the product ID column might be a more appropriate choice.
The values in the column that contains the "friendly name" of each product are effectively the headers for each row in the GridView control output. In other words, a visitor uses the row header value in conjunction with the value in the column header to uniquely identify any other value in that row. In plain English, the value in the price column of the first row in Figure 3 is "the Price of Tofu".
While this type of instant association in a table is easy for visually capable users, it's a lot more difficult for non-visual user agents or specialist page readers to achieve. To make it easier, two more properties of the GridView control have been set in this example. We set the RowHeaderColumn property to the name in the source rowset that provides the "friendly name" values, and set the UseAccessibleHeader property to True:
RowHeaderColumn="ProductName"
UseAccessibleHeader="True"
This forces several changes in the output generated by the GridView control:
- It forces the values in every row in the column defined by the RowHeaderColumn property to be placed in <th> elements (instead of <td> elements). All the column headers are also placed in <th> elements.
- It adds the scope attribute to each <th> header cell. The column headers gain the attribute scope="col" to indicate that the value in this cell identifies or describes the values in this column. The row headers gain the attribute scope="row" to indicate that the value in this cell identifies or describes the values in the remaining cells of this row.
The abbreviated listing below shows some of the output generated by the GridView control in our example so that you can see the results more clearly:
<table accesskey="G" tabindex="3" id="MyGrid">
<caption align="Top"><u>G</u>ridView
Example</caption>
<tr>
<th scope="col" abbr="Product Identifier">ID</th>
<th scope="col" abbr="Full Product Name">Product</th>
<th scope="col" abbr="Quantity per Unit">Packaging</th>
<th scope="col" abbr="Price per Unit">Price</th>
<th scope="col" abbr="Units in Stock">Stock</th>
</tr>
<tr>
<td>14</td>
<th scope="row">Tofu</th>
<td>40 - 100 g pkgs.</td>
<td>$ 23.25</td>
<td>35</td>
</tr>
...
...
</table>
Specifying the headers Attribute for Cells in a GridView Control
While the accessibility improvements that result from setting the RowHeaderColumn and UseAccessibleHeader properties are admirable and a huge advance on the abilities of the grid controls in ASP.NET 1.x, it's relatively easy to improve on this even more by adding in the headers attribute to each non-header cell.
- Each cell in every row that is not a <th> header cell should have a headers attribute that identifies the row and column headers for this cell. For example, referring back to Figure 3, the cell containing the price of Tofu should carry the attribute: headers="id-of-Price-column-header, id-of-Tofu-row-header". If the column and row headers have ID values "ColumnHeader_Price" and "RowHeader_14" then the headers attribute should be: headers="ColumnHeader_Price,RowHeader_14".
To achieve this, we have to "interfere with" the generation of the output of the GridView control, by handling one of the events that is raised when each row is being created. The ideal event is RowDataBound, and to handle this we simply add the OnRowDataBound attribute to the declaration of the GridView control, specifying the name of the event handler we want to execute as each row is bound to the data source and the cells for the resulting HTML table row are being generated:
<asp:GridView id="MyGrid" runat="server"
...
OnRowDataBound="AddHeadersAttr">
The AddHeadersAttr event handler is shown in the next two listings. While it might look complicated, all it does is examine each row - as it is being data bound - and add the appropriate attributes to each cell in that row. If the current row is the header row for the grid (denoted by having the value Header from the DataControlRowType enumeration for its RowType property), the code adds an id attribute to each <th> cell that is created in this row. The value used is the text string "ColumnHeading_" concatenated with the column heading text:
Sub AddHeadersAttr(ByVal sender As Object, ByVal e As GridViewRowEventArgs)
If e.Row.RowType = DataControlRowType.Header Then
' this is the column header row, so add ID to each column using column name
' NOTE: cannot set ID property because this includes the ID of all parent
' controls as well, for example "MyGrid_ctl1_3" instead of just "3"
For i As Integer = 0 To e.Row.Cells.Count - 1
e.Row.Cells(i).Attributes.Add("id", _
"ColumnHeader_" & MyGrid.Columns(i).HeaderText)
Next
...
Notice that we add the id attribute using the Attributes collection to get the value we want. If we set the ID property of the cell directly, the control automatically prefixes it with the IDs of the parent controls - so that the value is guaranteed to be unique within the control tree even if you were to insert two instances of this GridView control into the page using the same ID values for the column header cells. However, as this ID contains the ID of the row as well as the ID of the GridView control, it's not suitable for use here. We need a value that is not dependent on the ID of the column-heading row when we come to add the headers attribute to the data cells in each row.
If the current row is not a header row, we then check if it's a data row (a row that is bound to the source data rowset). This type of row is denoted by the value DataRow for the RowType property. If it is a data row, it will have one value that is displayed in a <th> cell (the row header) and the remaining values displayed in <td> cells. So, as we iterate through the cells in the row, we have to check the control type.
If it's the row header (of type DataControlFieldHeaderCell), we add an id attribute to it as we did previously for the column headers, but this time using the text "RowHeader_" and the product ID value from this row. We can obtain the product ID from the DataKeys collection of the GridView control because we specified the attribute DataKeyNames="ProductID" when we declared the GridView control.
If the current cell in this row is an ordinary <td> data cell (of type DataControlFieldCell), we must instead add the appropriate headers attribute to it. This contains the ID values of the matching column and row headers, which can be built up using the current column name and the product ID (from the DataKeys collection) for the current row.
...
ElseIf e.Row.RowType = DataControlRowType.DataRow Then
' this is a data row
For i As Integer = 0 To e.Row.Cells.Count - 1
Dim oCell As Object = e.Row.Cells(i)
If TypeOf oCell Is DataControlFieldHeaderCell Then
' this is the row header, so add an ID to it using value of ProductID
CType(oCell, DataControlFieldHeaderCell).Attributes.Add("id", _
"RowHeader_" & MyGrid.DataKeys(e.Row.RowIndex).Value.ToString())
Else
' this is a data cell, so add the appropriate headers attribute
CType(oCell, DataControlFieldCell).Attributes.Add("headers", _
"ColumnHeader_" & MyGrid.Columns(i).HeaderText _
& ",RowHeader_" & MyGrid.DataKeys(e.Row.RowIndex).Value.ToString())
End If
Next
End If
End Sub
Notice that each value in the DataKeys collection (there is one value for each row) is itself a collection of DataKey instances, because in ASP.NET 2.0 the DataKeyNames attribute can be used to specify more than one column from the source data rowset as the keys for each row. You can access the values in the DataKeys collection using the new methods GetKeyByIndex, GetKeyByName, and the new property AllKeys. Alternatively, the Value property returns the value of the key for a specified row, given a row index, when only a single key is specified in the DataKeyNames attribute of the GridView.
The result of adding the event handler shown above is that the HTML table generated by the GridView control now has full accessibility support in the form of scope, id and headers attributes for every cell. The abbreviated listing here shows these attributes:
<table accesskey="G" tabindex="3"id="MyGrid">
<caption align="Top"><u>G</u>ridView
Example</caption>
<tr>
<th id="ColumnHeader_ID" scope="col" abbr="Product
Identifier">ID</th>
<th id="ColumnHeader_Product" scope="col" abbr="Full Product
Name">Product</th>
<th id="ColumnHeader_Packaging" scope="col" abbr="Quantity per
Unit">Packaging</th>
<th id="ColumnHeader_Price" scope="col" abbr="Price per
Unit">Price</th>
<th id="ColumnHeader_Stock" scope="col" abbr="Units in
Stock">Stock</th>
</tr>
<tr>
<td headers="ColumnHeader_ID,RowHeader_3">14</td>
<th id="RowHeader_3" scope="row">Tofu</th>
<td headers="ColumnHeader_Packaging,RowHeader_3">40 - 100 g
pkgs.</td>
<td headers="ColumnHeader_Price,RowHeader_3">$ 23.25</td>
<td headers="ColumnHeader_Stock,RowHeader_3">35</td>
</tr>
...
...
</table>
Viewing the Results in the IBM Home Page Reader
To get a feel for the way that these attributes can be used, and the huge advantage they provide, the IBM Home Page Reader allows the user to switch into Table Navigation Mode when there is an HTML table on the current page. It first describes the table by reading the caption, and then announces the number of rows and columns in the table.
Next, it reads the text in the header row, and continues for each cell in the table. However, the user can press the arrow keys at any time to move the current "reading point" to any other cell - moving up, left, right or down. As it moves from one column to the next, the reader uses the extra accessibility attributes to announce the header of the column that the user has moved to. When the user moves from one row to the next, it announces the value of the row header.
So, for example, when moving from the Price cell in the row containing the values for Aniseed Syrup to the row containing values for Alice Mutton, the reader will announce "Alice Mutton $39.00" (see Figure 4).

Figure 4 - How the IBM Home Page Reader uses the scope and headers attributes
The lower section of the window contains the text that is read to the user by the built-in speech synthesizer. The highlighted word is that currently being read aloud to the user. The IBM Home Page Reader is a combined graphical, text and aural Web browser with a raft of great features. See the section "Testing Your Sites and Applications" (earlier in this article) for details of how to obtain a copy of the IBM Home Page Reader, the Lynx text browser, and other specialist user agents.
This is the end of the first part of an article on using the new accessibility features in version 2.0 of ASP.NET. The second part continues the theme by looking at creating headers and scope attributes in custom HTML tables.