The correct way to structure markup is to use HTML elements appropriately and to then use cascading style sheet (CSS) rules to specify their appearance. For example, use heading (h) elements to correctly structure a document, and then use CSS rules to specify the appropriate font size for each heading level.
This section contains the following guidelines that explain how to avoid using tables to define page layout, and how to make sure that tables created by ASP.NET controls are accessible:
Developers often mix structure and presentation in Web page design by using table elements to lay out the page or to control the relative locations of visual elements on the page. When tables are used for layout, the result is often a complex system of outer tables and embedded inner tables. Such structures are likely to cause screen readers to announce cell contents in a confusing sequence that is unrelated to the actual logical structure of the page.
There are many ways to use CSS with div and other HTML elements to control the appearance and location of visual features of the page. However, if you cannot avoid using tables for layout, you should verify that the content of the tables makes sense when linearized (that is, read in table-cell order: top row first, then the next row down, and so on).
For more information about how to use CSS, see Working with CSS Overview.
Using ASP.NET Controls that Generate Tables for Page Layout
Some ASP.NET controls automatically generate HTML tables for page layout, depending on how they configured. The following table lists these controls and explains how to configure them to avoid generating tables that do not conform to accessibility guidelines.
Note |
|---|
These controls generate semantically correct HTML and can use CSS instead of tables for formatting only when the RenderingCompatibility property is set to 4.0 or greater. |
Controls | Comments |
|---|
| By default, these controls render HTML that is wrapped in a table element whose purpose is to apply inline styles to the entire control. (You apply inline styles by setting properties such as BackColor or CssClass.) You do not have to use inline styles if you use templates to specify how HTML will be rendered for these controls. In that case you can set the RenderOuterTable property to false to prevent the outer table from being rendered. |
| By default, these controls render lists as table elements. However, they can render ul (unordered list) elements, ol (ordered list) elements, or span elements instead, depending on how you set the RepeatLayout property. To render semantically correct markup, set the RepeatLayout property to UnorderedList or OrderedList. The following formatting limitations apply if you use an ordered list or unordered list: The layout direction must be vertical. You cannot specify multiple columns. Headers, footers, and separators are not supported.
|
| By default, this control generates HTML that conforms to accessibility guidelines in the following ways: The generated HTML is structured as an unordered list (ul element). CSS is used for visual formatting. The menu behaves in accordance with ARIA standards for keyboard access. ARIA role and property attributes are added to the generated HTML.
However, the control provides the RenderingMode property for backward compatibility. If you set this property to Table, the control will generate HTML that uses table elements for formatting, as it did in ASP.NET 3.5 and earlier versions |
Even if you use HTML tables to present tabular data instead of for page layout, they can cause accessibility problems. When the content of a table is read aloud, it is easy for the listener to lose track of the current position in the table or and hard to remember which heading a particular cell pertains to. However, you can include features that make tables easier to understand.
thead and th Elements
To ensure that screen reader software can clearly associate table cells with their associated headers, table headings should always be in th elements, and the heading row should be within a thead element, as shown in the following example:
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr>
<td>Milk</td>
<td>$2.33</td>
</tr>
<tr>
<td>Cereal</td>
<td>$5.61</td>
</tr>
</tbody>
</table>
.png)
Some developers avoid using th elements because they do not like their default visual appearance. However, elements should not be relied on to control presentation. If you want the column headings to resemble normal table cells, you can add a style rule such as the following:
<style type="text/css">
th {text-align:left;font-weight:normal}
</style>
scope, headers, and axis Attributes
In order to make a table accessible, you should also explicitly indicate the heading or headings that are associated with each cell. There are several attributes that you can use for this purpose: scope, headers, and axis.
The scope attribute can be used to indicate whether a th element is a column heading or a row heading. For example, the following table contains both column headings and row headings, marked with th tags that use the scope attribute:
<table>
<thead>
<tr>
<th></th>
<th scope="col">First Train</th>
<th scope="col">Last Train</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Alewife</th>
<td>5:24am</td>
<td>12:15am</td>
</tr>
<tr>
<th scope="row">Braintree</th>
<td>5:15am</td>
<td>12:18am</td>
</tr>
</tbody>
</table>
.png)
This example table contains the schedule for the Red Line subway in Boston, Massachusetts. Notice that each column heading includes a scope="col" attribute, and each row heading includes a scope="row" attribute.
The scope attribute works well for simple tables. However, for more complex tables, you must use the headers attribute. For example, a nested table might have three or more headings that are associated with a single cell. In the following example of a Red Line Schedule table, the cell that contains 5:24am pertains to three headings: First Train, Weekday, and Alewife:
.png)
The headers attribute enables you to explicitly identify the headings that pertain to each cell. You can specify multiple headings by entering them as a space-delimited list.
The axis attribute enables you to categorize headings. In the Red Line Schedule table, you can identify Alewife and Braintree as location headings, Weekday and Saturday as day headings, and First Train and Last Train as train headings. If a single heading belongs to multiple categories, you can specify a comma-delimited list for the axis attribute.
The following .aspx page renders the Red Line Schedule, using headers and axis attributes.
<%@ Page %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head id="Head1" runat="server">
<title>Red Line Subway Schedule</title>
<style type="text/css">
caption {color:white;background-color:red;font-size:xx-large}
table {width:500px;border-collapse:collapse}
td,th {padding:5px}
td {border:1px solid black}
tbody th {text-align:right}
.headerRow th {font-size:x-large;text-align:left}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<table summary="This table contains the schedule of train
departures for the Red Line">
<caption>Red Line Schedule</caption>
<thead>
<tr>
<th></th>
<th id="hdrFirstTrain" axis="train">First Train</th>
<th id="hdrLastTrain" axis="train">Last Train</th>
</tr>
</thead>
<tbody>
<tr class="headerRow">
<th id="hdrWeekday" axis="day" colspan="3">Weekday</th>
</tr>
<tr>
<th id="hdrAlewife1" axis="location">Alewife</th>
<td headers="hdrAlwife1 hdrWeekday hdrFirstTrain">5:24am</td>
<td headers="hdrAlwife1 hdrWeekday hdrLastTrain">12:15am</td>
</tr>
<tr>
<th id="hdrBraintree1" axis="location">Braintree</th>
<td headers="hdrBraintree1 hdrWeekday hdrFirstTrain">
5:15am
</td>
<td headers="hdrBraintree1 hdrWeekday hdrLastTrain">
12:18am
</td>
</tr>
<tr class="headerRow">
<th id="hdrSaturday" axis="day" colspan="3">Saturday</th>
</tr>
<tr>
<th id="hdrAlewife2" axis="location">Alewife</th>
<td headers="hdrAlewife2 hdrSaturday hdrFirstTrain">8:24am</td>
<td headers="hdrAlewife2 hdrSaturday hdrLastTrain">11:15pm</td>
</tr>
<tr>
<th id="hdrBraintree2" axis="location">Braintree</th>
<td headers="hdrBraintree2 hdrSaturday hdrFirstTrain">
7:16am
</td>
<td headers="hdrBraintree2 hdrSaturday hdrLastTrain">
10:18pm
</td>
</tr>
</tbody>
</table>
</div>
</form>
</body>
</html>
Each heading is a th element that has a unique id value, and each table cell is a td element that has a headers attribute. Each headers attribute contains a list of the heading id values that pertain to that cell. Each th element also has an axis attribute that identifies the category that is associated with the heading.
caption and summary Elements
The semantically correct way to provide a title for an HTML table is to use the caption element. By default, browsers render the contents of the caption element as the title of the table. The title of the Red Line Schedule table that is shown in the previous section is a caption that has been formatted with a red background and white foreground by using CSS.
You can also provide a longer description of the table by using the summary attribute. The summary attribute is not rendered by the browser but can be announced by a screen reader, the way the alt attribute for an image is used. The preceding .aspx page also defines a summary attribute for the Red Line Schedule table.
Using the Table Control to Create Accessible Tables
If you use the ASP.NET Table control to create a table, you can set the caption attribute by setting the Caption property. You can create table headers by creating instances of the TableHeaderRow class and by setting the TableSection property to the TableRowSection..::.TableHeader enumeration value. This causes the table to render thead and tbody elements. When you create a cell with the TableCell control, you can set the AssociatedHeaderCellID property to the ID of a table header cell. This causes the cell to render a header attribute that associates the cell with the corresponding column heading.
The following example shows how to use the Table control to create a table that is identical to the one that is shown in Figure 2. The HTML that is rendered for this table includes thead and tbody elements and header attributes.
<asp:Table ID="Table1" runat="server">
<asp:TableHeaderRow TableSection="TableHeader">
<asp:TableHeaderCell ID="productheader">Product</asp:TableHeaderCell>
<asp:TableHeaderCell ID="priceheader">Price</asp:TableHeaderCell>
</asp:TableHeaderRow>
<asp:TableRow>
<asp:TableCell AssociatedHeaderCellID="productheader">Milk</asp:TableCell>
<asp:TableCell AssociatedHeaderCellID="priceheader">$2.33</asp:TableCell>
</asp:TableRow>
<asp:TableRow>
<asp:TableCell AssociatedHeaderCellID="productheader">Cereal</asp:TableCell>
<asp:TableCell AssociatedHeaderCellID="priceheader">$5.61</asp:TableCell>
</asp:TableRow>
</asp:Table>
Using Data Controls that Automatically Create Tables
ASP.NET controls that automatically display data in HTML tables include the following:
The tables that these controls generate include accessibility features by default, such as th elements that have scope attributes. In addition, you can enable additional accessibility features by setting certain properties. For example, the GridView control supports several properties relevant to accessibility:
Caption and CaptionAlign. Use these properties to add a caption to the HTML table that is generated by the GridView control.
RowHeaderColumn. Use this property to indicate a column that is used for row headers. Set the property to the name of a column returned from the data source (such as CustomerID). This property only works with bound fields. It does not work with template fields.
HeaderRow and FooterRow. Use these properties property to generate thead, tbody, and tfoot elements. Set the TableSection property of the HeaderRow property to TableHeader in order to generate thead elements, and set the TableSection property of the FooterRow property to TableFooter to generate tfoot elements. If either thead or tfoot elements are generated, tbody elements are generated also.
UseAccessibleHeader. Use this property to indicate whether column headings should be rendered within th elements or td elements. By default, this property is set to true.
The GridView control does not have a predefined property that maps to the summary attribute. However, you can declare the summary attribute in markup as an expando attribute.
In the following example, a GridView control is bound to a SqlDataSource control that retrieves Customer table rows from the database. When the page is opened in a browser, the customer information is displayed in an HTML table.
<%@ Page Language="VB"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader
End Sub
</script>
<html >
<head id="Head1" runat="server">
<title>Display Customers</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server"
AutoGenerateColumns="False"
RowHeaderColumn="CustomerID"
Caption="Customers"
summary="This table shows a list of customers."
DataKeyNames="CustomerID" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="CustomerID"
HeaderText="Customer ID"
InsertVisible="False" ReadOnly="True"
SortExpression="CustomerID" />
<asp:BoundField DataField="FirstName"
HeaderText="FirstName"
SortExpression="FirstName" />
<asp:BoundField DataField="MiddleName"
HeaderText="MiddleName"
SortExpression="MiddleName" />
<asp:BoundField DataField="LastName"
HeaderText="LastName"
SortExpression="LastName" />
</Columns>
</asp:GridView>
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:AdventureWorksLTConnectionString %>"
SelectCommand="SELECT CustomerID, FirstName, MiddleName,
LastName FROM SalesLT.Customer">
</asp:SqlDataSource>
</div>
</form>
</body>
</html>
<%@ Page Language="C#"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
protected void Page_Load(object sender, EventArgs e)
{
GridView1.HeaderRow.TableSection = TableRowSection.TableHeader;
}
</script>
<html >
<head id="Head1" runat="server">
<title>Display Customers</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView ID="GridView1" runat="server"
AutoGenerateColumns="False"
RowHeaderColumn="CustomerID"
Caption="Customers"
summary="This table shows a list of customers."
DataKeyNames="CustomerID" DataSourceID="SqlDataSource1">
<Columns>
<asp:BoundField DataField="CustomerID"
HeaderText="Customer ID"
InsertVisible="False" ReadOnly="True"
SortExpression="CustomerID" />
<asp:BoundField DataField="FirstName"
HeaderText="FirstName"
SortExpression="FirstName" />
<asp:BoundField DataField="MiddleName"
HeaderText="MiddleName"
SortExpression="MiddleName" />
<asp:BoundField DataField="LastName"
HeaderText="LastName"
SortExpression="LastName" />
</Columns>
</asp:GridView>
<asp:SqlDataSource ID="SqlDataSource1" runat="server"
ConnectionString="<%$ ConnectionStrings:AdventureWorksLTConnectionString %>"
SelectCommand="SELECT CustomerID, FirstName, MiddleName,
LastName FROM SalesLT.Customer">
</asp:SqlDataSource>
</div>
</form>
</body>
</html>
.png)
The source of the rendered page shows the following features in the generated HTML:
th elements that have scope attributes for the heading row are generated automatically.
thead and tbody elements are generated because the HeaderRow property is set in the Page_Load method.
The caption element is generated because the Caption property is set in markup.
th elements with scope attributes are generated for the Customer ID column because the RowHeaderColumn property is set in markup.
The summary attribute is generated because a summary expando attribute is set in markup.
Using ASP.NET Dynamic Data
ASP.NET Dynamic Data uses GridView controls to display data in tables and FormView controls to display forms in which you can update the table data.
The tables displayed by GridView controls do not have caption attributes, thead elements, or tbody elements. However, you can add these features by creating a page for each data table in the CustomPages folder and customizing the markup and code-behind for the GridView control.
The forms displayed by the FormView controls use HTML tables for page layout. However, they conform to accessibility guidelines, because they make sense when linearized, and because field labels are rendered by using label elements with for attributes.
For more information about ASP.NET Dynamic Data, see ASP.NET Dynamic Data Content Map.
Using Templates with Data Controls
For some data controls, you can use templates to explicitly specify the HTML elements and attributes that data will be displayed in. The following controls require the use of templates:
These controls do not render any markup automatically. You define the header, body, and footer templates for the control, in which you can specify any markup. If you want one of these controls to render an HTML table, you should include the appropriate markup to meet accessibility standards.
The flexibility of templates makes it possible to generate complicated tables. For example, imagine that you want to display a list of customers and under each customer you want to display a list of that customer's addresses. In other words, you want to create a single-page master/detail form. In that case, you must include the headers attribute for each table cell.
The ASP.NET page in the following example creates a single-page master/detail form. This example illustrates how you can nest one ListView control in a second ListView control in order to generate a complex table that follows accessibility guidelines.
<%@ Page Language="VB" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
Private AddressesTable As New DataTable()
Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
Dim AddressesAdapter As New SqlDataAdapter(
"SELECT SalesLT.CustomerAddress.CustomerID, " &
"SalesLT.CustomerAddress.AddressID, " &
"SalesLT.Address.AddressLine1, " &
"SalesLT.Address.City, SalesLT.Address.StateProvince, " &
"SalesLT.Address.PostalCode " &
"FROM SalesLT.CustomerAddress " &
"INNER JOIN SalesLT.Address ON " &
"SalesLT.CustomerAddress.AddressID = SalesLT.Address.AddressID",
"Data Source=.\SQLEXPRESS;" &
"AttachDbFilename=|DataDirectory|\AdventureWorksLT_Data.mdf;" &
"Integrated Security=True;User Instance=True")
AddressesAdapter.Fill(AddressesTable)
End Sub
Protected Function GetAddresses(ByVal customerID As Object) As DataView
Dim addressesView As DataView = AddressesTable.DefaultView
addressesView.RowFilter = "CustomerID=" & customerID.ToString()
Return (addressesView)
End Function
Private Function GetCustomerHeaderID(ByVal item As ListViewItem) As String
Return ("hdrCustomer" & item.DataItemIndex.ToString())
End Function
Private Function GetAddressHeaderID(ByVal item As ListViewItem) As String
Return ("hdrAddress" &
CType(item.DataItem, DataRowView)("AddressID").ToString())
End Function
Protected Function GetColumnHeaderIDs(ByVal item As ListViewDataItem,
ByVal columnHeader As String) As String
Dim customerHeaderID As String =
GetCustomerHeaderID(
CType(item.NamingContainer.NamingContainer, ListViewItem))
Dim addressHeaderID As String = GetAddressHeaderID(item)
Return (String.Format("{0} {1} {2}",
customerHeaderID,
addressHeaderID,
columnHeader))
End Function
Protected Sub CustomersListView_ItemDataBound(ByVal sender As Object,
ByVal e As ListViewItemEventArgs)
Dim addressesListView As New ListView()
addressesListView = CType(e.Item.FindControl("AddressesListView"), ListView)
Dim drv As DataRowView = CType(e.Item.DataItem, DataRowView)
addressesListView.DataSource = GetAddresses(drv("CustomerID"))
addressesListView.DataBind()
End Sub
</script>
<html >
<head id="Head1" runat="server">
<title>Customers and Addresses</title>
<style type="text/css">
.customerRow
{
background-color: yellow;
}
th
{
text-align: left;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:SqlDataSource ID="CustomersSqlDataSource" runat="server" ConnectionString="<%$ ConnectionStrings:AdventureWorksLTConnectionString %>"
SelectCommand="SELECT CustomerID,
FirstName, MiddleName, LastName FROM SalesLT.Customer" />
<asp:ListView ID="CustomersListView" runat="server" DataKeyNames="CustomerID" DataSourceID="CustomersSqlDataSource"
OnItemDataBound="CustomersListView_ItemDataBound">
<LayoutTemplate>
<table summary="A list of customers with one or more addresses for each customer.">
<caption>
Customers and Addresses</caption>
<thead>
<tr>
<th id="hdrID">
ID
</th>
<th id="hdrStreet">
Street
</th>
<th id="hdrCity">
City
</th>
<th id="hdrState">
State
</th>
</tr>
</thead>
<tbody>
<tr id="itemPlaceholder" runat="server">
</tr>
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr class="customerRow">
<th colspan="4" id='<%# GetCustomerHeaderID(Container) %>'>
<%# Eval("FirstName") %>
<%# Eval("MiddleName") %>
<%# Eval("LastName") %>
</th>
</tr>
<asp:ListView ID="AddressesListView" runat="server">
<LayoutTemplate>
<tr id="itemPlaceHolder" runat="server">
</tr>
</LayoutTemplate>
<ItemTemplate>
<tr>
<th id='<%# GetAddressHeaderID(Container) %>'>
<%# Eval("AddressID") %>
</th>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrStreet") %>'>
<%# Eval("AddressLine1") %>
</td>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrCity") %>'>
<%# Eval("City") %>
</td>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrState") %>'>
<%# Eval("StateProvince") %>
</td>
</tr>
</ItemTemplate>
</asp:ListView>
</ItemTemplate>
</asp:ListView>
</div>
</form>
</body>
</html>
<%@ Page Language="C#" %>
<%@ Import Namespace="System.Data" %>
<%@ Import Namespace="System.Data.SqlClient" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<script runat="server">
private DataTable AddressesTable = new DataTable();
protected void Page_Load(object sender, EventArgs e)
{
SqlDataAdapter AddressesAdapter = new SqlDataAdapter(
"SELECT SalesLT.CustomerAddress.CustomerID, " +
"SalesLT.CustomerAddress.AddressID, " +
"SalesLT.Address.AddressLine1, " +
"SalesLT.Address.City, SalesLT.Address.StateProvince, " +
"SalesLT.Address.PostalCode FROM SalesLT.CustomerAddress " +
"INNER JOIN SalesLT.Address ON " +
"SalesLT.CustomerAddress.AddressID = SalesLT.Address.AddressID",
@"Data Source=.\SQLEXPRESS;" +
@"AttachDbFilename=|DataDirectory|\AdventureWorksLT_Data.mdf;" +
@"Integrated Security=True;User Instance=True");
AddressesAdapter.Fill(AddressesTable);
}
protected DataView GetAddresses(object customerID)
{
DataView view = AddressesTable.DefaultView;
view.RowFilter = "CustomerID=" + customerID.ToString();
return view;
}
private string GetCustomerHeaderID(ListViewItem item)
{
return "hdrCustomer" + item.DataItemIndex.ToString();
}
private string GetAddressHeaderID(ListViewItem item)
{
return "hdrAddress" +
((DataRowView)item.DataItem)["AddressID"].ToString();
}
protected string GetColumnHeaderIDs
(ListViewDataItem item, string columnHeader)
{
string customerHeaderID =
GetCustomerHeaderID
((ListViewItem)item.NamingContainer.NamingContainer);
string addressHeaderID = GetAddressHeaderID(item);
return string.Format("{0} {1} {2}",
customerHeaderID, addressHeaderID, columnHeader);
}
protected void CustomersListView_ItemDataBound
(object sender, ListViewItemEventArgs e)
{
ListView addressesListView = new ListView();
addressesListView = e.Item.FindControl("AddressesListView")
as ListView;
DataRowView drv = e.Item.DataItem as DataRowView;
addressesListView.DataSource = GetAddresses(drv["CustomerID"]);
addressesListView.DataBind();
}
</script>
<html >
<head id="Head1" runat="server">
<title>Customers and Addresses</title>
<style type="text/css">
.customerRow { background-color: yellow; }
th { text-align: left; }
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:SqlDataSource ID="CustomersSqlDataSource" runat="server"
ConnectionString="<%$ ConnectionStrings:AdventureWorksLTConnectionString %>"
SelectCommand="SELECT CustomerID,
FirstName, MiddleName, LastName FROM SalesLT.Customer" />
<asp:ListView ID="CustomersListView" runat="server"
DataKeyNames="CustomerID" DataSourceID="CustomersSqlDataSource"
OnItemDataBound="CustomersListView_ItemDataBound">
<LayoutTemplate>
<table summary="A list of customers with one or more addresses for each customer.">
<caption>Customers and Addresses</caption>
<thead>
<tr>
<th id="hdrID">ID</th>
<th id="hdrStreet">Street</th>
<th id="hdrCity">City</th>
<th id="hdrState">State</th>
</tr>
</thead>
<tbody>
<tr id="itemPlaceholder" runat="server"></tr>
</tbody>
</table>
</LayoutTemplate>
<ItemTemplate>
<tr class="customerRow">
<th colspan="4" id='<%# GetCustomerHeaderID(Container) %>'>
<%# Eval("FirstName") %>
<%# Eval("MiddleName") %>
<%# Eval("LastName") %>
</th>
</tr>
<asp:ListView ID="AddressesListView" runat="server">
<LayoutTemplate>
<tr id="itemPlaceHolder" runat="server"></tr>
</LayoutTemplate>
<ItemTemplate>
<tr>
<th id='<%# GetAddressHeaderID(Container) %>'>
<%# Eval("AddressID") %>
</th>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrStreet") %>'>
<%# Eval("AddressLine1") %>
</td>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrCity") %>'>
<%# Eval("City") %>
</td>
<td headers='<%# GetColumnHeaderIDs(Container, "hdrState") %>'>
<%# Eval("StateProvince") %>
</td>
</tr>
</ItemTemplate>
</asp:ListView>
</ItemTemplate>
</asp:ListView>
</div>
</form>
</body>
</html>
.png)
In this example, the outer ListView control lists the customer names, and the inner ListView control lists the matching addresses (a customer can have multiple addresses). The GetCustomerHeaderID and GetAddressHeaderID functions generate id values for the customer name and address ID headers, and the GetColumnHeaderIDs generates headers attribute values for table cells.
The ASP.NET page that is shown in the preceding example generates an HTML table that looks like the following:
<table>
<thead>
<tr>
<th id="hdrID">ID</th>
<th id="hdrStreet">Street</th>
<th id="hdrCity">City</th>
<th id="hdrState">State</th>
</tr>
</thead>
<tbody>
<tr class="customerRow">
<th colspan="4" id='hdrCustomer0'>Orlando N. Gee</th>
</tr>
<tr>
<th id='hdrAddress832'>832</th>
<td headers='hdrCustomer0 hdrAddress832 hdrStreet'>
2251 Elliot Avenue
</td>
<td headers='hdrCustomer0 hdrAddress832 hdrCity'>
Seattle
</td>
<td headers='hdrCustomer0 hdrAddress832 hdrState'>
Washington
</td>
</tr>
<tr class="customerRow">
<th colspan="4" id='hdrCustomer1'>Keith Harris</th>
</tr>
<tr>
<th id='hdrAddress833'>833</th>
<td headers='hdrCustomer1 hdrAddress833 hdrStreet'>
3207 S Grady Way
</td>
<td headers='hdrCustomer1 hdrAddress833 hdrCity'>
Renton
</td>
<td headers='hdrCustomer1 hdrAddress833 hdrState'>
Washington
</td>
</tr>
<tr>
<th id='hdrAddress297'>297</th>
<td headers='hdrCustomer1 hdrAddress297 hdrStreet'>
7943 Walnut Ave
</td>
<td headers='hdrCustomer1 hdrAddress297 hdrCity'>
Renton
</td>
<td headers='hdrCustomer1 hdrAddress297 hdrState'>
Washington
</td>
</tr>
[remaining rows of the table]
</tbody>
<table>
Notice that each td tag contains an appropriate headers attribute.
If you access a Web page form through a screen reader, it might be difficult to associate form fields with their corresponding labels. For example, imagine that a Web page contains the following form that displays input fields for a person's first name and last name:
First Name, Last Name:
<input name="txtFirstName" />
<input name="txtLastName" /></td>
The text and elements all display on the same line, and it would be immediately obvious to a person who is viewing the screen which input box goes with which field label. But because the "Last Name" text is next to the txtFirstName textbox, it might be difficult for the user of a screen reader to associate each label with its matching form field. HTML 4.0 addresses this problem by introducing the label element to enable you to explicitly associate a form field label with a form field. The following example shows how the previous form should be written by using a label element:
<label for="txtFirstName">First Name</label>,
<label for="txtLastName">Last Name</label>:
<input name="txtFirstName" id="txtFirstName" />
<input name="txtLastName" id="txtLastName" />
Notice that the input fields include an id attribute. The value of the for attribute must be an input field's id and not its name attribute.
Associating Input Fields With ASP.NET Label Controls
Normally, the ASP.NET Label control generates a span element. However, if you set the control's AssociatedControlID property, it renders a label element. The following example shows how you can generate an accessible form with ASP.NET Label and TextBox controls.
<asp:Label AssociatedControlID="txtFirstName"
runat="server">First Name</asp:Label>,
<asp:Label AssociatedControlID="txtLastName"
runat="server">Last Name</asp:Label>:
<asp:TextBox ID="txtFirstName" runat="server" />
<asp:TextBox ID="txtLastName" runat="server" />
When you provide a label for an ASP.NET control, you should use the ASP.NET Label control instead of the HTML label element. This is because by default, ASP.NET renders an HTML id attribute value that differs from the ID that you assign when you declare a control such as a TextBox control. Therefore, if you use a label element and put the declared ID of the TextBox control in the label element's for attribute, the rendered id and corresponding for attributes might not match. When you use the ASP.NET Label control, ASP.NET automatically makes sure that the rendered id and corresponding for attributes match.
Associating ASP.NET CheckBox and RadioButton Controls With Labels
The following ASP.NET controls automatically render label elements:
When you add one of these controls in a page, make sure that you use the Text property instead of including text between beginning and ending tags in markup. For example, you should not do the following:
<asp:CheckBox runat="Server" /> Include Gift Wrap
Instead, do the following:
<asp:CheckBox text="Include Gift Wrap" runat="Server" />
A label element with a for attribute is generated for one of these controls only when you set its Text property.