Partager via


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:

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:

Element collection HTML table

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 noteSecurity 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

  1. 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.

  2. Right-click App_Code and then click Add New Item.

  3. Under Installed Templates click Visual Basic or Visual C#, and then click Class.

  4. In the Name text box, enter ElementDataSource.vb or ElementDataSource.cs, and then click OK.

  5. Delete all the code in the new class file.

  6. 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

  1. In Solution Explorer, right-click the project name and then click Add New Item.

    The Add New Item dialog box appears.

  2. Under Installed Templates, click Visual Basic or Visual C#, and then click Web Form.

  3. In the Name text box, enter Element.aspx.

  4. Make sure that the Place code in separate file check box is selected.

  5. 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.)

  6. Click OK.

  7. If the Select a Master Page dialog box appears, click OK. There is only one master page, and it will already be selected.

  8. 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.

  9. 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.

  10. 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.

  11. 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.

  12. 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.

  13. 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.

  14. 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.

  15. 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:

    Element collection HTML table

  16. 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

  1. Open the Element.aspx.vb or Element.aspx.cs file.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

    Element collection HTML table

  6. 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:

    Element collection HTML table

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

  1. 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:

    Configuration Browser Element List Page

    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.

  2. 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:

See Also

Concepts

Accessibility in Visual Studio and ASP.NET