嵌套的 Web 数据控件32

简介

除了静态 HTML 和数据绑定语法,模板还可以包括 Web 控件和用户控件。这些 Web 控件可以有它们的属性,可以通过声明、数据绑定语法为属性赋值,或者在适当的服务器端 event handler 中通过编程访问它们。

通过将控件嵌入到模板内,可以自定义和改进外观和用户体验。例如,在“ 在 GridView 控件中使用TemplateFields” 教程中,我们讨论如何通过在 TemplateField 中添加 Calendar 控件以显示员工的雇佣日期来自定义 GridView 的显示;在 向编辑和插入界面添加校验控件 和 自定义数据修改界面 教程中,我们讨论如何通过添加校验控件、TextBoxes 、DropDownLists 和其他 Web 控件来自定义编辑和插入界面。

模板还可以包含其他 Web 控件。即,可以有一个在其模板内包含另一个 DataList (或 Repeater ,或 GridView ,或 DetailsView 等)的 DataList。开发这样界面的挑战是将适当的数据绑定到内部 Web 数据控件。从使用 ObjectDataSource 的声明性选项到一些编程方法,有几个方法可以使用。

本教程中 , 我们将了解怎样使用嵌入到另一个 Repeater 内的 Repeater 。外层 Repeater 将在数据库中包含每个类别的项,显示类别名称和描述。每个类别项的内层 Repeater 将以列表的形式显示属于该类别的每种产品的信息(参见图 1 )。我们的示例将说明如何通过声明和编程填充内层。

图1 :每种类别以及该类产品都列出来

步骤1 :创建类别列表

当构建使用嵌套 Web 数据控件的网页时,我发现首先设计、创建和测试最外层的 Web 数据控件,而不去担心内部嵌套的控件,是很有帮助的。因此,我们首先向网页添加一个Repeater控件,列出每个类别的名称和描述 。

首先打开 DataListRepeaterBasics 文件夹中的 NestedControls.aspx 页并向该页添加一个 Repeater 控件,将它的 ID 属性设置为 CategoryList 。从 Repeater 的智能标记,选择创建新的名称为 CategoriesDataSource 的 ObjectDataSource。

图2 :将新的 ObjectDataSource 命名为 CategoriesDataSource

配置 ObjectDataSource ,使其可以从 CategoriesBLL 类的 GetCategories 方法提取它的数据。

图3 :配置新的ObjectDataSource ,从而使用 CategoriesBLL 类的 GetCategories 方式

若要指定 Repeater 的模板内容,需要转到 “源” 视图并手动输入声明式语法。添加一个在 <h4> 元素中显示类别名称和在段落元素 (<p>) 中显示描述的 ItemTemplate 。此外,使用一条横线 (<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

完成类别列表之后,下一个任务是向 CategoryList 的用于显示属于相应类别产品信息的 ItemTemplate 添加 Repeater 。有许多方法可以用来检索此内层 Repeater 的数据,我们将介绍其中的两个。现在,我们只在 CategoryList Repeater 的 ItemTemplate 内创建产品 Repeater 。具体来说,我们让产品 Repeater 以列表的形式显示每种产品,其中每个列表项包括产品的名称和价格。

若要创建此 Repeater ,需要手动将内层 Repeater 的声明式语法和模板输入到 CategoryList 的ItemTemplate 。在 CategoryList Repeater 的 ItemTemplate 内添加以下标记:

<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 :将特定类别的产品绑定到 ProductsByCategoryList Repeater

如果此时通过浏览器访问该页面,屏幕将与图 4 一样,因为我们还未将任何数据绑定到 Repeater 。有几种方法可以获得相应产品的记录并将它们绑定到 Repeater ,其中一些方法比另外一些方法更有效。此处的主要挑战是返回指定类别的相应产品。

要绑定到内层 Repeater 控件的数据可以通过 CategoryList Repeater 的 ItemTemplate 中的 ObjectDataSource 通过声明访问,或者从 ASP.NET 页的code-behind 页通过编程访问。同样地,此数据可以通过声明绑定到内层 Repeater—— 通过内层 Repeater 的 DataSourceID 属性或通过声明式数据绑定语法—— 或者通过编程绑定到内层 Repeater—— 通过在 CategoryList Repeater 的 ItemDataBound event handler 中引用内层 Repeater ,通过编程设置它的 DataSource 属性,并调用它的 DataBind() 方法。下面对每种方法进行探讨。

使用 ObjectDataSource 控件和 ItemDataBound event handler 通过声明访问数据

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

遗憾的是,Repeater 不允许通过 “设计” 视图编辑它的模板,因此需要为此 ObjectDataSource 控件手动添加声明式语法。添加此新的 ObjectDataSource (ProductsByCategoryDataSource) 之后,下面的代码显示 CategoryList Repeater 的 ItemTemplate :

<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 这种方法时,我们需要将 ProductsByCategoryList Repeater 的DataSourceID 属性设置为 ObjectDataSource (ProductsByCategoryDataSource) 的ID 。此外,注意 ObjectDataSource 有一个 <asp:Parameter> 元素,它指定将要传递到 GetProductsByCategoryID(categoryID) 方法的 categoryID 的值。但如何指定此值呢?理想情况下,只能使用数据绑定语法设置 <asp:Parameter> 元素的 DefaultValue 属性,如下所示:

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

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

要设置此值,需要为 CategoryList Repeater 的 ItemDataBoundTo 创建一个 event handler。记得 ItemDataBound 事件会为每个绑定到 Repeater 的项触发一次。因此,每当此事件为外层 Repeater 而触发时,我们可以将当前的 CategoryID 值赋给 ProductsByCategoryDataSource ObjectDataSource 的 CategoryID 参数。

通过以下代码为 CategoryList Repeater 的ItemDataBound 事件创建一个 event handler :

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(); 
    } 
}

此event handler 首先确保我们正在处理数据项而不是页眉、页脚或分隔符项。然后,引用刚绑定到当前的 RepeaterItem 的实际的 CategoriesRow 实例。最后,在ItemTemplate 中引用ObjectDataSource 并将它的CategoryID 参数值赋给当前的RepeaterItem 的 CategoryID 。

通过此 event handler,每个RepeaterItem 中的 ProductsByCategoryList Repeater 空间,将会被绑定到属于RepeaterItem 的类别的那些产品。图5 显示得到的输出的屏幕快照。

图5 :外层 Repeater 列出每种类别;内层 Repeater 列出该类别的产品

通过编程按类别数据访问产品

不使用 ObjectDataSource 来检索当前类别的产品,我们可以在 ASP.NET 页的 code-behind 类(或者在 App_Code 文件夹或在单独的Class Library 项目) 中创建一个方法,当传入一个CategoryID 时该方法返回相应的一组产品。假设在 ASP.NET 页的 code-behind 类中有这样的方法并且它的名称为 GetProductsInCategory(categoryID) 。有了此方法之后,我们可以使用以下的声明式语法将当前类别的产品绑定到内层 Repeater :

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

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

使用此方法,需要创建 GetProductsInCategory(categoryID) 方法并让它检索相应的一组指定了categoryID 的产品。可以通过只返回由 ProductsBLL 类的 GetProductsByCategoryID(categoryID) 方法返回的 ProductsDataTable 来完成这一任务。在 code-behind 类中为 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) 的结果。请注意,方法必须标记为 Public 或 Protected ;如果方法标记为 Private ,它在 ASP.NET 页的声明式标记中将不可访问。

为使用此新的方法而做出这些更改之后,花些时间通过浏览器查看页面。输出应该与使用 ObjectDataSource 和 ItemDataBound event handler 这种方法时的输出相同(请参阅图5 以查看屏幕快照)。

注意:它可能像在 ASP.NET 页的 code-behind 类中创建 GetProductsInCategory(categoryID) 方法的作业一样。毕竟,此方法只是创建 ProductsBLL 类的一个实例并返回它的 GetProductsByCategoryID(categoryID) 方法的结果。为什么不直接从内层 Repeater 中的数据绑定语法调用此方法,如下所示:DataSource='<%# ProductsBLL.GetProductsByCategoryID(CType(Eval("CategoryID"), Integer)) %>' 虽然此语法不适用于 ProductsBLL 类的当前实现(因为 GetProductsByCategoryID(categoryID) 方法是实例方法),但是可以修改ProductsBLL 类,使其包含一个静态 GetProductsByCategoryID(categoryID) 方法或者让类包含一个静态 Instance() 方法以返回 ProductsBLL 类的新实例。

虽然这种修改使得在 ASP.NET 页的 code-behind 类中不再需要 GetProductsInCategory(categoryID) 方法,但 code-behind 类方法在处理数据检索时可以为我们提供更大的灵活性,我们马上就会看到。

一次检索所有的产品信息

我们前面已经介绍过的两种方法通过调用ProductsBLL 类的 GetProductsByCategoryID(categoryID) 方法获取当前类别的产品信息(第一种方法通过 ObjectDataSource 、第二种方法通过 code-behind 类中的 GetProductsInCategory(categoryID) 方法)。每次调用此方法,业务逻辑层调用数据访问层,这将使用 SQL 语句来查询数据库,返回 CategoryID 字段与提供的输入参数匹配的 Products 表中的行。

假设系统中有 N 种类别,此方法对数据库进行 N+1 次调用—— 一次数据库查询用以获得所有的类别,N 次调用用来获得特定于每种类别的产品。但是,我们可以仅通过两次数据库调用检索所有需要的数据 —— 一次调用获取所有的类别,另一次调用获取所有的产品。当获得所有的产品之后,可以对这些产品进行筛选以便使与当前的 CategoryID 匹配的产品才绑定到该类别的内层 Repeater 。

若要提供此功能,只需对 ASP.NET 页的 code-behind 类中的 GetProductsInCategory(categoryID) 方法略做修改。不选择轻率地返回 ProductsBLL 类的GetProductsByCategoryID(categoryID) 方法,改为首先访问所有的产品(如果还没有访问它们),然后根据传入的 CategoryID 只返回经过筛选的产品的视图。

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 。

此增强没有引入任何对页面的呈现标记的更改,也没有比其他方法返回更少的记录。它只是减少了对数据库调用的次数。

注意:从直觉上看,减少数据库访问次数的一个可能原因是这样无疑可以提高性能。但是,实际可能并非如此。例如,如果有大量 CategoryID 为 NULL 的产品,对 GetProducts 方法的调用将返回大量从不显示的产品。此外,如果只显示类别的子集,返回所有的产品可能是浪费的,如果已经实现分页可能就是如此。

如往常一样,就分析两种技术的性能而言,惟一可靠的方法是运行专为应用程序的常见案例情况设计的受控测试。

小结

本教程中,我们了解了怎样在一个 Web 数据控件内嵌套另一个 Web 数据控件,专门探讨了如何使外层 Repeater 显示每种类别的条目,使内层 Repeater 以列表的形式列出每种类别的产品。构建嵌套的用户界面的主要挑战在于访问并将正确的数据绑定到内层 Web 数据控件。有多种方法可供使用,本教程中介绍了其中的两种。介绍的第一种方法在外层 Web 数据控件的 ItemTemplate 中使用 ObjectDataSource ,ObjectDataSource 通过它的 DataSourceID 属性绑定到内层 Web 数据控件。第二种方法通过 ASP.NET 页的 code-behind 类中的方法访问数据。然后此方法可以通过数据绑定语法绑定到内层 Web 数据控件的 DataSource 属性。

虽然本教程中介绍的嵌套用户界面使用嵌入到Repeater 中的 Repeater ,但是这些方法可以扩展到其他 Web 数据控件。可以在 GridView 内嵌套 Repeater ,或者在 DataList 内嵌套 GridView 等等。

快乐编程!

posted @ 2016-05-01 23:09  迅捷之风  阅读(99)  评论(0编辑  收藏  举报