嵌套的数据 Web 控件 (C#)

作者 :Scott Mitchell

下载 PDF

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

简介

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

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

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

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

列出每个类别及其产品

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

步骤 1:创建类别列表

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

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

将新建对象命名为DataSource 类别DataSource

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

配置 ObjectDataSource,使其从 CategoriesBLL 类的 方法 GetCategories 拉取其数据。

配置 ObjectDataSource 以使用 CategoriesBLL 类 getCategories 方法

图 3:将 ObjectDataSource 配置为使用 CategoriesBLL 类 s 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:添加嵌套产品中继器

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

若要创建此中继器,需要手动将内部中继器声明性语法和模板输入到 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 产品绑定到 ProductsByCategoryList 中继器

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

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

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

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

遗憾的是,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 s 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 void CategoryList_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
    if (e.Item.ItemType == ListItemType.AlternatingItem ||
        e.Item.ItemType == ListItemType.Item)
    {
        // Reference the CategoriesRow object being bound to this RepeaterItem
        Northwind.CategoriesRow category =
            (Northwind.CategoriesRow)((System.Data.DataRowView)e.Item.DataItem).Row;
        // Reference the ProductsByCategoryDataSource ObjectDataSource
        ObjectDataSource ProductsByCategoryDataSource =
            (ObjectDataSource)e.Item.FindControl("ProductsByCategoryDataSource");
        // Set the CategoryID Parameter value
        ProductsByCategoryDataSource.SelectParameters["CategoryID"].DefaultValue =
            category.CategoryID.ToString();
    }
}

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

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

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

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

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

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

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

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

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

protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
    // Create an instance of the ProductsBLL class
    ProductsBLL productAPI = new ProductsBLL();
    // Return the products in the category
    return productAPI.GetProductsByCategoryID(categoryID);
}

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

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

注意

在 ASP.NET 页代码隐藏类中创建 GetProductsInCategory(categoryID) 方法似乎很忙。 毕竟,此方法只是创建 类的 ProductsBLL 实例,并返回其 GetProductsByCategoryID(categoryID) 方法的结果。 为什么不直接从内部中继器中的数据绑定语法调用此方法,例如: DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>' 尽管此语法不适用于类 (的当前实现 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 Northwind.ProductsDataTable allProducts = null;
protected Northwind.ProductsDataTable GetProductsInCategory(int categoryID)
{
    // First, see if we've yet to have accessed all of the product information
    if (allProducts == null)
    {
        ProductsBLL productAPI = new ProductsBLL();
        allProducts = productAPI.GetProducts();
    }
    // Return the filtered view
    allProducts.DefaultView.RowFilter = "CategoryID = " + categoryID;
    return allProducts;
}

请注意页面级变量 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。