Server Control Accessibility Enhancements in ASP.NET 2.0
Part one of this article discussed general accessibility issues that you really should bear in mind when building Web sites and Web applications to make it as easy as possible for disabled visitors to be able to understand the content, navigate around the site, and generally be able to use it with a specialist user agent such as a text browser or aural page reader. I demonstrated some of the new features in ASP.NET that make accessibility easier to include in your sites. The second part of the article examines the remaining features.
Example 1, in the previous part of the article, demonstrated the new HtmlLink control, the AssociatedControlID property of the Label control and the use of a hotkey, plus the use of table captions and table row and column headers.
Example 2, which is the first of the three remaining examples in this part of the article, demonstrates the use of the AssociatedHeaderCellID property and the use of headers and scope attributes when generating HTML tables dynamically.
Example 3 demonstrates the improved support for the alt attribute for images.
Example 4 demonstrates the new LabelAttributes and InputAttributes properties of the CheckBox control.
Example 2: Generating the headers and scope Attribute in an HTML Table
In the first example (in the previous article), you saw how you can add attributes to the HTML table that is generated by a GridView control to make it easier for non-visual page readers to understand the content of the table. However, if you want to exert more control over the output - for example by using cells that span more than one row or column or have some specific formatting requirements - the ASP.NET Table control is probably the only way to go.
The main issue here is that, when you build your output using the Table control, the automatic generation of scope and headers attributes is not available. Instead, you have to use features of the Table and its child control types to generate them yourself. This approach uses a new property of the TableCell control:
The AssociatedHeaderCellID property takes an array of String values and generates a headers attribute. This attribute links a table cell to one or more specific table header cells through their ID values. In tables that are not a simple grid layout (in other words, use column or row spans, or individual row headers), this allows non-visual user agents to relate the data with the header. This property applies only to the TableCell control.
The example shown here (example2.aspx) uses a DataTable that is created in code, so you ccan display it using a GridView control. However, to demonstrate the use of the AssociatedHeaderCellID property, you create the output using the ASP.NET Table, TableCell, TableHeaderCell and TableFooterCell controls. You add custom formatting to the resulting table by adding a footer row containing column totals, and check for invalid results as you build the table to display a warning message. Figure 5 shows the results of this example in Internet Explorer.
Figure 5 - The resulting table generated by example2.aspx shown in Internet Explorer
The code to generate this table is shown in the listings that follow. Because you need to create an instance of an ADO.NET DataTable, you have to import the System.Data namespace. And, as you'll be generating color values dynamically for the cells in the table, you also need the System.Drawing namespace in order to use the static Color class:
The declaration of the table is shown next. As well as the styling and layout attributes, you provide a caption for the table and a hot key to access the table. Although there are no editable rows or interactive controls in the table, the hot key allows the user to easily scroll the page to bring the top of the table into view:
<asp:Table id="MyTable" runat="server"
GridLines="Both"
Caption="<u>A</u>ggregated and Total Results"
AccessKey="A"
SummaryViewColumnIndex="1"
BackColor="#99ccff" BorderStyle="Solid"
BorderWidth="3" BorderColor="#000099"
CellPadding="5" CellSpacing="8"
HorizontalAlign="NotSet" />
The Server-side Code to Generate the Table
The server-side code that populates the table is in a routine named CreateTable, which uses as its source data a DataTable created by the GetDataTable function. These routines are called in the Page_Load event handler:
Sub Page_Load()
CreateTable(GetDataTable())
End Sub
The GetDataTable function is simple enough. It creates a new DataTable instance, specifies the five columns, and then adds four rows. Note that the second row has zero values for the last three columns, which is taken to signify invalid data in this example:
Function GetDataTable() As DataTable
Dim oTable As New DataTable("MyTable")
oTable.Columns.Add("Period", System.Type.GetType("System.String"))
oTable.Columns.Add("Input", System.Type.GetType("System.Int32"))
oTable.Columns.Add("Output", System.Type.GetType("System.Int32"))
oTable.Columns.Add("Aggregation", System.Type.GetType("System.Decimal"))
oTable.Columns.Add("Result", System.Type.GetType("System.Decimal"))
Dim oRow As DataRow = oTable.NewRow()
oRow("Period") = "Q1-2002"
oRow("Input") = 3675
oRow("Output") = 6282
oRow("Aggregation") = 17.3
oRow("Result") = 2.88
oTable.Rows.Add(oRow)
oRow = oTable.NewRow()
oRow("Period") = "Q2-2002"
oRow("Input") = 731
oRow("Output") = 0
oRow("Aggregation") = 0
oRow("Result") = 0
oTable.Rows.Add(oRow)
... more rows created here ...
End Function
The CreateTable routine is, by comparison, much more complex. However, bear in mind that this type of routine is used specifically because you want to be able to create a non-standard and non-regular tabular output. You start by declaring some variables that will be required, and then generate the row of cells that will form the column headers.
Sub CreateTable(ByVal oDT As DataTable)
Dim iRowCount As Integer = oDT.Rows.Count
Dim iColCount As Integer = oDT.Columns.Count
Dim iRow, iCol As Integer
Dim oRow As TableRow
Dim oCell As TableCell
' create header row, setting ID of each cell
oRow = New TableHeaderRow()
For iCol = 0 To iColCount - 1
oCell = New TableHeaderCell()
' set ID for each header cell
oCell.ID = "ColumnHeader_" & oDT.Columns(iCol).ColumnName
oCell.Controls.Add(New LiteralControl(oDT.Columns(iCol).ColumnName))
oCell.BorderStyle = BorderStyle.None
' add scope="col" attribute to each header cell
oCell.Attributes.Add("scope", "col")
oRow.Cells.Add(oCell)
Next
MyTable.Rows.Add(oRow)
...
Notice that you use the TableHeaderCell control, so as to create <th> elements, and that each one has an ID property specified (in this example we use the text "ColumnHeader_" followed by the column name). These two features are required for the AssociatedHeaderCellID property to be used to generate the headers attributes. You also add the scope attribute to the column header cells, so that page readers can identify how they relate to the content of the rows:
Creating the Data Rows for the Body of the Table
The next section of code is the most complicated, as it must generate the rows that form the body of the table. However, each row must have one column that acts as the "header" for that row (the row header), and the usual position for this is in the left-most column. In this example, this column displays the date and year.
Of course, as you saw with the GridView example (in part one of this article), the row header does not have to be located in the first column. But most users will expect it to be there, especially where the output you are generating more resembles a spreadsheet than a rowset.
The basic process involves iterating through the rows. For each row, the code creates a new TableRow and adds to it a TableHeaderCell for the first value in the row (to get a <th> element). It also assigns it an ID that is specific to this row. You use the text "RowHeader_" and the value in that cell as the ID. If there was an ID value in the source data rowset - as with the GridView example in part one - this value could be used instead. It must be a value that is unique amongst the set of rows, so that it can be used in the headers attribute for the remaining cells in this row.
Next the code adds the attribute scope="row" that helps page readers relate the row header to the rest of the table. And then it can create a TableCell for each of the remaining values in this row, and add them to the current TableRow:
...
' create data rows
For iRow = 0 To iRowCount - 1
' first cell is a <th> "header" containing description of row
oRow = New TableRow()
oCell = New TableHeaderCell()
'set ID for "header" cell
oCell.ID = "RowHeader_" & oDT.Rows(iRow)(0)
oCell.Controls.Add(New LiteralControl(oDT.Rows(iRow)(0)))
oCell.BorderStyle = BorderStyle.None
' add scope="row" attribute to this header cell
oCell.Attributes.Add("scope", "row")
oRow.Cells.Add(oCell)
' remaining cells are data <td> elements
For iCol = 1 To iColCount - 1
oCell = New TableCell()
oCell.BorderStyle = BorderStyle.None
oCell.HorizontalAlign = HorizontalAlign.Center
' add HTML "headers" attributes to cell
oCell.AssociatedHeaderCellID = New String() _
{"ColumnHeader_" & oDT.Columns(iCol).ColumnName, _
"RowHeader_" & oDT.Rows(iRow)(0)}
' check if "Output" value is valid
If (oDT.Columns(iCol).ColumnName = "Output") _
And (oDT.Rows(iRow)(iCol).ToString = "0") Then
oCell.ColumnSpan = 3
' insert "invalid" message into cell
oCell.Controls.Add(New LiteralControl("Invalid Result"))
oCell.BackColor = Color.FromName("Red")
oCell.ForeColor = Color.FromName("White")
oCell.Font.Bold = True
' skip next two columns
iCol += 2
Else
' insert value from Data Table into cell
oCell.Controls.Add(New LiteralControl(oDT.Rows(iRow)(iCol).ToString()))
End If
oRow.Cells.Add(oCell)
Next
MyTable.Rows.Add(oRow)
Next
...
The point of interest here is how the headers attribute is created for each cell. The AssociatedHeaderCellID property expects to receive an Array of String values that are the IDs of the header cells that correspond to this cell. These values are calculated for each cell using the text strings we specified when we created the header cells ("ColumnHeader_" and "RowHeader_") concatenated with the ID of the current column and row header cell IDs. For clarity, the code line that sets the AssociatedHeaderCellID property is shown again here:
If you view the output that is generated, you'll see the scope attributes, the row header IDs and the headers attributes for the cells they apply to, as in this abbreviated example:
In most cases, there will be just two ID values in the headers attribute: the column and the row header cell IDs. However, you can add more if there are other headers that are applicable to the cell, for example if the cell spans more than one row or column. Also remember that you can add the abbr attribute to each column and row header if you want to include a more meaningful description of the column or row contents, just like we did in the GridView example. There you simply set the AccessibleHeaderText property of each column. When constructing HTML tables with a Table control, you would have to add the abbr attributes directly to the Attributes collection of each column header cell using something like MyHeaderCell.Attributes.Add("abbr","alternate-text-here").
Creating the Footer Row and Totals
Finally, the CreateTable routine calculates the total for each numeric column, and inserts these into cells that are created using the TableFooterCell control. However, again, the first value in the row is a row header, which is used to help identify the remaining cells that contain column totals. So the first cell is created using a TableHeaderCell, and has an ID added to it. In this case, the ID is simply "RowHeader_Total". Then the remaining cells have their AssociatedHeaderCellID property set using this value and the ID of the respective column header:
...
' create footer row
oRow = New TableFooterRow()
' first cell is a <th> "header" containing description of row
oCell = New TableHeaderCell()
oCell.ID = "RowHeader_Total" 'set ID for "header" cell
oCell.Controls.Add(New LiteralControl("Total"))
oCell.BorderStyle = BorderStyle.None
oRow.Cells.Add(oCell)
' remaining cells are data <td> elements
Dim dTotal As Decimal
For iCol = 1 To iColCount - 1
oCell = New TableCell()
' add HTML "headers" attributes to cell
oCell.AssociatedHeaderCellID = New String() _
{"ColumnHeader_" & oDT.Columns(iCol).ColumnName, "RowHeader_Total"}
' calculate and insert value
dTotal = 0
For Each oDataRow As DataRow In oDT.Rows
Try
dTotal += oDataRow(iCol)
Catch
End Try
Next
oCell.Controls.Add(New LiteralControl(dTotal.ToString()))
oCell.BorderStyle = BorderStyle.None
oCell.HorizontalAlign = HorizontalAlign.Center
oCell.Font.Bold = True
oRow.Cells.Add(oCell)
Next
MyTable.Rows.Add(oRow)
End Sub
The result for the footer row is that each cell except the row header (the first cell) contains the appropriate headers attribute:
Example 3: Adding alt and longdesc Attributes to an Image
The third example demonstrates two new properties for the existing and new ASP.NET controls:
The DescriptionUrl property is used with images to provide further explanation of the content and meaning of the image in non-visual page readers. It sets the longdesc attribute of the <img> element that is generated, which should be the URL of a page containing details of the image in text or even aural format. This property is available only on the Image control.
The GenerateEmptyAlternateText property causes the control to add the attribute alt="" (an empty string) to the element(s) it generates. Images that do not contribute to the meaning or content of the page, such as page divider images, graphical bullets, or "blank" images that are used to align or position other elements, should always carry this attribute. This property is available on the Image, ImageButton and ImageMap controls.
The code in the listing shown below (example1.aspx) declares three ASP.NET Image controls and sets the ImageUrl and ImageAlign properties so that a graphical "bullet" image is displayed followed by some explanatory text. Then for the first Image control, the AlternateText property is set to an empty string in an attempt to add the attribute alt="" to the <img> element that is generated. The second Image element has the GenerateEmptylternateText attribute added, which sets the GenerateEmptyAlternateText property of the control to True:
example3.aspx
<asp:Image id="img1" runat="server"
ImageUrl="bullet.gif"
ImageAlign="AbsMiddle"
AlternateText="" />
Image with AlternateText=""<p />
<asp:Image id="img2" runat="server"
ImageUrl="bullet.gif"
ImageAlign="AbsMiddle"
GenerateEmptyAlternateText="True" />
Image with GenerateEmptyAlternateText="True"<p />
<asp:Image id="Image1" runat="server"
ImageUrl="bullet.gif"
ImageAlign="AbsMiddle"
AlternateText="Sample Bullet Image"
DescriptionUrl="bullet.htm" />
Image with AlternateText="Sample Bullet Image"
and DescriptionUrl="bullet.htm"<p />
Although these two settings would appear to be equivalent, this is not the case. Open the page in your browser (See Figure 6) and then use the View|Source menu to examine the output that ASP.NET has generated. You'll see that the first <img> element has no alt attribute. This is because specifying an empty string for a property of the ASP.NET server controls causes them to remove the equivalent attribute from the element(s) they generate. Of course, as you'll expect, the second <img> element, where the GenerateEmptyAlternateText property was set to True, does have the required empty alt attribute (alt="").
Figure 6 - The page example3.aspx displayed in Internet Explorer
The third Image control demonstrates the use of the AlternateText property (which was available in version 1.x) and the new DescriptionUrl property. If you view the output generated for this control, you'll see that the <img> element contains the attribute longdesc="bullet.htm". Ordinary graphical Web browsers, such as Internet Explorer, ignore this attribute and provide no way for the user to access the page that it references.
In non-visual browsers and user agents, the result is - of course - completely different. For example, in the Lynx text browser, you'll see the following output for this page:
[bullet] Image with AlternateText=""
Image with GenerateEmptyAlternateText="True"
Sample Bullet Image Image with AlternateText="Sample Bullet Image" and
DescriptionUrl="bullet.htm"
For the first image, which has no alt attribute, Lynx takes its "best shot" and displays the image name without the file extension as [bullet], followed by the text placed in the page. However, where there is the alt="" attribute on the image, Lynx ignores it and just displays the text placed after this image. Finally, for the third image, it displays the value placed in the alt attribute - again followed by the text placed after it in the page.
However, specialist page readers should provide a link that the user can activate to view the page specified by the DescriptionUrl property. Figure 7 shows the IBM Home Page Reader displaying the example page. The central 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. You can see that the image placeholder (which displays the alternate text) for the third image is followed by a hyperlink that the user can activate to open the page bullet.htm.
Figure 7 - The page example3.aspx being read aloud by the IBM Home Page Reader
Figure 8 shows the page bullet.htm that opens from the hyperlink next to the third image that you can see in the page in Figure 7. This hyperlink is created from the longdesc attribute, and allows the user to view the extended description of the image.
Figure 8 - The page bullet.htm displayed in the IBM Home Page Reader
Example 4: Enhancements to the CheckBox Control
The last example in this series of two articles demonstrates an interesting new feature of the CheckBox control, which allows you to exert more control over the attributes that are added to the individual elements that the CheckBox control creates:
A CheckBox control can create three HTML elements, a <span> that contains a <label> and an <input> element. However, the <label> and <input> are not separate server controls and therefore cannot be accessed directly in ASP.NET. The new InputAttributes and LabelAttributes properties of the CheckBox control effectively allow you to access the Attributes collection of these two elements, allowing specific or custom attributes to be added or removed. These properties are available only on the CheckBox control.
The CheckBox control does not create the enclosing <span> element unless you specify a value for one or more properties that require this element to be present. Examples are properties that affect the appearance of the control (style and font properties), the text alignment, the Tooltip property, etc.
Several of the server controls in ASP.NET generate multiple HTML elements. Amongst these are the HyperLink control (when you specify an image for the ImageUrl property), and the CheckBox control. Setting many of the properties of these controls adds attributes to the "enclosing" element (the <a> in a HyperLink and the <span> for a CheckBox) and not to the "enclosed" elements (the <img> in a HyperLink and the <input> and <label> in a CheckBox). And, because the enclosed elements are not themselves server controls, they don't have an Attributes collection that you can manipulate directly.
While this isn't generally as issue, the resolution of this issue for the CheckBox control does allow you to exert finer control over its behavior. For example, the CheckBox control does not have a Title property that you would expect to use to set the pop-up tool tip and alternate text for the control, although you can add one declaratively as a pass-through title attribute. At runtime, however, you can now add title (and other) attributes to the enclosed <label> and <input> elements using the new InputAttributes and LabelAttributes properties of the CheckBox control.
The page example4.aspx shows how this is achieved. All the declaration of the CheckBox specifies is the Text property. The CheckBox control generates just the <input> and <label> elements, automatically adding the for attribute to the <label> element to link it to the <input> element. In the Page_Load event, the code then adds accesskey, title and tabindex attributes directly to the <label> and <input> elements.
example4.aspx
<form runat="server">
<asp:CheckBox id="MyCheck" runat="server"
Text="So<u>m</u>ething Important" />
</form>
...
<script runat="server">
Sub Page_Load()
MyCheck.LabelAttributes.Add("accesskey", "M")
MyCheck.LabelAttributes.Add("title", "Press ALT-M to update the checkbox setting")
MyCheck.InputAttributes.Add("tabindex", "0")
MyCheck.InputAttributes.Add("title", "Tick this box to signify something important") End Sub
</script>
The result, shown in Figure 9, is that the label and checkbox have different tool tips, and the accesskey attribute is set on the <label> element rather than the <input> element. OK, so the control does work fine without these additions, but it does serve to demonstrate how you can fine-tune the content to suit your page design and accessibility requirements.
Figure 9 - Adding attributes to the <label> and <input> elements for a CheckBox control
Summary
In this two-part article, I've tried to show you how easy it is to provide better accessibility in your pages for disadvantaged visitors. In particular, the users of non-graphical and non-visual user agents depend heavily on the extra features that you include in your pages, but which are not visible to users of the ubiquitous graphical browser such as Internet Explorer or Mozilla.
As well as being vital for disabled visitors, providing good accessibility is actually required by law in most countries of the world. While the origins of the laws might have been aimed at wheelchair ramps and wider doorways, they are being continually extended and applied to all areas of industry and commerce. You leave yourself open to challenges, and possible legal action, if you fail to provide the appropriate accessibility features on commercial Web sites. And, of course, you should be providing good access to all your visitors anyway, just because it is "the right thing to do"!
While considering the law and the general techniques (summarized in part one), this article specifically focuses on the enhancements to some of the server controls in ASP.NET that make it easier to add better accessibility features to your pages. We concentrated on the attributes specific to HTML tables, as generated by both the new GridView control and the ASP.NET Table control. You also looked at the use of alternate text for images and other controls, captions for tables and grids, plus hot keys and tab indexing for interactive and non-interactive controls.
Bear in mind that this article is not designed to be a primer for all the accessibility techniques that are available, or a reference to how you might implement them. Instead, it aims to convince developers as they move to ASP.NET version 2.0 that it's not hard to implement good accessibility, and it really is worth it!
About the Author
Alex Homer began his love-hate relationship with computers in 1980 with the Altair and Sinclair Z80. Since 1994 he has been working with and writing about various programming technologies - from databases to building Help systems. However with the growth of the Web, and particularly as ASP rapidly become a viable application platform, he concentrated almost entirely on this topic. Alex has written or contributed to more than 30 books on Web development topics for the major publishers, as well as producing articles for ASPToday, DevX and other sites. He is an MVP and a member of the INETA speaker's bureau, and speaks regularly on a range of Web development topics at conferences such as Microsoft PDC, Tech-Ed, ASP Connections, WebDevCon and VS.NET Live. In what spare time is left, he runs his own software and consultancy company Stonebroom Limited.
While the .NET Framework made building ASP.NET applications easier then it had ever been in the past, .NET 2.0 builds on that foundation in order to take things to the next level. This article shows you to how to construct an N-Tier ASP.NET 2.0 Web application by leveraging the new features of ASP.NET 2.0 and SQL Server 2005.
[Read This Article][Top]
With the release of ASP.NET 2.0, Microsoft has greatly increased the power of ASP.NET by introducing a suite of new features and functionalities. As part of this release, ASP.NET 2.0 also comes with a host of new special files and folders that are meant to be used to implement a specific functionality. This article examines these new files and folders in detail and provides examples that demonstrate how to utilize them to create ASP.NET 2.0 applications.
[Read This Article][Top]
Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server.
[Read This Article][Top]
Alex Homer continues his detailed look at the major changes to the DataSet class. In this part, he looks at two features that allow developers to work with data in a more structured and efficient way when using the DataSet with a SQL Server 2005 database server. [Read This Article][Top]
In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance. [Read This Article][Top]
In this article, Alex Homer looks at the changes between the version 1.x and version 2.0 DataSet and their associated classes, showing you how you can take advantage of the new features to improve your applications' capabilities and performance. [Read This Article][Top]
In ASP.NET 2.0 and Visual Studio 2005, you can quickly program custom authentication pages with the provided Membership Login controls. In this article, Dina Fleet Berry examines the steps involved in using the Login control with a custom SQL Server membership database.
[Read This Article][Top]
In this article, Thiru Thangarathinam examines .NET 2.0's new ClickOnce deployment technology that is designed to ease deployment of Windows forms applications. This new technology not only provides an easy application installation mechanism, it also eases deployment of upgrades to existing applications. [Read This Article][Top]
With ASP.NET 2.0, Microsoft has made great strides in increasing developer productivity and has made implementing previously complex solutions relatively easy. Where this version of ASP.NET really shines, however, is in its new administrative tools that allow developers to spend less time managing the configuration of the servers and software and more time developing great code.
[Read This Article][Top]
Thiru Thangarathinam introduces ASP.NET 2.0's new TreeView control which provides a seamless way to consume and display information from hierarchical data sources. The article discusses this new control in depth and explains how to use this feature rich control in your ASP.NET applications. [Read This Article][Top]
Mailing List
Want to receive email when the next article is published? Just Click Here to sign up.