©2004 Stonebroom Limited,
Some two years ago, I was honored to be invited to take part in the design review panel for ASP.NET, looking at the feature set proposed for version 2.0. That seems a long time ago now, but one of concerns I expressed at the time was that ASP.NET should provide better support for developers to add features to their pages that improve accessibility for all users. The World Wide Web Consortium (W3C) and many other industry bodies have long been pushing for wider support across all sites for disabled and disadvantaged users, many of whom use special types of user agent to access Web pages.
The W3C issues guidelines under the umbrella of the Web Accessibility Initiative (WAI), whose mission is "...to lead the Web to its full potential..." includes amongst its goals "...promoting a high degree of usability for people with disabilities" (see http://www.w3.org/WAI/ for more details). Other sites provide useful guides, software, and tools that make it easier to build and test pages that maximize accessibility. You only have to search the Web for "Web Accessibility Guidelines" to get some idea of the variety and number of sites devoted to this topic.
However, this article is not a general reference to accessibility issues, or a primer on building accessible pages. What it does cover is how the ASP.NET team at Microsoft has added new features to the Framework that you can take advantage of in your Web applications to more easily provide better support for alternative types of browsers and user agents. It involves little extra effort on the part of the developer, but can make the world of difference to users.
While the
issues of providing reasonable accessibility for as large a proportion of the
population as possible might seem to be justifiable on their own, you also have
to consider the legal ramifications of not following such a policy. As in most
walks of life, governments are unwilling to leave it to industry to manage
their own affairs. To some extent, this is understandable – the W3C
accessibility initiatives have been around for a while, yet recent surveys show
that only a small number of “large commercial Web sites” (around 20%) have made
any real attempts at implementing them.
In the
In
In the
Probably the most direct and unequivocal indication of the
way governments around the world are beginning to involve themselves in
accessibility issues is from
And, to demonstrate how the Australian law has already been applied, probably the most famous of all actions taken by disabled Web users was when Bruce Maguire (a blind person) cited the Act in a claim against the Sydney Organizing Committee for the Olympic Games, which were held in 1999/2000. He complained to the committee that images on the Olympics.com Web site had no text equivalent for screen readers and Braille displays. SOCOG failed to react to directives and were fined A$20,000.
There are similar clauses in the in the UK Disability Discrimination Act, the US Americans with Disabilities Act, the Canadian Human Rights Act, plus various provincial, state, and territorial human-rights codes. Following settlement of the claim and the subsequent fine on SOCOG, the Toronto-based Web Content Consultancy contenu.nu stated that: “The case of Maguire vs. SOCOG will inevitably come into play as a precedent for legal cases worldwide.”
Web pages today are very different from the original vision of the pioneers the World Wide Web. They saw it as a sharing and publishing environment for scientific information, rather than the public network offering online retailing, the source of references material on every topic under the sun, and the general entertainment arena that it has now become.
Wide public access, and the continuing commercialization of the Web, has brought changes in the type of content it offers. The most remarkable change has been the move away from the functional, mainly text-based, types of pages. Today, the Web is ruled as much by designers and graphic artists as by developers and network specialists.
Web pages have become more complicated. No longer is it permissible for your company “home page” to contain a picture of your offices and a simple text menu for the services you offer. Now you have to have drop-down or pop-up menus with myriads of links, graphics (preferably animated), and dozens of headlines that lead to press releases, new product details, or testimonials of your services.
All this is fine if your visitors can look at the page and easily identify the areas that interest them, or scan up and down the links to other pages looking for what they want. And, should they stray to the wrong page, it’s usually pretty obvious from a quick glance. However, things are nowhere near this easy for all visitors.
A proportion of visitors will have difficulties with most current Web sites, which are designed almost without exception for people who have reasonable eyesight and are using a pointer device such as a mouse, trackball or graphics tablet. There are many people to whom one or both of these conditions cannot be applied. Often they will be using a specially designed user agent, or maybe a simple text-based browser. Or it might be aural page reader, which reads the contents of pages out loud, or a Braille reader that translates the text into a format that can be read by fingertip on a special output device.
For all these types of device, even the most basic Web site or Web application (designed and tested only in a modern graphical browser such as Internet Explorer) can be hard to read. At best, navigation through the site can be challenging, and certain parts of the content may be meaningless. Graphics and pictures won’t be displayed on text-based devices, while color-blind users may not be able to distinguish between the different lines or pie segments in your charts. At worst, it may be completely impossible use online forms, to even access parts of the site at all.
Accessibility problems for Web sites generally fall into three areas:
Unfortunately, it’s not easy to cleanly subdivide the techniques you should be using to improve accessibility into these three simple categories. Some features span more than one category, as you’ll see when we look at them in more detail.
This
section summarizes the recommendations for improving accessibility for visitors
in your Web pages and Web applications. For more details see the W3C site at http://www.w3.org/TR/WCAG20/.
<link rel="stylesheet"
title="Default" href="defaultstyle.css" type="text/css"
/>
Appreciating that you need to improve accessibility in your own sites, or build it into new sites, is only half the battle. Knowing where to start, what to do, and how best to do it is not easy. There are tools to help you check your pages. Examples can be found at the Watchfire 'Bobby' Accessibility Test Tools site (http://bobby.watchfire.com/), UsableNet (http://www.usablenet.com/) and at http://cast.org/products/. Guidelines and standards can be found at W3C’s own site: the Web Content Accessibility Guidelines are at http://www.w3.org/TR/WAI-WEBCONTENT/, and the W3C Policy and Laws Guide by Country are at http://www.w3.org/WAI/Policy/.
Other more direct techniques are to test your site using one of the specialist browsers or user agents. Try accessing your pages in a text-only browser such as Lynx (http://lynx.browser.org/) to see if you can navigate through all the pages without a mouse, and understand what they contain when there are no images. Or try an aural page reader, such as the IBM Home Page Reader (http://www-306.ibm.com/able/solution_offerings/hpr.html). Turn off your screen and see if you can navigate through your own site when the links and content are read out aloud.
In this article (which is divided into two parts), we'll demonstrate the main changes to the server controls provided with ASP.NET in comparison to those in ASP.NET 1.x, concentrating almost solely on accessibility issues. We've provided four example pages that you can experiment with, and which show the various new controls and properties in use.
You can download the examples from http://www.daveandal.net/articles/v2accessibility/.
The first
example demonstrates some useful features (as concerns accessibility) of two
new controls, as well as additional properties for some existing controls:
Although you can achieve the same effects with an HtmlGenericControl in ASP.NET 1.x, the new HtmlLink control makes it easier to declaratively or dynamically insert <link> elements into your pages and then manipulate the properties of these elements. A prime use for the <link> element, especially when maximizing accessibility, is to specify one or more stylesheets for your page(s). For example:
<link id="MyLink" runat="server" href="style.css" rel="stylesheet"
type="text/css" />
The HtmlLink element exposes a property named Href that sets the href attribute of the resulting <link> element, and so the URL of the stylesheet can be manipulated at runtime by reading and setting the value of this property. The remaining attributes in the declaration do not correspond to properties of the control. However, the ASP.NET server controls simply copy any unrecognized attributes in the declaration directly on to the element that they generate.
An alternative approach is to dynamically generate the HtmlLink elements for a page. The declaration of the first part of the example page listed below (example1.aspx) contains no <link> elements:
example1.aspx
<html>
<head id="elemHead" runat="server">
<!-- link elements will be inserted here -->
<title>DataGrid Control Enhancements</title>
</head>
<body>
...
However,
the server-side code contains a Page_Load event handler that creates new
instances of the HtmlLink control, sets their properties, and
inserts them into the page. It sets the Href property of each one to the URL of
the stylesheet, but the remaining attributes can only be set dynamically by
accessing the Attributes collection of the control directly.
<script runat="server">
Sub Page_Load()
ds1.ConnectionString = ConfigurationSettings. _
ConnectionStrings("nwind").ConnectionString
' generate new HtmlLink control for "Standard" stylesheet
Dim oLink1 As New HtmlLink()
With oLink1
' set properties and add required attributes
.Href = "style.css"
.Attributes.Add("rel", "stylesheet")
.Attributes.Add("type", "text/css")
.Attributes.Add("title", "Default")
End With
' insert into <head> section as a child control
elemHead.Controls.Add(oLink1)
' repeat for "Large Text" alternate stylesheet link
Dim oLink2 As New HtmlLink()
With oLink2
.Href = "style-large.css"
.Attributes.Add("rel", "alternate stylesheet")
.Attributes.Add("type", "text/css")
.Attributes.Add("title", "Large Text")
End With
elemHead.Controls.Add(oLink2)
End Sub
</script>
Notice that
both stylesheet links have a title attribute added. This attribute
specifies the text that the user will see for each one in the list of
stylesheets, if their browser allows them to choose the stylesheet to use. This
option is not available in Internet Explorer 6, which uses the last <link> element that contains the attribute
rel="stylesheet". The usual approach is to use the
title Default
for the default stylesheet, and then rel="alternate
stylesheet"
and a descriptive value for the title attribute in all alternative
stylesheet links. Internet Explorer will ignore stylesheet links that contain a
rel="alternate
stylesheet"
attribute in the <link> element.
The new HtmlLink controls are inserted into the control tree of the page as children of
the <head> element. The <head> element is declared as a server control (with
the runat="server" attribute - see the listing above), and so it
is represented in the control tree by an instance of the new HtmlHead control that is added to ASP.NET in version 2.0. The HtmlHead control (like almost all ASP.NET server controls) has a Controls collection for its child controls.
The Add
method of this collection adds the specified control, in our case the new HtmlLink controls, to the end of the Controls collection.
The output
that is generated from the code shown above is:
<link href="style.css" rel="stylesheet"
type="text/css" title="Default"
/>
<link href="style-large.css" rel="alternate
stylesheet" type="text/css"
title="Large Text" />
Figure 1 shows the page displayed in Mozilla 1.5, which allows the
style sheet to be selected. You can see the effects of choosing the "Large
Text" stylesheet in this screenshot.

Figure 1 - Choosing a stylesheet for example1.aspx in Mozilla
The next section
of control declarations in the example1.aspx page demonstrates the use of the
new AssociatedControlID property of the Label control, along with a couple of
other properties that were available in ASP.NET 1.x. When you build pages that
contain interactive controls, a useful feature for all users, not just those
with special accessibility requirements, is to provide hotkeys that move the
input focus directly to a specific control.
By using a
combination of the AssociatedControlID, AccessKey and TabIndex properties, you can make forms or
other pages containing interactive controls much easier for all your visitors
to use. Setting the TabIndex allows you to control the order
that the input focus moves from one control to the next. By default, the
browser uses the order of the control declarations within the page. In complex
forms, you can use the TabIndex property to make the input focus
move down the columns of a table, rather than the default of moving across the
rows, or force it to follow whatever path through the controls you require. Of
course, the path you choose should be an "obvious" one, so as not to
confuse users who will wonder where the input focus went if it jumps from
control to control in some non-intuitive way!
The AccessKey property specifies the key that, when pressed in conjunction with the Alt key, will move the focus to that
control on the page (there are some keys you can’t use, such as Alt-T, which opens the Tools menu in
Internet Explorer). It's also useful to indicate the hotkey to the user by
underlining that letter in the caption of the control - it works well with most
controls, although it doesn’t work with the ASP.NET Button control. The listing below
demonstrates the use of the TabIndex and AccessKey properties, along with underlining
the corresponding letter in the Label control that acts as the caption of
the TextBox:
...
<asp:Label id="lblProduct" runat="server"
Text="<u>P</u>roduct Name:"
AccessKey="P"
AssociatedControlID="txtProduct"
TabIndex="0" />
<asp:TextBox id="txtProduct" runat="server"
TabIndex="1"
Text="A" />
<asp:Button runat="server" id="bntGo" Text="Go"
ToolTip="Start search for matching products"
TabIndex="2" />
...
Because a Label control can’t receive the focus in
the browser, the focus will move to the next control in the declarative or
tabbing order of the page. So, in the code above, the TextBox will automatically get the focus when Alt-P is pressed. However,
there is no external indication that this will happen, especially for users who
depend on aural page readers or other specialist devices. The new AssociatedControlID property solves the problem by connecting the Label control to the interactive control
that it refers to, by adding the for attribute to it. For example, the code listed
above generates the following HTML when rendered to the client:
Figure 2
shows the appearance of the controls in Internet Explorer. Bear in mind,
however, that not all graphical browsers fully support hotkeys and the use of
associated labels.
![]()
Figure 2 - Using a combination of the AssociatedControlID,
AccessKey
and TabIndex
properties
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
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"
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>
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.
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:
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>
While the accessibility improvements that result from setting the RowHeaderColumn and UseAccessibleHeader properties is 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.
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
...
If the current row in 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(