Walkthrough: Accessibility Guidelines for Using Nested ListView Controls
This walkthrough shows how to use nested ListView controls to create complex data tables in ways that help make a Web page accessible for people who use screen reader software. These techniques can help you meet the following Web Content Accessibility Guidelines (WCAG) 2.0 guideline:
- Separating structure from presentation (WCAG guideline 1.3).
For more information about accessibility and WCAG 2.0, see Accessibility in Visual Studio and ASP.NET.
Prerequisites
In order to run this walkthrough, you must have the following:
Visual Studio 2010 or later versions.
An ASP.NET 4 Web site to which you can add a new Web page.
If you are building the Configuration System Browser application, this will be the Web site that you created in Walkthrough: Accessibility Guidelines for Using Image Controls, Menu Controls, and AutoPostBack.
The Configuration System Browser Application
This walkthrough is the fourth in a series that demonstrates techniques that can help an ASP.NET Web site conform to WCAG 2.0 accessibility guidelines. This series of walkthroughs creates a Web application that you can use to view ASP.NET configuration settings. If you want to do all the walkthroughs, start with Walkthrough: Accessibility Guidelines for Using Image Controls, Menu Controls, and AutoPostBack. If you do not want to complete other walkthroughs in the series, follow the alternate instructions that are provided for a few of the steps. The same accessibility features are illustrated whether you choose to complete the walkthrough as part of the series or independently.
The Web page that you create in this walkthrough displays a collection of configuration elements from the machine.Config configuration file. The table consists of a group of rows that have a heading for each group, as shown in the following illustration:
You can specify the element collection in a query parameter. For more information about configuration files and the section groups, sections, and elements that they contain, see ASP.NET Configuration Files. However, it is not necessary to be familiar with the ASP.NET configuration file system to use these walkthroughs as illustrations of how to create Web pages that comply with accessibility guidelines.
Security Note |
---|
The configuration information that is displayed by the application that you create in these walkthroughs is useful for developers, but you should not display it in a production Web site for security reasons. Configuration settings might include sensitive information that should be shown only to authorized users. |
A Visual Studio Web site project with source code is available to accompany this topic: Download.
Creating a Data Source
The data to display in the ListView controls comes from the ASP.NET configuration system. In the following procedure, you will create a class that retrieves a specified Configuration object, selects an element collection from that object, and returns the collection.
Note
This section of the walkthrough does not illustrate accessibility features specifically. It just provides data for you to work with in the ListView control.
To create a class that returns a list of configuration sections
If the Web site does not have an App_Code folder, in Solution Explorer right-click the project name, click Add ASP.NET Folder, and then click App_Code.
Right-click App_Code and then click Add New Item.
Under Installed Templates click Visual Basic or Visual C#, and then click Class.
In the Name text box, enter ElementDataSource.vb or ElementDataSource.cs, and then click OK.
Delete all the code in the new class file.
In its place, insert the following code:
Imports Microsoft.VisualBasic Imports System.Web.Configuration Imports System.Configuration Imports System.Reflection ''' <summary> ''' Retrieves a list of properties for an element or for each ''' item in an element collection. ''' </summary> Public Class ElementDataSource Public Sub New() End Sub Public Function GetElements(ByVal sectionName As String, ByVal elementName As String, ByVal virtualPath As String, ByVal site As String, ByVal locationSubPath As String, ByVal server As String) _ As List(Of ElementItemHeaderInfo) Dim elementList As New List(Of ElementItemHeaderInfo)() Dim config As Configuration = WebConfigurationManager.OpenWebConfiguration(virtualPath, site, locationSubPath, server) Dim cs As ConfigurationSection = config.GetSection(sectionName) Dim sectionType As Type = cs.GetType() Dim reflectionElement As System.Reflection.PropertyInfo = sectionType.GetProperty(elementName) Dim elementObject As Object = reflectionElement.GetValue(cs, Nothing) Dim elementType As Type = elementObject.GetType() Dim reflectionProperty As System.Reflection.PropertyInfo = elementType.GetProperty("Count") If reflectionProperty IsNot Nothing Then Dim elementCount As Integer = Convert.ToInt32(reflectionProperty.GetValue( elementObject, Nothing)) For i As Integer = 0 To elementCount - 1 Dim ei As New ElementItemHeaderInfo() ei.ItemName = String.Format( "Item {0} of {1}", i + 1, elementCount) ei.Index = i ei.Name = elementName ei.SectionName = sectionName elementList.Add(ei) Next Else Dim ei As New ElementItemHeaderInfo() ei.Name = elementName ei.ItemName = "Item 1 of 1" ei.SectionName = sectionName elementList.Add(ei) End If Return elementList End Function Public Function GetProperties(ByVal sectionName As String, ByVal elementName As String, ByVal index As Integer, ByVal virtualPath As String, ByVal site As String, ByVal locationSubPath As String, ByVal server As String) _ As List(Of ElementItemInfo) Dim elementItemList As New List(Of ElementItemInfo)() Dim config As Configuration = WebConfigurationManager.OpenWebConfiguration( virtualPath, site, locationSubPath, server) Dim cs As ConfigurationSection = config.GetSection(sectionName) Dim sectionType As Type = cs.GetType() Dim reflectionElement As System.Reflection.PropertyInfo = sectionType.GetProperty(elementName) Dim elementObject As Object = reflectionElement.GetValue(cs, Nothing) Dim elementType As Type = elementObject.GetType() Dim reflectionProperty As System.Reflection.PropertyInfo = elementType.GetProperty("Count") Dim elementCount As Integer = IIf(reflectionProperty Is Nothing, 0, Convert.ToInt32(reflectionProperty.GetValue( elementObject, Nothing))) If elementCount > 0 Then Dim i As Integer = 0 Dim elementItems As ConfigurationElementCollection = TryCast(elementObject, ConfigurationElementCollection) For Each elementItem As ConfigurationElement In elementItems If i = index Then elementObject = elementItem End If i += 1 Next End If Dim reflectionItemType As Type = elementObject.GetType() Dim elementProperties As PropertyInfo() = reflectionItemType.GetProperties() For Each rpi As System.Reflection.PropertyInfo In elementProperties If rpi.Name <> "SectionInformation" _ AndAlso rpi.Name <> "LockAttributes" _ AndAlso rpi.Name <> "LockAllAttributesExcept" _ AndAlso rpi.Name <> "LockElements" _ AndAlso rpi.Name <> "LockAllElementsExcept" _ AndAlso rpi.Name <> "LockItem" _ AndAlso rpi.Name <> "Item" _ AndAlso rpi.Name <> "ElementInformation" _ AndAlso rpi.Name <> "CurrentConfiguration" Then Dim eii As New ElementItemInfo() eii.Name = rpi.Name eii.TypeName = rpi.PropertyType.ToString() Dim uniqueID As String = rpi.Name + index.ToString() eii.UniqueID = uniqueID.Replace("/", "") Dim indexParms As ParameterInfo() = rpi.GetIndexParameters() If rpi.PropertyType Is GetType(IList) _ OrElse rpi.PropertyType Is GetType(ICollection) _ OrElse indexParms.Length > 0 Then eii.Value = "List" Else Dim propertyValue As Object = rpi.GetValue(elementObject, Nothing) eii.Value = IIf(propertyValue Is Nothing, "", propertyValue.ToString()) End If elementItemList.Add(eii) End If Next Return elementItemList End Function End Class Public Class ElementItemHeaderInfo Private _Name As String Public Property Name() As String Get Return _Name End Get Set(ByVal value As String) _Name = value End Set End Property Private _ItemName As String Public Property ItemName() As String Get Return _ItemName End Get Set(ByVal value As String) _ItemName = value End Set End Property Private _SectionName As String Public Property SectionName() As String Get Return _SectionName End Get Set(ByVal value As String) _SectionName = value End Set End Property Private _Value As String Public Property Value() As String Get Return _Value End Get Set(ByVal value As String) _Value = value End Set End Property Private _Index As Integer Public Property Index() As Integer Get Return _Index End Get Set(ByVal value As Integer) _Index = value End Set End Property End Class Public Class ElementItemInfo Private _Name As String Public Property Name() As String Get Return _Name End Get Set(ByVal value As String) _Name = value End Set End Property Private _TypeName As String Public Property TypeName() As String Get Return _TypeName End Get Set(ByVal value As String) _TypeName = value End Set End Property Public ReadOnly Property TypeNameUrl() As String Get Return "https://msdn.microsoft.com/en-us/library/" _ & TypeName & ".aspx" End Get End Property Private _Value As String Public Property Value() As String Get Return _Value End Get Set(ByVal value As String) _Value = value End Set End Property Private _UniqueID As String Public Property UniqueID() As String Get Return _UniqueID End Get Set(ByVal value As String) _UniqueID = value End Set End Property End Class
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Configuration; using System.Configuration; using System.ComponentModel; using System.Reflection; using System.Collections; /// <summary> /// Retrieves a list of properties for an element or for each /// item in an element collection. /// </summary> public class ElementDataSource { public ElementDataSource() { } public List<ElementItemHeaderInfo> GetElements( string sectionName, string elementName, string virtualPath, string site, string locationSubPath, string server) { List<ElementItemHeaderInfo> elementList = new List<ElementItemHeaderInfo>(); Configuration config = WebConfigurationManager.OpenWebConfiguration( virtualPath, site, locationSubPath, server); ConfigurationSection cs = config.GetSection(sectionName); Type sectionType = cs.GetType(); System.Reflection.PropertyInfo reflectionElement = sectionType.GetProperty(elementName); Object elementObject = reflectionElement.GetValue(cs, null); Type elementType = elementObject.GetType(); System.Reflection.PropertyInfo reflectionProperty = elementType.GetProperty("Count"); if (reflectionProperty != null) { int elementCount = Convert.ToInt32(reflectionProperty.GetValue( elementObject, null)); for (int i = 0; i < elementCount; i++) { ElementItemHeaderInfo ei = new ElementItemHeaderInfo(); ei.ItemName = String.Format( "Item {0} of {1}", i + 1, elementCount); ei.Index = i; ei.Name = elementName; ei.SectionName = sectionName; elementList.Add(ei); } } else { ElementItemHeaderInfo ei = new ElementItemHeaderInfo(); ei.Name = elementName; ei.ItemName = "Item 1 of 1"; ei.SectionName = sectionName; elementList.Add(ei); } return elementList; } public List<ElementItemInfo> GetProperties( string sectionName, string elementName, int index, string virtualPath, string site, string locationSubPath, string server) { List<ElementItemInfo> elementItemList = new List<ElementItemInfo>(); Configuration config = WebConfigurationManager.OpenWebConfiguration( virtualPath, site, locationSubPath, server); ConfigurationSection cs = config.GetSection(sectionName); Type sectionType = cs.GetType(); System.Reflection.PropertyInfo reflectionElement = sectionType.GetProperty(elementName); Object elementObject = reflectionElement.GetValue(cs, null); Type elementType = elementObject.GetType(); System.Reflection.PropertyInfo reflectionProperty = elementType.GetProperty("Count"); int elementCount = reflectionProperty == null ? 0 : Convert.ToInt32( reflectionProperty.GetValue(elementObject, null)); if (elementCount > 0) { int i = 0; ConfigurationElementCollection elementItems = elementObject as ConfigurationElementCollection; foreach (ConfigurationElement elementItem in elementItems) { if (i == index) { elementObject = elementItem; } i++; } } Type reflectionItemType = elementObject.GetType(); PropertyInfo[] elementProperties = reflectionItemType.GetProperties(); foreach (System.Reflection.PropertyInfo rpi in elementProperties) { if (rpi.Name != "SectionInformation" && rpi.Name != "LockAttributes" && rpi.Name != "LockAllAttributesExcept" && rpi.Name != "LockElements" && rpi.Name != "LockAllElementsExcept" && rpi.Name != "LockItem" && rpi.Name != "Item" && rpi.Name != "ElementInformation" && rpi.Name != "CurrentConfiguration") { ElementItemInfo eii = new ElementItemInfo(); eii.Name = rpi.Name; eii.TypeName = rpi.PropertyType.ToString(); string uniqueID = rpi.Name + index.ToString(); eii.UniqueID = uniqueID.Replace("/",""); ParameterInfo[] indexParms = rpi.GetIndexParameters(); if (rpi.PropertyType == typeof(IList) || rpi.PropertyType == typeof(ICollection) || indexParms.Length > 0) { eii.Value = "List"; } else { object propertyValue = rpi.GetValue(elementObject, null); eii.Value = propertyValue == null ? "" : propertyValue.ToString(); } elementItemList.Add(eii); } } return elementItemList; } } public class ElementItemHeaderInfo { public string Name { get; set; } public string ItemName { get; set; } public string SectionName { get; set; } public string Value { get; set; } public int Index { get; set; } } public class ElementItemInfo { public string Name { get; set; } public string TypeName { get; set; } public string TypeNameUrl { get { return "https://msdn.microsoft.com/en-us/library/" + TypeName + ".aspx"; } } public string Value { get; set; } public string UniqueID { get; set; } }
The ElementDataSource class contains a GetElements method and a GetProperties method.
The GetElements method accepts parameters that specify which element collection to retrieve. It returns a collection of ElementItemHeaderInfo objects, one object for each item in the collection. If the specified element is only a single element, a collection that has one item is returned. The ElementItemHeaderInfo class is defined immediately following the SectionDataSource class.
The GetProperties method accepts parameters that specify which item in an element collection to retrieve. It returns a collection of ElementItemInfo objects, one object for each property of the specified item in the element collection. The ElementItemInfo class is defined immediately following the ElementItemHeaderInfo class.
Both methods also accept parameters that specify which Configuration object to retrieve.
Note
The Configuration Browser application used in these walkthroughs includes redundant data source code that could have been refactored into common classes and methods. However, the code was duplicated to make sure that each walkthrough can be done separately. This approach also minimizes the number of steps in parts of the walkthrough that are not directly relevant to accessibility.
Creating a Web Page that Displays Tabular Data
In this section you create a Web page that uses ObjectDataSource controls to provide data to nested ListView controls. One of the ObjectDataSource controls calls the GetElements method of the ElementDataSource object that you created in the previous procedure. The other ObjectDataSource control calls the GetProperties method.
Note
Whether you are using the ObjectDataSource or some other method to retrieve data (for example, a database query using the SqlDataSource control or the LinqDataSource control), the methods for configuring the ListView controls are the same.
The ListView controls create a complex HTML table element that has a group of rows for each item in the element collection. Each group of rows has a header row that spans all the columns and indicates which item in the collection the following rows are for.
To make the table more accessible for people who use screen reader software, you will configure the ListView controls to include the following features in the HTML table that they generate:
A caption element describes the purpose of the table in a short heading.
A summary element provides a longer description of the purpose of the table.
th elements that have id attributes identify group header cells and column header cells.
td elements that have id attributes identify row header cells.
headers attributes in data cells specifically identify the group, column, and row headers that pertain to them.
Note
The HTML markup for a complex table illustrated in this walkthrough is one of two possible approaches. Instead of using headers attributes you can group rows in tbody elements and identify a group header by using a scope="rowgroup" attribute. For more information, see section "11.4, Table rendering by non-visual user agents", in the HTML 4.01 specification on the W3C Web site.
In the following procedure you will create a Web page and add markup that displays the list of elements in nested ListView controls.
To create a Web page that displays a list of configuration sections
In Solution Explorer, right-click the project name and then click Add New Item.
The Add New Item dialog box appears.
Under Installed Templates, click Visual Basic or Visual C#, and then click Web Form.
In the Name text box, enter Element.aspx.
Make sure that the Place code in separate file check box is selected.
If you are adding this page to the Configuration System Browser application, make sure that the Select master page check box is selected. (If you are not adding this page to the Web site that you create in Walkthrough: Accessibility Guidelines for Using Image Controls, Menu Controls, and AutoPostBack, there might not be a master page.)
Click OK.
If the Select a Master Page dialog box appears, click OK. There is only one master page, and it will already be selected.
In the @ Page directive, set the Title property to Configuration System Browser - Element Properties, as shown in the following example:
<%@ Page Language="VB" AutoEventWireup="true" Title="Configuration System Browser - Element Properties" CodeFile="Element.aspx.vb" MasterPageFile="~/Site.master" Inherits="Element" %>
<%@ Page Language="C#" AutoEventWireup="true" Title="Configuration System Browser - Element Properties" CodeFile="Element.aspx.cs" MasterPageFile="~/Site.master" Inherits="Element" %>
This title identifies the site and the page in the site. Setting the page title is required by accessibility guidelines.
Inside the Content element that is for the MainContent ContentPlaceHolder control, insert the following markup:
<h2> <asp:Label ID="HeadingLabel" runat="server" Text="Elements in Section [name]"> </asp:Label> </h2>
<h2> <asp:Label ID="HeadingLabel" runat="server" Text="Elements in Section [name]"> </asp:Label> </h2>
(If you creating a Web page that is not part of the Configuration System Browser application, insert the markup between the <div> and </div> tags.)
This markup adds a heading in a Label control so that the heading can be changed programmatically. The PreRender event handler for the outer ListView control will replace the string "[name]" in the heading with the actual name of the element that the page is displaying.
Below the markup that you inserted in the previous step, insert the following markup:
<asp:ObjectDataSource ID="OuterObjectDataSource" runat="server" SelectMethod="GetElements" TypeName="ElementDataSource"> <SelectParameters> <asp:QueryStringParameter Name="sectionName" QueryStringField="Section" Type="String" DefaultValue="system.web/webParts" /> <asp:QueryStringParameter Name="elementName" QueryStringField="Element" Type="String" DefaultValue="Transformers" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource>
<asp:ObjectDataSource ID="OuterObjectDataSource" runat="server" SelectMethod="GetElements" TypeName="ElementDataSource"> <SelectParameters> <asp:QueryStringParameter Name="sectionName" QueryStringField="Section" Type="String" DefaultValue="system.web/webParts" /> <asp:QueryStringParameter Name="elementName" QueryStringField="Element" Type="String" DefaultValue="Transformers" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource>
This markup creates an ObjectDataSource control that calls the GetElements method of an ElementDataSource object. Parameters that are passed to the GetElements method specify the element collection and the specific Configuration object from which to retrieve the section.
The name of the element and the section it is in is retrieved from query string fields that are named "Section" and "Element". If there is no query string, the default section is "system.web/webParts", and the default element collection is "Transformers". Values for the other four parameters are retrieved from session state. Values are placed in session state by another page in the Configuration System Browser application. Therefore, only the default values will be used if you are creating this walkthrough as an independent Web page.
Below the ObjectDataSource control, insert the following markup:
<div class="dataTable"> <asp:ListView ID="ListView1" runat="server" DataSourceID="OuterObjectDataSource" OnPreRender="ListView1_PreRender" onitemdatabound="ListView1_ItemDataBound">
<div class="dataTable"> <asp:ListView ID="ListView1" runat="server" DataSourceID="OuterObjectDataSource" OnPreRender="ListView1_PreRender" onitemdatabound="ListView1_ItemDataBound">
This markup creates a ListView control in a div element (you will add the closing tags for these elements later). This is the outer ListView control. It will generate the table structure and the group header rows. An inner ListView control will generate the group detail rows.
The markup registers a handler for the control's PreRender event so that the EmptyDataTemplate object, the caption element of the HTML table, and the page heading can be customized using the name of the selected configuration element collection.
Below the markup that you inserted in the preceding step, insert the following markup:
<LayoutTemplate> <table class="listViewTable" width="100%" cellpadding="5" rules="all" border="1" summary="This table shows properties of items contained in an element collection."> <caption runat="server" ID="ElementTableCaption"> Properties of the [name] Element </caption> <tr style=""> <th id="hdrName" axis="field">Name</th> <th id="hdrType" axis="field">Type</th> <th id="hdrValue" axis="field">Value</th> </tr> <tbody id="itemPlaceholder" runat="server"></tbody> </table> </LayoutTemplate>
<LayoutTemplate> <table class="listViewTable" width="100%" cellpadding="5" rules="all" border="1" summary="This table shows properties of items contained in an element collection."> <caption runat="server" ID="ElementTableCaption"> Properties of the [name] Element </caption> <tr style=""> <th id="hdrName" axis="field">Name</th> <th id="hdrType" axis="field">Type</th> <th id="hdrValue" axis="field">Value</th> </tr> <tbody id="itemPlaceholder" runat="server"></tbody> </table> </LayoutTemplate>
This markup creates a LayoutTemplate object for the ListView control. The LayoutTemplate object specifies that the control will generate an HTML table that has the following features that relate to accessibility:
The caption element is a short description of the table's contents.
The summary attribute is a longer description of the table's contents.
th elements that have id and axis attributes identify column header cells.
Below the markup that you inserted in the preceding step, insert the following markup:
<ItemTemplate> <tr> <th colspan="3" id="<%# GetElementHeaderID(Container) %>" axis="item"> <asp:Label ID="Label1" runat="server" Text='<%# Eval("ItemName") %>' > </asp:Label> </th> </tr>
<ItemTemplate> <tr> <th colspan="3" id="<%# GetElementHeaderID(Container) %>" axis="item"> <asp:Label ID="Label1" runat="server" Text='<%# Eval("ItemName") %>' > </asp:Label> </th> </tr>
This markup creates the first part of an ItemTemplate object for the ListView control. The markup specifies that each data row will generate a tr element that contains a single cell that spans all three columns. This cell functions as a header for a group of rows. In the following steps you will add the remainder of the item template to specify how the other rows of the group are generated.
To help with accessibility, the td element has id and axis attributes. The id attribute uniquely identifies this header table cell so that detail cells can refer to it. The value for the id attribute will be "hdrElementX", where "X" is the number of the group. For example, hdrElement0 would be the id value in the first group. The value is generated by a method in the page code that you will add in a later step.
Below the markup that you inserted in the previous step, insert the following markup:
<asp:ObjectDataSource ID="InnerObjectDataSource" runat="server" SelectMethod="GetProperties" TypeName="ElementDataSource"> <SelectParameters> <asp:Parameter Name="sectionName" Type="String" DefaultValue="system.web/webParts" /> <asp:Parameter Name="elementName" Type="String" DefaultValue="Transformers" /> <asp:Parameter Name="index" Type="Int32" DefaultValue="1" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource>
<asp:ObjectDataSource ID="InnerObjectDataSource" runat="server" SelectMethod="GetProperties" TypeName="ElementDataSource"> <SelectParameters> <asp:Parameter Name="sectionName" Type="String" DefaultValue="system.web/webParts" /> <asp:Parameter Name="elementName" Type="String" DefaultValue="Transformers" /> <asp:Parameter Name="index" Type="Int32" DefaultValue="1" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource>
This markup creates an ObjectDataSource control that calls the GetProperties method of an ElementDataSource object. Parameters that are passed to the GetProperties method specify the specific item in an element collection and the specific Configuration object from which to retrieve the section.
In a later step you will add code that sets the parameters for the GetProperties method. The values will come from the header row that is generated by the outer ListView control.
Below the markup that you inserted in the preceding step, insert the following markup:
<asp:ListView ID="PropertiesListView" runat="server" DataSourceID="InnerObjectDataSource"> <LayoutTemplate> <tr id="itemPlaceHolder" runat="server"></tr> </LayoutTemplate> <ItemTemplate> <tr> <td id="<%# GetPropertyHeaderID(Container) %>" axis="property"> <%# Eval("Name") %> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrType") %>"> <asp:HyperLink ID="HyperLink2" runat="server" Text='<%# Eval("TypeName") %>' NavigateUrl='<%# Eval("TypeNameUrl") %>'> </asp:HyperLink> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrValue") %>"> <%# Eval("Value") %> </td> </tr> </ItemTemplate> </asp:ListView> </ItemTemplate>
<asp:ListView ID="PropertiesListView" runat="server" DataSourceID="InnerObjectDataSource"> <LayoutTemplate> <tr id="itemPlaceHolder" runat="server"></tr> </LayoutTemplate> <ItemTemplate> <tr> <td id="<%# GetPropertyHeaderID(Container) %>" axis="property"> <%# Eval("Name") %> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrType") %>"> <asp:HyperLink ID="HyperLink2" runat="server" Text='<%# Eval("TypeName") %>' NavigateUrl='<%# Eval("TypeNameUrl") %>'> </asp:HyperLink> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrValue") %>"> <%# Eval("Value") %> </td> </tr> </ItemTemplate> </asp:ListView> </ItemTemplate>
This markup creates a LayoutTemplate object and an ItemTemplate object for the inner ListView control. The ItemTemplate object specifies that each data row will generate a tr element that has the following features that relate to accessibility:
Because each row presents the data for an element, and the first column contains the element name, td elements in this column are generated that contain id and axis attributes.
The value for the id attribute will be "hdrPropertyXY", where "X" is the name of the property and "Y" is the number of the group. For example, "hdrPropertyName0" would be the id value for the "Name" property in the first group. The value is generated by a method in the page code that you will add in a later step.
The td elements of the second and third columns are generated that include headers attributes that identify the three header cells that pertain to each detail cell.
For example, the headers attribute for the cell that has the value "RowToFieldTransformer" in the following illustration has a headers attribute value of "hdrElement0 hdrPropertyName0 hdrValue". These values indicate that the cell pertains to the Item 1 of 2 group header, the Name row header, and the Value column header. The headers attribute value is generated by a method in the page code that you will add in a later step.
The following illustration shows the result:
Below the markup that you inserted in the preceding step, insert the following markup:
<EmptyDataTemplate> <asp:Label ID="NoElementsLabel" runat="server" Text="No information is available for the [element] element."> </asp:Label> </EmptyDataTemplate> </asp:ListView> </div>
<EmptyDataTemplate> <asp:Label ID="NoElementsLabel" runat="server" Text="No information is available for the [element] element."> </asp:Label> </EmptyDataTemplate> </asp:ListView> </div>
This markup creates an EmptyDataTemplate object for the outer ListView control and provides the closing tags for the outer ListView control and the div element that it is in. The EmptyDataTemplate object provides a custom message in case the selected configuration element collection does not contain any elements.
The Element.aspx page now resembles the following example:
<%@ Page Language="VB" AutoEventWireup="true" Title="Configuration System Browser - Element Properties" CodeFile="Element.aspx.vb" MasterPageFile="~/Site.master" Inherits="Element" %> <asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" Runat="Server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" Runat="Server"> <h2> <asp:Label ID="HeadingLabel" runat="server" Text="Elements in Section [name]"> </asp:Label> </h2> <asp:ObjectDataSource ID="OuterObjectDataSource" runat="server" SelectMethod="GetElements" TypeName="ElementDataSource"> <SelectParameters> <asp:QueryStringParameter Name="sectionName" QueryStringField="Section" Type="String" DefaultValue="system.web/webParts" /> <asp:QueryStringParameter Name="elementName" QueryStringField="Element" Type="String" DefaultValue="Transformers" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource> <div class="dataTable"> <asp:ListView ID="ListView1" runat="server" DataSourceID="OuterObjectDataSource" OnPreRender="ListView1_PreRender" onitemdatabound="ListView1_ItemDataBound"> <LayoutTemplate> <table class="listViewTable" width="100%" cellpadding="5" rules="all" border="1" summary="This table shows properties of items contained in an element collection."> <caption runat="server" ID="ElementTableCaption"> Properties of the [name] Element </caption> <tr style=""> <th id="hdrName" axis="field">Name</th> <th id="hdrType" axis="field">Type</th> <th id="hdrValue" axis="field">Value</th> </tr> <tbody id="itemPlaceholder" runat="server"></tbody> </table> </LayoutTemplate> <ItemTemplate> <tr> <th colspan="3" id="<%# GetElementHeaderID(Container) %>" axis="item"> <asp:Label ID="Label1" runat="server" Text='<%# Eval("ItemName") %>' > </asp:Label> </th> </tr> <asp:ObjectDataSource ID="InnerObjectDataSource" runat="server" SelectMethod="GetProperties" TypeName="ElementDataSource"> <SelectParameters> <asp:Parameter Name="sectionName" Type="String" DefaultValue="system.web/webParts" /> <asp:Parameter Name="elementName" Type="String" DefaultValue="Transformers" /> <asp:Parameter Name="index" Type="Int32" DefaultValue="1" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource> <asp:ListView ID="PropertiesListView" runat="server" DataSourceID="InnerObjectDataSource"> <LayoutTemplate> <tr id="itemPlaceHolder" runat="server"></tr> </LayoutTemplate> <ItemTemplate> <tr> <td id="<%# GetPropertyHeaderID(Container) %>" axis="property"> <%# Eval("Name") %> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrType") %>"> <asp:HyperLink ID="HyperLink2" runat="server" Text='<%# Eval("TypeName") %>' NavigateUrl='<%# Eval("TypeNameUrl") %>'> </asp:HyperLink> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrValue") %>"> <%# Eval("Value") %> </td> </tr> </ItemTemplate> </asp:ListView> </ItemTemplate> <EmptyDataTemplate> <asp:Label ID="NoElementsLabel" runat="server" Text="No information is available for the [element] element."> </asp:Label> </EmptyDataTemplate> </asp:ListView> </div> </asp:Content>
<%@ Page Language="C#" AutoEventWireup="true" Title="Configuration System Browser - Element Properties" CodeFile="Element.aspx.cs" MasterPageFile="~/Site.master" Inherits="Element" %> <asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" Runat="Server"> </asp:Content> <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" Runat="Server"> <h2> <asp:Label ID="HeadingLabel" runat="server" Text="Elements in Section [name]"> </asp:Label> </h2> <asp:ObjectDataSource ID="OuterObjectDataSource" runat="server" SelectMethod="GetElements" TypeName="ElementDataSource"> <SelectParameters> <asp:QueryStringParameter Name="sectionName" QueryStringField="Section" Type="String" DefaultValue="system.web/webParts" /> <asp:QueryStringParameter Name="elementName" QueryStringField="Element" Type="String" DefaultValue="Transformers" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource> <div class="dataTable"> <asp:ListView ID="ListView1" runat="server" DataSourceID="OuterObjectDataSource" OnPreRender="ListView1_PreRender" onitemdatabound="ListView1_ItemDataBound"> <LayoutTemplate> <table class="listViewTable" width="100%" cellpadding="5" rules="all" border="1" summary="This table shows properties of items contained in an element collection."> <caption runat="server" ID="ElementTableCaption"> Properties of the [name] Element </caption> <tr style=""> <th id="hdrName" axis="field">Name</th> <th id="hdrType" axis="field">Type</th> <th id="hdrValue" axis="field">Value</th> </tr> <tbody id="itemPlaceholder" runat="server"></tbody> </table> </LayoutTemplate> <ItemTemplate> <tr> <th colspan="3" id="<%# GetElementHeaderID(Container) %>" axis="item"> <asp:Label ID="Label1" runat="server" Text='<%# Eval("ItemName") %>' > </asp:Label> </th> </tr> <asp:ObjectDataSource ID="InnerObjectDataSource" runat="server" SelectMethod="GetProperties" TypeName="ElementDataSource"> <SelectParameters> <asp:Parameter Name="sectionName" Type="String" DefaultValue="system.web/webParts" /> <asp:Parameter Name="elementName" Type="String" DefaultValue="Transformers" /> <asp:Parameter Name="index" Type="Int32" DefaultValue="1" /> <asp:SessionParameter Name="virtualPath" SessionField="Path" Type="String" DefaultValue="" /> <asp:SessionParameter Name="site" SessionField="Site" Type="String" DefaultValue="" /> <asp:SessionParameter Name="locationSubPath" SessionField="SubPath" Type="String" DefaultValue="" /> <asp:SessionParameter Name="server" SessionField="Server" Type="String" DefaultValue="" /> </SelectParameters> </asp:ObjectDataSource> <asp:ListView ID="PropertiesListView" runat="server" DataSourceID="InnerObjectDataSource"> <LayoutTemplate> <tr id="itemPlaceHolder" runat="server"></tr> </LayoutTemplate> <ItemTemplate> <tr> <td id="<%# GetPropertyHeaderID(Container) %>" axis="property"> <%# Eval("Name") %> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrType") %>"> <asp:HyperLink ID="HyperLink2" runat="server" Text='<%# Eval("TypeName") %>' NavigateUrl='<%# Eval("TypeNameUrl") %>'> </asp:HyperLink> </td> <td headers="<%# GetColumnHeaderIDs(Container, "hdrValue") %>"> <%# Eval("Value") %> </td> </tr> </ItemTemplate> </asp:ListView> </ItemTemplate> <EmptyDataTemplate> <asp:Label ID="NoElementsLabel" runat="server" Text="No information is available for the [element] element."> </asp:Label> </EmptyDataTemplate> </asp:ListView> </div> </asp:Content>
Adding Code to Populate the HTML Table
In the following procedure you will add code that performs the following functions:
Binds the inner ListView control to the data that corresponds to each row that is generated by the outer ListView control.
Puts the name of the selected configuration element collection in the page heading, the caption element of the HTML table, and the EmptyDataTemplate object if no data is returned.
Constructs unique id attribute values for the element (group) header and the property (row) header.
Constructs values for the headers attributes.
To add code to populate the HTML table
Open the Element.aspx.vb or Element.aspx.cs file.
At the end of the using statements (Imports in Visual Basic) add the following code:
Imports System.Web.UI.HtmlControls
using System.Web.UI.HtmlControls;
This is a using or Imports statement that is needed for a class that you will reference in the following steps.
Below the Page_Load method, add the following code:
Protected Sub ListView1_ItemDataBound(ByVal sender As Object, ByVal e As ListViewItemEventArgs) Dim ei As ElementItemHeaderInfo = TryCast(e.Item.DataItem, ElementItemHeaderInfo) Dim ods As ObjectDataSource = CType(e.Item.FindControl("InnerObjectDataSource"), ObjectDataSource) ods.SelectParameters("sectionName").DefaultValue = ei.SectionName ods.SelectParameters("elementName").DefaultValue = ei.Name 'propertiesListView.DataBind(); ods.SelectParameters("index").DefaultValue = ei.Index.ToString() End Sub
protected void ListView1_ItemDataBound( object sender, ListViewItemEventArgs e) { ElementItemHeaderInfo ei = e.Item.DataItem as ElementItemHeaderInfo; ObjectDataSource ods = (ObjectDataSource)e.Item.FindControl("InnerObjectDataSource"); ods.SelectParameters["sectionName"].DefaultValue = ei.SectionName; ods.SelectParameters["elementName"].DefaultValue = ei.Name; ods.SelectParameters["index"].DefaultValue = ei.Index.ToString(); //propertiesListView.DataBind(); }
This handler for the outer ListView control's ItemDataBound event gets the current ElementItemHeaderInfo object from the outer ListView control and puts data values from that object into the parameters of the inner ObjectDataSource control.
Below the code that you inserted for the preceding step, add the following code:
Protected Sub ListView1_PreRender(ByVal sender As Object, ByVal e As EventArgs) Dim elementName As String = (OuterObjectDataSource.SelectParameters("sectionName").DefaultValue.ToString() _ & "/") + _ OuterObjectDataSource.SelectParameters("elementName").DefaultValue.ToString() If Request.QueryString("Section") IsNot Nothing Then elementName = (Request.QueryString("Section") & "/") _ + Request.QueryString("Element") End If HeadingLabel.Text = HeadingLabel.Text.Replace("[name]", elementName) Dim tableCaption As HtmlGenericControl = TryCast(ListView1.FindControl("ElementTableCaption"), System.Web.UI.HtmlControls.HtmlGenericControl) If tableCaption IsNot Nothing Then tableCaption.InnerText = tableCaption.InnerText.Replace("[name]", elementName) End If Dim noElementsLabel As Label = TryCast(ListView1.Controls(0).FindControl("NoElementsLabel"), Label) If noElementsLabel IsNot Nothing Then noElementsLabel.Text = noElementsLabel.Text.Replace("[name]", elementName) End If End Sub
protected void ListView1_PreRender(object sender, EventArgs e) { string elementName = OuterObjectDataSource.SelectParameters["sectionName"].DefaultValue.ToString() + "/" + OuterObjectDataSource.SelectParameters["elementName"].DefaultValue.ToString(); if (Request.QueryString["Section"] != null) { elementName = Request.QueryString["Section"] + "/" + Request.QueryString["Element"]; } HeadingLabel.Text = HeadingLabel.Text.Replace("[name]", elementName); HtmlGenericControl tableCaption = ListView1.FindControl("ElementTableCaption") as System.Web.UI.HtmlControls.HtmlGenericControl; if (tableCaption != null) { tableCaption.InnerText = tableCaption.InnerText.Replace( "[name]", elementName); } Label noElementsLabel = ListView1.Controls[0].FindControl("NoElementsLabel") as Label; if (noElementsLabel != null) { noElementsLabel.Text = noElementsLabel.Text.Replace( "[name]", elementName); } }
This handler for the outer ListView control's PreRender event gets the selected section and element names from the query string. It then updates the page heading, the caption element, and the EmptyDataTemplate object. If there is no query string, the default parameter values for the outer ObjectDataSource control are used.
Below the code that you inserted for the preceding step, add the following code:
Protected Function GetElementHeaderID(ByVal item As ListViewItem) _ As String Return "hdrElement" & item.DataItemIndex.ToString() End Function Protected Function GetPropertyHeaderID(ByVal item As ListViewItem) _ As String Return "hdrProperty" & CType(item.DataItem, ElementItemInfo).UniqueID End Function
protected string GetElementHeaderID(ListViewItem item) { return "hdrElement" + item.DataItemIndex.ToString(); } protected string GetPropertyHeaderID(ListViewItem item) { return "hdrProperty" + ((ElementItemInfo)item.DataItem).UniqueID; }
These methods create id attribute values for the element (group) header and property (row) header cells. The methods are called by markup in the .aspx page when the header cells are being generated. They are also called by the method that you add in the following step which is used for creating the headers attribute values for detail cells. The following example shows the values that are supplied by these methods.
<th colspan="3" id="hdrElement0" axis="item"> <span id=...>Item 1 of 2</span> </th> </tr> <tr> <th id="hdrPropertyName0" axis="property" class="rowHeading"> Name </th> <td headers="hdrElement0 hdrPropertyName0 hdrType"> <a id=... class="smallText" href=...> System.String </a> </td> <td headers="hdrElement0 hdrPropertyName0 hdrValue"> RowToFieldTransformer </td> </tr>
The following illustration shows the heading row and the first detail row under the heading row that are created by the HTML in the previous example.
Below the code that you inserted for the preceding step, add the following code:
Protected Function GetColumnHeaderIDs(ByVal item As ListViewDataItem, ByVal columnHeader As String) _ As String Dim elementHeaderID As String = GetElementHeaderID( CType(item.NamingContainer.NamingContainer, ListViewItem)) Dim propertyHeaderID As String = GetPropertyHeaderID(item) Return String.Format("{0} {1} {2}", elementHeaderID, propertyHeaderID, columnHeader) End Function
protected string GetColumnHeaderIDs (ListViewDataItem item, string columnHeader) { string elementHeaderID = GetElementHeaderID ((ListViewItem)item.NamingContainer.NamingContainer); string propertyHeaderID = GetPropertyHeaderID(item); return string.Format("{0} {1} {2}", elementHeaderID, propertyHeaderID, columnHeader); }
This method creates headers attribute values for detail cells. The headers attribute is made up of id attribute values from three header cells, separated by spaces. To create the element (group) header id and the property (row) header id, this method calls the methods that were created in the previous step. The id attributes of the field (column) header cells never change. Therefore, those are passed to the method in a parameter. The following example shows the values that are supplied by these methods:
<tr> <th id="hdrPropertyName0" axis="property" class="rowHeading"> Name </th> <td headers="hdrElement0 hdrPropertyName0 hdrType"> <a id=... class="smallText" href=...> System.String </a> </td> <td headers="hdrElement0 hdrPropertyName0 hdrValue"> RowToFieldTransformer </td> </tr>
The following illustration shows the first detail row under the heading row:
Testing the Web Page
You can now test to verify that the table displays correctly and that accessible HTML is generated for it.
To test the page
In Solution Explorer, right-click Element.aspx and then click View in Browser.
You see a table that lists the elements in the system.web/webParts/Transformers element collection. Type names are displayed as hyperlinks that point to the MSDN documentation for the indicated type, as shown in the following illustration:
If you created the Web page as an independent page instead of as part of the Configuration System Browser application, the heading and the table contents will be the same, but there will be no title bar or menu bar, and the table caption will appear above the table.
In the browser, view the page source.
The following example shows the table elements that were added to enhance the table's accessibility for people who use screen readers.
<table class="listViewTable" width="100%" cellpadding="5" rules="all" border="1" summary="This table shows properties of items contained in an element collection."> <caption id=...> Properties of the system.web/webParts/Transformers Element </caption> <tr style=""> <th id="hdrName" axis="field">Name</th> <th id="hdrType" axis="field">Type</th> <th id="hdrValue" axis="field">Value</th> </tr> <tr> <th colspan="3" id="hdrElement0" axis="item"> <span id=...>Item 1 of 2</span> </th> </tr> <tr> <th id="hdrPropertyName0" axis="property" class="rowHeading"> Name </th> <td headers="hdrElement0 hdrPropertyName0 hdrType"> <a id=... class="smallText" href=...> System.String </a> </td> <td headers="hdrElement0 hdrPropertyName0 hdrValue"> RowToFieldTransformer </td> </tr> ... </table>
Next Steps
In this walkthrough you used nested ListView controls to generate an HTML table that contains elements and attributes that help make complex tabular data accessible for people who use screen reader software. Other walkthroughs in this series demonstrate other techniques that help your Web site conform to accessibility guidelines. The next walkthrough in the series is Walkthrough: Accessibility Guidelines for Using Label Controls, Validator Controls, and Panel Controls. The other walkthroughs are the following:
Walkthrough: Accessibility Guidelines for Using Image Controls, Menu Controls, and AutoPostBack
Walkthrough: Accessibility Guidelines for Using the GridView Control
Walkthrough: Accessibility Guidelines for Using the ListView Control