生成可访问的 ASP.NET Web 站点
遵守公共 Web 标准的好处是:它们使您以尽可能少地工作,使尽可能多的人能访问您的 Web 页。需要指出的是,可访问性标准使您能够生成可被残疾人士更容易访问的 Web 站点。
同样要强调的是,Web 站点用户中的相当一部分具有这样或那样的残疾。请试想一下您自己的家庭成员,并且考虑他们中有多少人会在与 Web 页交互时遇到麻烦。我就有一些上了年纪的亲戚是盲人或者失去了动作协调性。我猜测,本文的很多读者也有上了年纪的父母或祖父母,他们在使用大多数 Web 站点时会遇到很多困难。
生成可访问的 Web 站点有很多充分的理由:财务、道德、法律等等。但是,我们将重点讨论法律动机。在美国,按照康复法案 (Rehabilitation Act) 508 节的要求,联邦政府机构开发的任何 Web 站点都要使残疾人士可以访问。这一法律适用于联邦政府机构以及与联邦政府机构订立合同的公司(请参阅 http://www.section508.gov)。
其他国家/地区也具有类似的要求。例如,在加拿大,Treasury Board Common Look and Feel Standards 要求联邦政府机构开发的 Web 站点都要使残疾人士可以访问。在澳大利亚,Disability Discrimination Act 要求澳大利亚服务器上承载的所有 Web 站点(无论其是否是政府的 Web 站点)都要使残疾人士可以访问。(有关可访问性法律的详细信息,请参阅 http://www.w3.org/WAI/Policy。)
在我知道的 Web 站点开发人员中,没有任何人会故意生成残疾人士无法访问的 Web 站点。问题在于大多数开发人员都不熟悉各种可访问性标准。
在本文的下列各节中,将概述以下两个最重要的可访问性标准:WCAG 和 508 节标准。您还将学习如何通过使用 ASP.NET 控件生成可访问的 Web 页。最后,您将学习如何“验证”Web 页的可访问性。
可访问性标准
几乎所有可访问性标准和法律都源自 W3C Web Content Accessibility 1.0 Guidelines (WCAG)。这些准则是由 World Wide Web Consortium 在 1999 年 5 月 5 日作为推荐标准首次发布的(请参阅 http://www.w3.org/TR/WCAG10)。
WCAG 包含 14 条准则。每一条准则又包含一个或多个进一步阐明该准则的检查点。每个检查点都按 1 到 3 的优先级进行分级。为使这些准则的实施变得更加容易,W3C 已经发布了一组文档,其中包含有关如何遵守这些准则的技术(请参阅 http://www.w3.org/TR/WCAG10-TECHS/)。
您可以要求不同程度地遵守 WCAG 准则。如果要求 Web 站点满足所有优先级 1 的检查点,则您可以显示具有 Conformance Level A 的标识语。当 Web 站点满足所有优先级 1 和 2 检查点时,该 Web 站点可以显示 Conformance Level Double-A 标识语。最后,满足所有检查点的 Web 站点可以显示 Conformance Level Triple-A 标识语(请参阅 http://www.w3.org/WAI/WCAG1-Conformance.html)。
508 节准则源自 WCAG 准则。在美国,联邦政府机构(以及与联邦政府机构订立合同的公司)需要对这组准则给予最高程度的关注,因为这些准则具有法律效力。您可以在 508 节 Web 站点读到 508 节准则的完整文本。
ASP.NET 2.0 框架旨在使您能够满足所有 WCAG 优先级 1 和优先级 2 的检查点以及所有 508 节准则。这些准则使用起来非常严格。每个使用 ASP.NET 2.0 框架的开发人员都需要检查和测试每个 ASP.NET 控件的可访问性。而且,每个开发人员都应该在桌面上安装一个屏幕阅读器,以便针对这些准则测试页。
ASP.NET 2.0 中的可访问性改进
本文重点讨论 ASP.NET 2.0 框架中六个方面的可访问性改进。在下列各节中,您将学习如何使用 ASP.NET 控件来显示可访问的图像、表单、导航、数据和 XHTML。在本节结尾,我们还将考虑与在 ASP.NET 页中使用客户端脚本有关的可访问性问题。
创建可访问的图像
您不应当做这样的假设:与 Web 站点交互的每个人都可以实际看到您的 Web 站点。如果某个人是盲人或者视力不足,那么这个人就可能需要使用屏幕阅读器或盲文显示器来访问您的 Web 页。屏幕阅读器通过使用语音合成器来朗读 Web 页中的文本。盲文显示器将页中的文本转换为盲文表示。
对于无法看到它们的人而言,图像和其他非文本页元素(例如,Java、Shockwave 和 Flash 内容)都是没有用的。如果您希望使 Web 站点对于盲人或弱视的人是可访问的,那么需要为 Web 页中的所有非文本内容提供文本等效内容。
Web 页中的每个图像都应该包含 alt 属性。alt 属性用来表示由屏幕阅读器或其他辅助性设备阅读的替换文本。以下是 alt 属性的使用方式。
<img src="Products23.gif" alt="Image of Products" />
alt 属性应当包含图像的说明。在任何情况下,它都不应该只是包含该图像的文件名。alt 属性的目的是为盲人和视力正常的人传递相同的图像信息。写入 alt 属性的值时要求对该元素的含义进行人工解释。因此,不能自动完成创建 alt 属性的过程。
每个显示图像的 ASP.NET 控件都包含用于为该图像提供替换文本的方法。例如,ASP.NET 图像控件包含 AlternateText 属性。如果使用 Image 控件,则需要将 AlternateText 属性设置为有意义的值。
<asp:Image ImageUrl="Products23.gif" AlternateText="Image of Products" Runat="Server" />
如果某个图像只是用作设计元素,则应当将它的 alt 属性设置为空字符串。如果图像不具有需要传达的有用信息,那么就没有理由使屏幕阅读器的页解说变得散乱。
<img src="PageDivider.gif" alt="" />
在 ASP.NET 2.0 框架中,必须采取特殊措施,使您能够呈现空的 AlternateText。如果将空文本分配给 ASP.NET 控件的属性,则 ASP.NET 控件将根本不会呈现该属性。例如,假设将以下 ASP.NET Image 控件添加到页中。
<asp:Image ImageUrl="PageDivider.gif" AlternateText="" Runat="Server" />
在这种情况下,会呈现以下标记。
<img src="PageDivider.gif" style="border-width:0px;" />
请注意,alt 属性消失了。这是所有 ASP.NET 控件属性的默认行为。当没有为属性分配值时,该属性不会呈现出来。遗憾的是,在这种情况下,我们确实希望为 alt 属性呈现空值。
为了解决该问题,ASP.NET 2.0 框架中引入了以下新属性,使您能够用 Image 控件显示空替换文本:GenerateEmptyAlternateText 属性。
<asp:Image ImageUrl="PageDivider.gif" GenerateEmptyAlternateText="true" Runat="Server" />
如果使用 GenerateEmptyAlternateText 属性,则会正确地呈现 alt="" 属性。
当图像表示某个真正复杂的事物(例如,组织结构图)时,不能使用 alt 属性来提供替换文本说明。当需要提供较长的图像含义说明时,需要使用 longdesc 属性。
longdesc 属性接受相对或绝对 URL 作为它的值。该 URL 应当链接到包含图像内容的文本说明的页。以下示例说明如何将该属性与 <img> 标记配合使用。
<img src="OrgChart.gif" alt="Company Organization Chart" longdesc="/OrgChartDescription.aspx" />
ASP.NET Image 控件包含一个名为 DescriptionUrl 的属性,它对应于 HTML longdesc 属性。以下示例说明如何使用该属性。
<asp:Image ImageUrl="OrgChart.gif"
AlternateText="Company Organization Chart" DescriptionUrl="/OrgChartDescription.aspx" Runat="server" />
创建可访问的表单
Web 页表单可能给视力低下的人士以及缺乏动作协调性的人士带来问题。如果通过屏幕阅读器访问 Web 页表单,那么可能很难将表单域与其相应的标签相关联。例如,假设 Web 页包含以下表单。
<table>
<tr>
<td>First Name:</td>
<td><input name="txtFirstName" /></td>
</tr>
<tr>
<td>Last Name:</td>
<td><input name="txtLastName" /></td>
</tr>
</table>
该表单显示一个人的名和姓的输入域。在这种情况下,因为该表单显示在表中,所以屏幕阅读器用户可能很难将正确的标签与正确的表单域相关联。HTML 4.0 中引入以下新标记,以使您能够将表单域标签与表单域相关联:<label> 标记。下面说明如何使用 <label> 标记编写前面的表单。
<table>
<tr>
<td><label for="txtFirstName">First Name:</label></td>
<td><input name="txtFirstName" id="txtFirstName" /></td>
</tr>
<tr>
<td><label for="txtLastName">Last Name:</label></td>
<td><input name="txtLastName" id="txtLastName" /></td>
</tr>
</table>
<label> 标记将表单域标签与其相应的表单域显式关联起来。请注意,<input> 域包含 id 属性,这是因为 for 属性的值必须是输入域的 id 属性而不是 name 属性。
通常,ASP.NET Label 控件生成 <span> 标记。但是,如果您在声明 ASP.NET Label 控件时提供了 AssociatedControlId 属性,则该控件会呈现 <label> 标记。下面说明如何用 ASP.NET Label 和 TextBox 控件生成可访问的表单。
<table>
<tr>
<td><asp:Label AssociatedControlID="txtFirstName"
runat="server">First Name:</asp:Label></td>
<td><asp:TextBox ID="txtFirstName" runat="server" /></td>
</tr>
<tr>
<td><asp:Label AssociatedControlID="txtLastName"
runat="server">Last Name:</asp:Label></td>
<td><asp:TextBox ID="txtLastName" runat="server" /></td>
</tr>
</table>
在为 ASP.NET 控件提供标签时,应当使用 ASP.NET Label 控件,而不是 HTML <label> 标记。在将一个 ID 分配给 ASP.NET 控件(如 TextBox 控件)时,呈现到浏览器中的 ID 可能与您分配给该控件的 ID 不同。因此,如果使用 <label> 标记,则 <label> 标记中的 ID 可能与所呈现的 TextBox 控件的 ID 不匹配。另一方面,如果使用 ASP.NET Label 控件,则不必担心该问题。
ASP.NET CheckBox、RadioButton、CheckBoxList 和 RadioButtonList 控件自动呈现 <label> 标记。在使用这些控件时,请小心使用 Text 属性来标记控件的文本。您不应该执行以下操作。
<asp:CheckBox Runat="Server" /> Include Gift Wrap
相反,请执行以下操作。
<asp:CheckBox Text="Include Gift Wrap" Runat="Server" />
对于通过屏幕阅读器与 Web 页进行交互的用户,大型表单也可能产生问题。在聆听大型表单的内容时,很容易忘记正在聆听该表单的哪个部分。在显示大型表单时,将该表单划分为多个小块是一个好主意。您可以通过使用 <fieldset> 标记将单个表单划分为多个部分。以下示例说明如何使用该标记。
<form id="form1" runat="server">
<div>
<fieldset>
<legend>Contact Information</legend>
... form fields
</fieldset>
<fieldset>
<legend>Payment Information</legend>
... form fields
</fieldset>
</div>
</form>
该表单通过 <fieldset> 标记划分为两个子表单。<legend> 标记用来标记这些子表单的用途。在 Internet Explorer、Firefox 和 Opera 中显示时,这些子表单被边框直观地划分为多个单独的区域(参见图 5)。但是,重要的是要记住,<fieldset> 标记的主要用途是实现可访问性。如果不喜欢 <fieldset> 标记的可视化外观,那么可通过样式表规则修改该标记的外观,或通过使用 CSS display 或 visibility 属性将该标记完全隐藏。
图 5.
标记
视力低下的人士并不是 Web 页用户中唯一可能觉得 Web 表单难以访问的人。那些缺乏动作协调性的用户也会在与表单交互时遇到困难。
在生成 Web 表单时,为每个表单域包含 accesskey 和 tabindex 属性总是一个好主意。accesskey 属性使无法使用鼠标的用户能够直接导航到任何表单域。tabindex 属性使您能够控制表单域的 Tab 键顺序。对于那些必须通过键盘(或像键盘一样操作的辅助性设备)与页面进行交互的用户而言,这两个属性可使其生活变得更方便一些。
下面是一个同时使用 accesskey 和 tabindex 属性的示例表单。
<asp:Label
AssociatedControlID="txtFirstName"
AccessKey="f"
runat="server"><u>F</u>irst Name</asp:Label>
<asp:TextBox
id="txtFirstName"
TabIndex="1"
Runat="server" />
<br />
<asp:Label
AssociatedControlID="txtLastName"
AccessKey="l"
runat="server"><u>L</u>ast Name</asp:Label>
<asp:TextBox
id="txtLastName"
TabIndex="2"
Runat="server" />
tabindex 属性用来控制表单域的 Tab 键顺序。因为第一个表单域具有的 tabindex 值为 1,所以当用户第一次按 Tab 键时,该页中任何出现在该表单之前的其他元素都被跳过。
在使用 Internet Explorer 或 Firefox 时,按 ALT+F 可自动将焦点移至 First Name 文本框。如果按 ALT+L,则焦点会自动移至 Last Name 文本框。在使用 Opera 时,必须首先按 SHIFT+ESC,然后才能选择访问键。
请注意,First Name 和 Last Name 标签的第一个字母都带有下划线。通过为字母添加下划线,可以为 Web 站点的用户提供访问键的直观表示。这是在 Microsoft Windows 应用程序中标记访问键的标准方式。但是,还有其他在表单中指示访问键的推荐方法(请参阅 http://www.cs.tut.fi/~jkorpela/forms/accesskey.html)。
使用下划线指示访问键的一个问题是无法为按钮中的字符添加下划线,并且超链接已经带有下划线。例如,下面的 Button 控件并不像您预期和希望的那样工作。
<asp:Button
Text="<u>S</u>ubmit"
Runat="server" />
在呈现该 ASP.NET Button 控件时,会显示实际文本 Submit,而不是显示带下划线的<u>S</u>字符。ASP.NET Button 控件呈现 HTML<input type="submit"> 标记,但遗憾的是, <input type="submit">标记不支持下划线。
您可能认为可以通过使用样式规则解决该问题。遗憾的是,当前不存在以下这种跨浏览器兼容方法:即,使用层叠样式表为 <input type="submit">标记中的单个字符加下划线。
如果您愿意在页中使用客户端 JavaScript,则可以解决该问题。清单 4 中的页包含的 JavaScript 根据是否按下 ALT 键而显示或隐藏所有访问键。当按下 ALT 键时,会弹出一个框,显示访问键键盘组合键(参见图 6)。该脚本在 Internet Explorer 和 Firefox 中都能够正常工作(Opera 不使用 ALT 键来选择访问键)。
图 6. AccessKeys.aspx
清单 4. AccessKeys.aspx
<%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
<title>Contact Form</title>
<style type="text/css">
.accessKey
{
display:none;
position:absolute;
z-index:5000;
padding:3px;
border:solid 1px black;
background-color: #ffffe0
}
</style>
<script type="text/javascript">
/* <![CDATA[ */
window.onload = function()
{
document.onkeydown = displayAccessKeys;
}
function displayAccessKeys(e)
{
if (!e) e = window.event;
if (e.keyCode == 18)
{
toggleAccessKeys();
document.onkeydown = null;
document.onkeyup = hideAccessKeys;
}
}
function hideAccessKeys(e)
{
if (!e) e = window.event;
if (e.keyCode == 18)
{
toggleAccessKeys();
document.onkeyup = null;
document.onkeydown = displayAccessKeys;
}
}
function toggleAccessKeys()
{
var spans = document.getElementsByTagName('span');
for (var k=0;k<spans.length;k++)
if (spans[k].className == 'accessKey' )
{
if ( 'inline' != spans[k].style.display)
spans[k].style.display = 'inline';
else
spans[k].style.display = 'none';
}
}
/* ]]> */
</script>
</head>
<body>
<form id="form1" runat="server">
<div>
<table>
<tr>
<td>
<asp:Label
ID="lblFirstName"
AssociatedControlID="txtFirstName"
AccessKey="f"
runat="server">First Name</asp:Label>
</td>
<td>
<asp:TextBox ID="txtFirstName" runat="server" />
<span class="accessKey">access key is f</span>
</td>
</tr>
<tr>
<td>
<asp:Label
ID="lblLastName"
AssociatedControlID="txtLastName"
AccessKey="l"
runat="server">Last Name:</asp:Label>
</td>
<td>
<asp:TextBox ID="txtLastName" runat="server" />
<span class="accessKey">access key is l</span>
</td>
</tr>
<tr>
<td colspan="2">
<asp:Button Text="Submit" runat="server" />
<span class="accessKey">access key is s</span>
</td>
</tr>
</table>
</div>
</form>
</body>
</html>
清单 4 中的页包含样式表和客户端 JavaScript。样式表隐藏了由 accessKey 类标识的任何 <span> 标记的内容。JavaScript 会在 ALT 键已经被按下时进行检测,并且显示 <span> 标记的内容。
请注意,即使 Web 浏览器中禁用了样式表和 JavaScript,该页也能够正常工作。在这种情况下,将总是显示访问键帮助(参见图 7)。
图 7. AccessKeys.aspx 适度降格
创建可访问的导航
我讨厌拨打客户支持电话并按照自动系统的指示操作。当计算机语音系统用其低沉单调的声音宣布每个选项时,我感觉自己正在慢慢地变老。如果按错了一个键,那么您就会永远迷失在自动计算机系统深不可测的迷宫中。
遗憾的是,如果您被迫使用屏幕阅读器,那么这正是您在访问几乎任何 Web 页时的体验。大多数 Web 站点都在每一页中包含一个导航栏,其中包含指向 Web 站点各个部分的链接列表。如果使用屏幕阅读器,则每当您打开页时,都必须逐个聆听这些导航链接中的各个链接。
通过对导航栏进行一处简单的修改,就可显著提高 Web 页的可访问性。只需添加一个用来跳过所有导航链接的方法。可以用“跳过导航”链接完成该工作。
例如,CNN Web 站点包含一个导航栏,其中列出了 CNN Web 站点的不同部分(World、U.S.、Weather 等等)。但是,CNN Web 站点的设计人员已经做了一件聪明的事情。如果查看页的源代码,则您将注意到导航栏上会出现以下链接。
<a HREF="#ContentArea" TARGET="_self">
<img src="http://i.cnn.net/cnn/images/1.gif" alt="Click here to skip to main content."
width="10" height="1" border="0" align="right"></a>
当查看 CNN Web 站点的主页时,您绝对不会看到该链接。该链接中包含的图像是一幅透明的单像素图像。但是,如果您用屏幕阅读器访问该页,则会阅读与该图像相关联的替换文本。盲人可以选择跳过所有导航链接,并直接移至 Web 页的主要内容区域(这与在自动语音系统中按 0 并直接导航到话务员等效)。
“跳过导航”链接已经被集成到多个标准 ASP.NET 2.0 控件中。特别需要指出的是,Menu、TreeView、SiteMapPath、Wizard 和 CreateUserWizard 控件全都支持“跳过导航”链接。
例如,清单 5 中的页包含 ASP.NET Menu 控件。该控件用来显示指向该 Web 站点中其他页的链接列表。
清单 5. SiteMenu.aspx
<%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Skip Navigation</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Menu
id="Menu1"
Runat="server">
<Items>
<asp:MenuItem Text="Home" NavigateUrl="Home.aspx" />
<asp:MenuItem Text="Products" NavigateUrl="Products.aspx" />
<asp:MenuItem Text="Services" NavigateUrl="Services.aspx" />
<asp:MenuItem Text="About" NavigateUrl="About.aspx" />
</Items>
</asp:Menu>
<hr />
Here is the main content of the page...
</div>
</form>
</body>
</html>
如果查看清单 5 中页的源代码,您将看到以下链接出现在菜单顶部。
<a HREF="#Menu1_SkipLink" TARGET="_self"><img alt="Skip Navigation Links"
src="/WebResource.axd?d=ChXz41GuDxNm-7TcWyCl_w2&t=632495684475122400"
width="0" height="0" style="border-width:0px;" />
该链接包含一幅在您查看该页时不会出现的宽和高皆为零的图像。但是,通过屏幕阅读器访问该页的用户可以选择“跳过导航”链接跳到该菜单的结尾。
默认情况下,“跳过导航”链接包含文本 Skip Navigation Links。可以通过更改 Menu 控件的 SkipLinkText 属性修改该值。
创建可访问的数据
ASP.NET 2.0 框架包含一组丰富的、用于显示数据库数据的控件。这些控件包括 GridView、DetailsView、DataList、FormView 和 Repeater 控件。默认情况下,GridView、DetailsView 和 DataList 控件在 HTML 表中显示数据库记录。
在 HTML 表中呈现信息时,如果操作错误,则可能引起可访问性问题。在聆听 HTML 表的内容时,您很容易忘记自己当前在该表中的位置。例如,假设您使用 HTML 表显示一个产品信息列表。在聆听由屏幕阅读器朗读的表内容时,您很容易将某个表单元格所代表的信息搞混,不知道它们是有关产品名称的,还是有关所订购产品数量的,抑或是有关存储这些产品的仓库的代码。
在查看 HTML 表时,可通过扫视列或行标题来确定特定单元格的含义。为使表对于使用屏幕阅读器的用户是可访问的,需要显式标记表标题并将这些标题与各个单元格显式关联起来。
在创建表以显示数据时,应当始终使用正确的标记来标记列和行标题。表标题应当总是用 <th> 标记进行标记,如下所示。
<table>
<thead>
<tr>
<th>Product Name</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>
在该示例中,<th> 标记用来标记以下两个列标题:Product Name 和 Price。
一些设计人员避免使用 <th> 标记,因为他们不喜欢它的默认可视化外观。在大多数浏览器中,<th> 标记的内容居中并且加粗。但是,需要记住的是,标记绝不应当用来控制表示形式。如果您希望列标题看起来像标准的表单元格,则您应当添加如下所示的样式规则。
<style type="text/css"> th {text-align:left;font-weight:normal} </style>
为了使表可访问,还应当显式指明与各个单元格相关联的一个或多个标题。您可以将多个属性用于此目的:scope、headers 和 axis。
scope 属性可用来指示表标题是列标题还是行标题。例如,下面的表同时包含列标题和行标题,它们都通过使用 scope 属性的 <th> 标记进行标记。
<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>
该表包含 Boston 地铁 Red Line 的时间表(参见图 8)。请注意,每个列标题都包含 scope="col" 属性,而每个行标题都包含 scope="row" 属性。
图 8. 简单的地铁时间表
scope 属性非常适合于简单的表。但是,对于更为复杂的表,需要使用 headers 属性。例如,嵌套表可能有三个或更多的标题与单个单元格相关联。headers 属性使您能够用与各个单元格相关联的标题来标记它。
axis 属性使您能够对表标题进行分类。例如,在地铁时间表中,可以将属性 axis="location" 添加到每个表示位置的标题(Alewife 和 Braintree 标题)中。axis 属性接受由逗号分隔的类别列表。
清单 6 中的页包含一个更复杂版本的 Boston 地铁时间表,它同时使用了 headers 和 axis 属性(参见图 9)。
图 9. 复杂的地铁时间表
清单 6. Subway.aspx
<%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head 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>
请注意,每个表单元格都包含 headers 属性。headers 属性表示与列和行标题相对应的 ID 的空格分隔列表。地铁时间表中的每个单元格都具有相关联的位置、日期和列车标题。
同时,请注意,每个 <th> 标记都具有一个 axis 属性,用于表示与标题相关联的类别。例如,Weekday 和 Saturday 标题都与 day 轴相关联。First Train 和 Last Train 标题与 train 轴相关联。
最后,请注意清单 6 中的表同时包含 summary 属性和 标记。summary 属性的工作方式非常类似于 alt 属性。您可以使用 summary 属性提供浏览器未呈现的表的说明。另一方面,浏览器呈现 标记的内容。您应当使用 标记来标识表的用途。
如果使用 ASP.NET 2.0 GridView 或 DetailsView 控件来显示 HTML 表中的数据库数据,则默认情况下,生成的 HTML 表是可访问的。例如,清单 7 包含一个 ASP.NET 页,它通过使用 GridView 控件来显示 Titles 数据库表的内容。
清单 7. DisplayTitles.aspx
<%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Display Titles</title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:GridView
id="grdTitles"
DataSourceId="srcTitles"
Runat="server" />
<asp:SqlDataSource
id="srcTitles"
ConnectionString=
"Server=localhost;Trusted_Connection=true;Database=Pubs"
SelectCommand="Select * FROM Titles"
Runat="server" />
</div>
</form>
</body>
</html>
在清单 7 中,GridView 控件被绑定到一个表示 Titles 数据库表中记录的 SqlDataSource 控件。在浏览器中打开清单 7 中的 ASP.NET 页时,Titles 数据库表的内容显示在 HTML 表中(参见图 10)。
图 10. DisplayTitles.aspx
请注意,GridView 控件自动为每个列标题生成 <th> 标记。而且,如果在浏览器中选择 View Source,则可以看到为每个列标题自动生成 scope="col" 属性。
GridView 控件支持其他多个与可访问性相关的属性:
| • |
Caption 和 CaptionAlign — 使用这些属性可以向 GridView 控件生成的 HTML 表中添加标题。 |
| • |
RowHeaderColumn — 使用该属性可以指示行标题(相对于列标题而言)。请将该属性设置为从数据源返回的列的名称(例如,title_id)。 |
| • |
UseAccessibleHeader — 使用该属性可以指示是否应当用 或 <td> 标记呈现列标题。默认情况下,该属性具有值 true。 |
请注意,GridView 控件不具有 Summary 属性。但是,像大多数 ASP.NET 控件一样,GridView 控件支持 expando 属性。当您声明 GridView 控件时,您可以声明喜欢的任何属性,而该属性将被呈现到浏览器中。因此,如果您希望向 GridView 中添加摘要,则请按如下方式声明 summary 属性。
<asp:GridView
id="grdTitles"
DataSourceId="srcTitles"
summary="Displays the contents of the Titles database table"
Runat="server" />
GridView 控件的默认行为非常适合于以可访问的方式显示简单数据表。但是,如果您需要显示更为复杂的表(例如,一组嵌套表),则您必须完成额外的工作。
例如,您可能希望显示产品类别的列表,并且希望在每个类别下显示匹配产品的列表。换句话说,您希望创建单页“主要信息/详细信息”表单(参见图 11)。在这种情况下,需要为每个表单元格包含 headers 属性。
图 11. 嵌套 Repeater 控件
清单 8 中的页说明如何将一个 Repeater 控件嵌套到另一个 Repeater 控件中,以及如何生成符合可访问性准则要求的复杂表。
清单 8. NestedRepeaters.aspx
<%@ 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 dtblProducts As New DataTable
Sub Page_Load()
Dim dad As New SqlDataAdapter("SELECT * FROM PRODUCTS", _
"Server=localhost;Trusted_Connection=true;Database=Northwind")
dad.Fill(dtblProducts)
End Sub
Function GetProducts(ByVal CategoryID As Integer) As DataView
Dim view As DataView = dtblProducts.DefaultView
view.RowFilter = "CategoryID=" & CategoryID
Return view
End Function
Function GetCategoryHeader(ByVal index As Integer) As String
Return "hdrCategory" & index.ToString()
End Function
Function GetProductHeader(ByVal productID As Integer) As String
Return "hdrProduct" & productID.ToString()
End Function
Function GetHeaders(ByVal item As RepeaterItem, _
ByVal columnHeader As String) As String
Dim parent As RepeaterItem = _
item.NamingContainer.NamingContainer
Dim categoryHeader As String = _
GetCategoryHeader(parent.ItemIndex)
Dim productHeader As String = _
GetProductHeader(item.DataItem("ProductID"))
Return String.Format("{0} {1} {2}", categoryHeader, _
productHeader, columnHeader)
End Function
</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Untitled Page</title>
<style type="text/css">
.categoryRow {background-color:yellow}
</style>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Repeater
id="grdCategories"
DataSourceId="srcCategories"
Runat="server">
<HeaderTemplate>
<table>
<thead>
<th id="hdrID">ID</th>
<th id="hdrName">Name</th>
<th id="hdrPrice">Price</th>
</thead>
<tbody>
</HeaderTemplate>
<ItemTemplate>
<tr class="categoryRow">
<th colspan="3"
id='<%# GetCategoryHeader(Container.ItemIndex) %>'>
<%# Eval("CategoryName") %>
</th>
</tr>
<asp:Repeater
id="grdProducts"
DataSource='<%# GetProducts(Eval("CategoryID")) %>'
Runat="server">
<ItemTemplate>
<tr>
<th
id='<%# GetProductHeader(Eval("ProductID")) %>'>
<%# Eval("ProductID") %>
</th>
<td
headers='<%# GetHeaders(Container, "hdrName") %>'>
<%#Eval("ProductName")%>
</td>
<td headers=
'<%# GetHeaders(Container, "hdrPrice") %>'>
<%#Eval("UnitPrice", "{0:c}")%>
</td>
</tr>
</ItemTemplate>
</asp:Repeater>
</ItemTemplate>
<FooterTemplate>
</tbody>
</table>
</FooterTemplate>
</asp:Repeater>
<asp:SqlDataSource
id="srcCategories"
ConnectionString=
"Server=localhost;Trusted_Connection=true;Database=Northwind"
SelectCommand="SELECT * FROM Categories"
Runat="server" />
</div>
</form>
</body>
</html>
在清单 8 中,外层的 Repeater 控件用来列出产品类别,而内层的 Repeater 控件用来列出匹配产品。下列两个 Helper 函数用来生成 Category Name 和 Product ID 标题的 id 值:GetProductHeader 和 GetCategoryHeader 函数。另外一个单独的名为 GetHeaders 的 Helper 函数用来生成用于 headers 属性的值。
清单 8 中的 ASP.NET 页生成如下所示的 HTML 表。
<table>
<thead>
<th id="hdrID">ID</th>
<th id="hdrName">Name</th>
<th id="hdrPrice">Price</th>
</thead>
<tbody>
<tr class="categoryRow">
<th colspan="3" id='hdrCategory0'>
Beverages
</th>
</tr>
<tr>
<th id='hdrProduct1'>
1
</th>
<td headers='hdrCategory0 hdrProduct1 hdrName'>
Chai 2
</td>
<td headers='hdrCategory0 hdrProduct1 hdrPrice'>
$18.55
</td>
</tr>
<tr>
<th id='hdrProduct2'>
2
</th>
<td headers='hdrCategory0 hdrProduct2 hdrName'>
Chang
</td>
<td headers='hdrCategory0 hdrProduct2 hdrPrice'>
$19.00
</td>
</tr>
.... remainder of the table
请注意,每个 <td> 标记都包含适当的 headers 属性。
创建可访问的 XHTML
很多可访问性准则共有的一个主题是这样一个概念 — 即,Web 页应当符合标准,这样才能成为可访问的页。按照准则,您应当努力使用最新的 W3C 标准(例如,最新版本的 XHTML 和层叠样式表)来生成 Web 站点。
特别需要指出的是,在设计 Web 页时,您应当将文档的结构与它的表示形式分开。请使用标记来表示 Web 页的结构,并且使用层叠样式表来控制 Web 页的外观。
例如,绝不要仅仅使用 <blockquote>元素来缩进文本块。<blockquote> 元素的用途是创建原文的引文。如果您希望缩进文本,则应当改而使用层叠样式表 margin 属性。
您还应当只在表示数据表时使用 <table> 标记。尽管使用 <table> 标记来对 Web 页面进行布局在当前是一种常见的做法,但是,请尽可能改而使用 <div> 标记。例如,清单 9 中的页具有三列式布局,但是不包含一个 <table> 标记(参见图 12)。
图 12. 不含表的页布局
清单 9. Tableless.aspx
<%@ Page Language="VB" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
<title>Tableless Layout</title>
<style type="text/css">
#content
{
margin-left:auto;
margin-right:auto;
width:800px;
}
#leftColumn
{
float:left;
width:150px;
border:1px solid black;
padding:10px;
}
#middleColumn
{
float:left;
width:430px;
padding:10px;
}
#rightColumn
{
float:right;
width:150px;
border:1px solid black;
padding:10px;
}
</style>
</head>
<body>
<form id="form1" runat="server">
<div id="content">
<div id="leftColumn">
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
Left column contents...
</div>
<div id="middleColumn">
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
Middle column contents...
</div>
<div id="rightColumn">
Right column contents...
Right column contents...
Right column contents...
Right column contents...
Right column contents...
Right column contents...
Right column contents...
Right column contents...
</div>
</div>
</form>
</body>
</html>
清单 9 中的页包含四个 <div> 标记。第一个 <div> 标记(名为 content)用来指定页的内容区域的宽度。其余三个 <div> 标记(分别名为 left、middle 和 right)将内容区域划分为三列。该页可以在 Internet Explorer 6、Firefox 和 Opera 8 中正确显示(要查看一些不使用 HTML 表创建布局的真正漂亮的页面,请参见 http://csszengarden.com。)
WCAG 准则认为,不可能总是避免使用 <table> 标记来创建页布局,因为较旧的浏览器不完全支持层叠样式表标准(请参阅 WCAG 准则 5)。在无法避免使用表创建布局的情况下,您应该确认这些表的内容在进行线性化(即,按照表-单元格顺序来阅读)时是有意义的。
因为 ASP.NET 框架必须与旧式和新式浏览器同时兼容,所以一些 ASP.NET 控件实际上确实使用 <table> 标记来创建布局。例如,ASP.NET 2.0 Login 控件使用 <table> 标记来控制用户名和密码输入字段的布局。
创建可访问的脚本
WCAG 和 508 节准则中包含的一个非常严格的限制与客户端脚本有关。根据 WCAG 1.0 准则中的优先级 1 检查点,要求:
6.3 确保页在脚本、小程序或其他编程对象关闭或不受支持时是可用的。如果这是不可能的,请在可访问的替换页中提供等效信息。[优先级 1]
508 节准则包含类似的要求:
(l) 当页利用脚本语言来显示内容或者创建界面元素时,应该用可通过辅助性技术阅读的功能性文本标识由该脚本提供的信息。
问题在于,多个 ASP.NET 控件要求具有客户端 JavaScript 才能正常工作。这方面的主要示例是 ASP.NET LinkButton 控件。LinkButton 控件使用 JavaScript 将包含该控件的表单提交给 Web 服务器。
该问题没有很好的解决方案。如果需要生成能够满足所有可访问性准则的 Web 站点,则需要非常小心地使用客户端脚本。您可能需要避免使用某些依赖于 JavaScript 的 ASP.NET 控件,例如 LinkButton 控件。
遗憾的是,在生成现代 Web 站点时,很难遵守该准则。这种假定似乎使得 Web 站点更像杂志而不是应用程序。现代 Web 站点倾向于包含动态的客户端内容。例如,很多房地产 Web 站点包含一个 JavaScript 按揭计算器。人们尚不清楚 JavaScript 按揭计算器的文本等效物应该是什么。
验证页的可访问性
与存在 XHTML 的完全自动化验证程序不同,并不存在完全自动化的可访问性验证程序。之所以不存在可访问性的自动化验证程序,原因在于判断页的可访问性需要人工解释。
例如,为了使 Web 页可访问,该页中的每个图像都必须包含有意义的替换文本。目前,没有任何计算机能够确定一段文本是否具有与图像相同的含义。可访问性验证程序最多只能提供应该检查的事物的列表。
Visual Studio .NET 2005(但不是 Visual Web Developer)包含可访问性检查器。可从工具栏中打开可访问性检查器。还可通过选择菜单选项 Tools、Check Accessibility 来打开它(参见图 13)。
图 13. Visual Studio .NET 2005 可访问性检查器
可访问性检查器提供了用于按照 WCAG 优先级 1 检查点、WCAG 优先级 2 检查点或 508 节准则验证 Web 站点的选项。可以通过打开“Error List”(依次选择菜单选项 View、Other Windows、Error List)来查看 Web 站点的验证结果。
Visual Studio .NET 2005 可访问性检查器还提供显示可访问性问题的“手动检查列表”的选项。如果选择该选项,则每当验证 Web 站点的可访问性时,都会在 Error List 窗口中显示相同的可访问性问题静态列表。该检查列表包含无法通过可访问性检查器自动验证的问题。
如果使用 Visual Web Developer 生成 Web 站点,则还可以检查 Web 页的可访问性。为此,需要使用某个联机可访问性检查器。下面的链接指向两个最流行的联机可访问性检查器: