嵌套的数据 Web 控件 (VB)

作者 :Scott Mitchell

下载 PDF

在本教程中,我们将探讨如何使用嵌套在另一个中继器中的 Repeater。 这些示例将演示如何以声明方式和编程方式填充内部 Repeater。

简介

除了静态 HTML 和数据绑定语法外,模板还可以包括 Web 控件和用户控件。 这些 Web 控件可以通过声明性、数据绑定语法分配其属性,也可以在相应的服务器端事件处理程序中以编程方式访问。

通过在模板中嵌入控件,可以自定义和改进外观和用户体验。 例如,在 GridView 控件中使用 TemplateFields 教程中,我们了解了如何通过在 TemplateField 中添加日历控件来显示员工的雇用日期来自定义 GridView 的显示;在 将验证控件添加到编辑和插入接口自定义数据修改接口 教程中,我们了解了如何通过添加验证控件、TextBoxes、DropDownLists 和其他 Web 控件来自定义编辑和插入接口。

模板还可以包含其他数据 Web 控件。 也就是说,我们可以有一个 DataList,其中包含另一个 DataList (、Repeater、GridView 或 DetailsView,等等) 在其模板中。 此类接口的挑战是将适当的数据绑定到内部数据 Web 控件。 有几种不同的方法可用,从使用 ObjectDataSource 的声明性选项到编程方法不等。

在本教程中,我们将探讨如何使用嵌套在另一个中继器中的 Repeater。 外部中继器将包含数据库中每个类别的一个项,显示类别的名称和说明。 每个类别项的内部中继器将显示属于该类别的每个产品的信息 (请参阅项目符号列表中的图 1) 。 我们的示例将演示如何以声明方式和编程方式填充内部 Repeater。

列出每个类别及其产品

图 1:列出每个类别及其产品 (单击以查看全尺寸图像)

步骤 1:创建类别列表

生成使用嵌套数据 Web 控件的页面时,我发现首先设计、创建和测试最外层的数据 Web 控件很有帮助,甚至无需担心内部嵌套控件。 因此,让我们首先逐步完成向页面添加 Repeater 所需的步骤,该页面列出了每个类别的名称和说明。

首先打开 文件夹中的页面NestedControls.aspxDataListRepeaterBasics,并将 Repeater 控件添加到页面,并将其 ID 属性设置为 CategoryList。 从 Repeater 的智能标记中,选择创建名为 CategoriesDataSource的新 ObjectDataSource。

将新对象命名为DataSource CategoriesDataSource

图 2:将 New ObjectDataSource CategoriesDataSource 命名 (单击以查看全尺寸图像)

配置 ObjectDataSource,以便它从 CategoriesBLL 类方法 GetCategories 拉取其数据。

将 ObjectDataSource 配置为使用 CategoriesBLL 类 getCategories 方法

图 3:将 ObjectDataSource 配置为使用 CategoriesBLL 类方法 GetCategories (单击以查看全尺寸图像)

若要指定 Repeater 的模板内容,需要转到“源”视图并手动输入声明性语法。 添加一个 , ItemTemplate 用于在元素中 <h4> 显示类别名称,并在 paragraph 元素中添加类别说明, (<p>) 。 此外,让我们使用水平规则 (<hr>) 来分隔每个类别。 进行这些更改后,页面应包含 Repeater 和 ObjectDataSource 的声明性语法,如下所示:

<asp:Repeater ID="CategoryList" DataSourceID="CategoriesDataSource"
    EnableViewState="False" runat="server">
    <ItemTemplate>
        <h4><%# Eval("CategoryName") %></h4>
        <p><%# Eval("Description") %></p>
    </ItemTemplate>
    <SeparatorTemplate>
        <hr />
    </SeparatorTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="CategoriesDataSource" runat="server"
    OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>

图 4 显示了通过浏览器查看时的进度。

列出每个类别的名称和说明,由水平规则分隔

图 4:列出了每个类别的名称和说明,由水平规则分隔 (单击以查看全尺寸图像)

步骤 2:添加嵌套产品中继器

完成类别列表后,下一个任务是向 添加一个 Repeater CategoryListItemTemplate ,用于显示属于相应类别的这些产品的相关信息。 有多种方法可以检索此内部中继器的数据,我们稍后将探讨其中两种方法。 现在,让我们在 Repeater 中创建ItemTemplate产品 RepeaterCategoryList。 具体而言,让我们让产品中继器在项目符号列表中显示每个产品,其中包含包括产品名称和价格在内的每个列表项。

若要创建此中继器,需要将内部 Repeater 声明性语法和模板手动输入到 CategoryList s ItemTemplate中。 在 Repeater s ItemTemplate中添加CategoryList以下标记:

<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
    runat="server">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li><strong><%# Eval("ProductName") %></strong>
            (<%# Eval("UnitPrice", "{0:C}") %>)</li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>

步骤 3:将 Category-Specific Products 绑定到 ProductsByCategoryList Repeater

如果此时通过浏览器访问页面,屏幕将如图 4 中所示,因为我们尚未将任何数据绑定到中继器。 有几种方法可以获取适当的产品记录并将其绑定到 Repeater,有些方法比其他方法更有效。 此处main挑战是找回指定类别的相应产品。

可以通过 Repeater 中的 CategoryList ObjectDataSource 以声明方式访问要绑定到内部 Repeater ItemTemplate控件的数据,也可以以编程方式从 ASP.NET 页的代码隐藏页访问。 同样,此数据可以通过声明方式绑定到内部 Repeater - 通过内部 Repeater s DataSourceID 属性或通过声明性数据绑定语法,或者通过编程方式通过引用 Repeater 事件处理程序ItemDataBound中的CategoryList内部 Repeater、以编程方式设置其DataSource属性并调用其 DataBind() 方法。 让我们探讨其中每种方法。

使用 ObjectDataSource 控件和ItemDataBound事件处理程序以声明方式访问数据

由于我们在整个教程系列中广泛使用了 ObjectDataSource,因此访问此示例数据的最自然选择是坚持使用 ObjectDataSource。 类 ProductsBLL 有一个 GetProductsByCategoryID(categoryID) 方法,该方法返回属于指定 categoryID的这些产品的相关信息。 因此,我们可以将 ObjectDataSource 添加到 CategoryList Repeater, ItemTemplate 并将其配置为从此类方法访问其数据。

遗憾的是,Repeater 不允许通过设计视图编辑其模板,因此我们需要手动添加此 ObjectDataSource 控件的声明性语法。 以下语法显示了CategoryList在添加此新的 ObjectDataSource (ProductsByCategoryDataSource) 之后的 RepeaterItemTemplate

<h4><%# Eval("CategoryName") %></h4>
<p><%# Eval("Description") %></p>
<asp:Repeater ID="ProductsByCategoryList" EnableViewState="False"
        DataSourceID="ProductsByCategoryDataSource" runat="server">
    <HeaderTemplate>
        <ul>
    </HeaderTemplate>
    <ItemTemplate>
        <li><strong><%# Eval("ProductName") %></strong> -
                sold as <%# Eval("QuantityPerUnit") %> at
                <%# Eval("UnitPrice", "{0:C}") %></li>
    </ItemTemplate>
    <FooterTemplate>
        </ul>
    </FooterTemplate>
</asp:Repeater>
<asp:ObjectDataSource ID="ProductsByCategoryDataSource" runat="server"
           SelectMethod="GetProductsByCategoryID" TypeName="ProductsBLL">
   <SelectParameters>
        <asp:Parameter Name="CategoryID" Type="Int32" />
   </SelectParameters>
</asp:ObjectDataSource>

使用 ObjectDataSource 方法时,需要将 Repeater 的 DataSourceID 属性设置为 ProductsByCategoryListID ObjectDataSource (ProductsByCategoryDataSource) 的 。 另请注意,ObjectDataSource 具有一个 <asp:Parameter> 元素, categoryID 该元素指定将传递到方法中的 GetProductsByCategoryID(categoryID) 值。 但是,如何指定此值? 理想情况下,我们只需使用数据绑定语法设置 DefaultValue 元素的 <asp:Parameter> 属性,如下所示:

<asp:Parameter Name="CategoryID" Type="Int32"
    DefaultValue='<%# Eval("CategoryID")' />

遗憾的是,数据绑定语法仅在具有 DataBinding 事件的控件中有效。 类 Parameter 缺少此类事件,因此上述语法是非法的,将导致运行时错误。

若要设置此值,需要为 CategoryList Repeater 事件 ItemDataBound 创建事件处理程序。 回想一下,对于 ItemDataBound 绑定到中继器的每个项,该事件都会触发一次。 因此,每次为外部中继器触发此事件时,我们都可以将当前 CategoryID 值分配给 ProductsByCategoryDataSource ObjectDataSource s CategoryID 参数。

使用以下代码为 CategoryList Repeater 事件 ItemDataBound 创建事件处理程序:

Protected Sub CategoryList_ItemDataBound(sender As Object, e As RepeaterItemEventArgs) _
    Handles CategoryList.ItemDataBound
    If e.Item.ItemType = ListItemType.AlternatingItem _
        OrElse e.Item.ItemType = ListItemType.Item Then
        ' Reference the CategoriesRow object being bound to this RepeaterItem
        Dim category As Northwind.CategoriesRow = _
            CType(CType(e.Item.DataItem, System.Data.DataRowView).Row, _
                Northwind.CategoriesRow)
        ' Reference the ProductsByCategoryDataSource ObjectDataSource
        Dim ProductsByCategoryDataSource As ObjectDataSource = _
            CType(e.Item.FindControl("ProductsByCategoryDataSource"), _
                ObjectDataSource)
        ' Set the CategoryID Parameter value
        ProductsByCategoryDataSource.SelectParameters("CategoryID").DefaultValue = _
            category.CategoryID.ToString()
    End If
End Sub

此事件处理程序首先确保我们处理的是数据项,而不是页眉、页脚或分隔符项。 接下来,我们引用刚刚绑定到当前 RepeaterItem的实际CategoriesRow实例。 最后,我们在 中 ItemTemplate 引用 ObjectDataSource,并将其 CategoryID 参数值 CategoryID 分配给当前 RepeaterItem的 。

使用此事件处理程序, ProductsByCategoryList 每个 RepeaterItem 中的 Repeater 将绑定到 s 类别中的 RepeaterItem 那些产品。 图 5 显示了生成的输出的屏幕截图。

每个类别Lists外部中继器;内部中继器Lists该类别的产品

图 5:每个类别Lists外部中继器;内部中继器Lists该类别的产品 (单击以查看全尺寸图像)

以编程方式按类别数据访问产品

无需使用 ObjectDataSource 检索当前类别的产品,我们可以在 ASP.NET 页的代码隐藏类 (、文件夹或 App_Code 单独的类库项目中创建一个方法,) 在 CategoryID传入 时返回相应的产品集。 假设我们在 ASP.NET 页的代码隐藏类中有这样一个方法,并且它被命名为 GetProductsInCategory(categoryID)。 使用此方法后,我们可以使用以下声明性语法将当前类别的产品绑定到内部中继器:

<asp:Repeater runat="server" ID="ProductsByCategoryList" EnableViewState="False"
      DataSource='<%# GetProductsInCategory(CType(Eval("CategoryID"), Integer)) %>'>
  ...
</asp:Repeater>

Repeater 属性 DataSource 使用数据绑定语法来指示其数据来自 GetProductsInCategory(categoryID) 方法。 由于 Eval("CategoryID") 返回类型的 Object值,因此在将 对象 Integer 传递到 方法之前,将对象 GetProductsInCategory(categoryID) 强制转换为 。 请注意,CategoryID此处通过数据绑定语法访问的 是CategoryID外部 Repeater (CategoryList) ,即绑定到表中记录Categories的 。 因此,我们知道 不能 CategoryID 是数据库 NULL 值,这就是为什么我们可以盲目地强制转换 Eval 方法而不检查是否正在处理 DBNull

使用此方法时,我们需要创建 方法, GetProductsInCategory(categoryID) 并让其检索给定的 categoryID相应产品集。 为此,只需返回 ProductsDataTable 类方法GetProductsByCategoryID(categoryID)返回的 ProductsBLL 。 让我们 GetProductsInCategory(categoryID) 在页面的代码隐藏类 NestedControls.aspx 中创建 方法。 使用以下代码执行此操作:

Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
    As Northwind.ProductsDataTable
    ' Create an instance of the ProductsBLL class
    Dim productAPI As ProductsBLL = New ProductsBLL()
    ' Return the products in the category
    Return productAPI.GetProductsByCategoryID(categoryID)
End Function

此方法只是创建 方法的 ProductsBLL 实例并返回方法的结果 GetProductsByCategoryID(categoryID) 。 请注意,方法必须标记为 PublicProtected;如果方法标记为 Private,则无法从 ASP.NET 页声明性标记访问该方法。

进行这些更改以使用此新技术后,请花点时间通过浏览器查看页面。 使用 ObjectDataSource 和 ItemDataBound 事件处理程序方法时,输出应与输出相同, (返回图 5 以查看) 屏幕截图。

注意

在 ASP.NET 页代码隐藏类中创建 GetProductsInCategory(categoryID) 方法似乎很忙。 毕竟,此方法只是创建 类的 ProductsBLL 实例并返回其 GetProductsByCategoryID(categoryID) 方法的结果。 为什么不直接从内部中继器中的数据绑定语法调用此方法,例如: DataSource='<%# ProductsBLL.GetProductsByCategoryID(CType(Eval("CategoryID"), Integer)) %>' 尽管此语法不适用于类 (的当前实现 ProductsBLL ,因为 GetProductsByCategoryID(categoryID) 该方法是) 的实例方法,但你可以修改 ProductsBLL 以包含静态 GetProductsByCategoryID(categoryID) 方法或让类包含静态 Instance() 方法以返回类的新实例 ProductsBLL

虽然此类修改将消除 ASP.NET 页代码隐藏类中方法的需要 GetProductsInCategory(categoryID) ,但代码隐藏类方法让我们在处理检索到的数据方面具有更大的灵活性,我们稍后将看到。

一次性检索所有产品信息

我们检查过的两种技术通过调用 ProductsBLL 类方法 GetProductsByCategoryID(categoryID) 获取当前类别的产品, (第一种方法通过 ObjectDataSource 进行调用,第二种方法通过 GetProductsInCategory(categoryID) 代码隐藏类) 中的 方法获取。 每次调用此方法时,业务逻辑层都会向下调用数据访问层,该层使用 SQL 语句查询数据库,该语句从 Products 表中 CategoryID 返回其字段与提供的输入参数匹配的行。

给定系统中 的 N 个类别,此方法将 N + 1 调用数据库一个数据库查询以获取所有类别,然后 N 个调用以获取特定于每个类别的产品。 但是,我们可以检索两个数据库调用中的所有所需数据,一个调用以获取所有类别,另一个调用以获取所有产品。 拥有所有产品后,可以筛选这些产品,以便仅将与当前 CategoryID 匹配的产品绑定到该类别的内部中继器。

若要提供此功能,只需对 ASP.NET 页代码隐藏类中的 方法进行轻微修改 GetProductsInCategory(categoryID) 。 我们可以先访问 ( 的所有产品(如果尚未) 访问它们),然后仅返回基于传入CategoryID的产品的筛选视图,而不是盲目返回 类 方法GetProductsByCategoryID(categoryID)的结果ProductsBLL

Private allProducts As Northwind.ProductsDataTable = Nothing
Protected Function GetProductsInCategory(ByVal categoryID As Integer) _
    As Northwind.ProductsDataTable
    ' First, see if we've yet to have accessed all of the product information
    If allProducts Is Nothing Then
        Dim productAPI As ProductsBLL = New ProductsBLL()
        allProducts = productAPI.GetProducts()
    End If
    ' Return the filtered view
    allProducts.DefaultView.RowFilter = "CategoryID = " & categoryID
    Return allProducts
End Function

请注意页面级变量 allProducts的添加。 这会保存有关所有产品的信息,并在首次调用方法时 GetProductsInCategory(categoryID) 填充。 在确保allProducts已创建并填充对象后,该方法会筛选 DataTable 的结果,以便仅可访问与指定 CategoryID 匹配的CategoryID行。 此方法将访问数据库的次数从 N + 1 减少到 2。

此增强功能不会对页面的呈现标记进行任何更改,也不会返回比其他方法更少的记录。 它只是减少了对数据库的调用次数。

注意

可以直观地推断,减少数据库访问数可以肯定地提高性能。 但是,情况可能并非如此。 例如,如果大量产品 CategoryIDNULL,则调用 GetProducts 方法将返回从不显示的大量产品。 此外,如果仅显示类别的子集(如果已实现分页),则返回所有产品可能会造成浪费。

与往常一样,在分析两种技术的性能时,唯一的肯定措施是运行针对应用程序常见情况定制的受控测试。

总结

在本教程中,我们了解了如何将一个数据 Web 控件嵌套在另一个控件中,具体介绍如何让外部中继器显示每个类别的项,其中包含一个内部 Repeater,其中列出了项目符号列表中的每个类别的产品。 构建嵌套用户界面main难题在于访问正确的数据并将其绑定到内部数据 Web 控件。 有多种可用的技术,其中两种是在本教程中介绍的。 检查的第一种方法在外部数据 Web 控件 ItemTemplate 中使用 ObjectDataSource,该对象通过属性 DataSourceID 绑定到内部数据 Web 控件。 第二种方法通过 ASP.NET 页代码隐藏类中的 方法访问数据。 然后,可以通过数据绑定语法将此方法绑定到内部数据 Web 控件的 DataSource 属性。

虽然本教程中介绍的嵌套用户界面使用了嵌套在 Repeater 中的 Repeater,但这些技术可以扩展到其他数据 Web 控件。 可以在 GridView 中嵌套 Repeater,或在 DataList 中嵌套一个 GridView,等等。

编程愉快!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。

特别感谢

本教程系列由许多有用的审阅者查看。 本教程的主要审阅者是 Zack Jones 和 Liz Shulok。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。