แก้ไข

แชร์ผ่าน


Nested Data Web Controls (C#)

by Scott Mitchell

Download PDF

In this tutorial we will explore how to use a Repeater nested inside another Repeater. The examples will illustrate how to populate the inner Repeater both declaratively and programmatically.

Introduction

In addition to static HTML and databinding syntax, templates can also include Web controls and User Controls. These Web controls can have their properties assigned via declarative, databinding syntax, or can be accessed programmatically in the appropriate server-side event handlers.

By embedding controls within a template, the appearance and user experience can be customized and improved upon. For example, in the Using TemplateFields in the GridView Control tutorial, we saw how to customize the GridView s display by adding a Calendar control in a TemplateField to show an employee s hire date; in the Adding Validation Controls to the Editing and Inserting Interfaces and Customizing the Data Modification Interface tutorials, we saw how to customize the editing and inserting interfaces by adding validation controls, TextBoxes, DropDownLists, and other Web controls.

Templates can also contain other data Web controls. That is, we can have a DataList that contains another DataList (or Repeater or GridView or DetailsView, and so on) within its templates. The challenge with such an interface is binding the appropriate data to the inner data Web control. There are a few different approaches available, ranging from declarative options using the ObjectDataSource to programmatic ones.

In this tutorial we will explore how to use a Repeater nested inside another Repeater. The outer Repeater will contain an item for each category in the database, displaying the category s name and description. Each category item s inner Repeater will display information for each product belonging to that category (see Figure 1) in a bulleted list. Our examples will illustrate how to populate the inner Repeater both declaratively and programmatically.

Each Category, Along with its Products, are Listed

Figure 1: Each Category, Along with its Products, are Listed (Click to view full-size image)

Step 1: Creating the Category Listing

When building a page that uses nested data Web controls, I find it helpful to design, create, and test the outermost data Web control first, without even worrying about the inner nested control. Therefore, let s start by walking through the steps necessary to add a Repeater to the page that lists the name and description for each category.

Start by opening the NestedControls.aspx page in the DataListRepeaterBasics folder and add a Repeater control to the page, setting its ID property to CategoryList. From the Repeater s smart tag, choose to create a new ObjectDataSource named CategoriesDataSource.

Name the New ObjectDataSource CategoriesDataSource

Figure 2: Name the New ObjectDataSource CategoriesDataSource (Click to view full-size image)

Configure the ObjectDataSource so that it pulls its data from the CategoriesBLL class s GetCategories method.

Configure the ObjectDataSource to Use the CategoriesBLL Class s GetCategories Method

Figure 3: Configure the ObjectDataSource to Use the CategoriesBLL Class s GetCategories Method (Click to view full-size image)

To specify the Repeater s template content we need to go to the Source view and manually enter the declarative syntax. Add an ItemTemplate that displays the category s name in an <h4> element and the category s description in a paragraph element (<p>). Furthermore, let s separate each category with a horizontal rule (<hr>). After making these changes your page should contain declarative syntax for the Repeater and ObjectDataSource that is similar to the following:

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

Figure 4 shows our progress when viewed through a browser.

Each Category s Name and Description is Listed, Separated by a Horizontal Rule

Figure 4: Each Category s Name and Description is Listed, Separated by a Horizontal Rule (Click to view full-size image)

Step 2: Adding the Nested Product Repeater

With the category listing complete, our next task is to add a Repeater to the CategoryList s ItemTemplate that displays information about those products belonging to the appropriate category. There are a number of ways we can retrieve the data for this inner Repeater, two of which we'll explore shortly. For now, let s just create the products Repeater within the CategoryList Repeater s ItemTemplate. Specifically, let s have the product Repeater display each product in a bulleted list with each list item including the product s name and price.

To create this Repeater we need to manually enter the inner Repeater s declarative syntax and templates into the CategoryList s ItemTemplate. Add the following markup within the CategoryList Repeater s 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>

Step 3: Binding the Category-Specific Products to the ProductsByCategoryList Repeater

If you visit the page through a browser at this point, your screen will look the same as in Figure 4 because we ve yet to bind any data to the Repeater. There are a few ways that we can grab the appropriate product records and bind them to the Repeater, some more efficient than others. The main challenge here is getting back the appropriate products for the specified category.

The data to bind to the inner Repeater control can either be accessed declaratively, through an ObjectDataSource in the CategoryList Repeater s ItemTemplate, or programmatically, from the ASP.NET page s code-behind page. Similarly, this data can be bound to the inner Repeater either declaratively - through the inner Repeater s DataSourceID property or through declarative databinding syntax or programmatically by referencing the inner Repeater in the CategoryList Repeater s ItemDataBound event handler, programmatically setting its DataSource property, and calling its DataBind() method. Let s explore each of these approaches.

Accessing the Data Declaratively with an ObjectDataSource Control and theItemDataBoundEvent Handler

Since we ve used the ObjectDataSource extensively throughout this tutorial series, the most natural choice for accessing data for this example is to stick with the ObjectDataSource. The ProductsBLL class has a GetProductsByCategoryID(categoryID) method that returns information about those products that belong to the specified categoryID. Therefore, we can add an ObjectDataSource to the CategoryList Repeater s ItemTemplate and configure it to access its data from this class s method.

Unfortunately, the Repeater doesn t allow its templates to be edited through the Design view so we need to add the declarative syntax for this ObjectDataSource control by hand. The following syntax shows the CategoryList Repeater s ItemTemplate after adding this new ObjectDataSource (ProductsByCategoryDataSource):

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

When using the ObjectDataSource approach we need to set the ProductsByCategoryList Repeater s DataSourceID property to the ID of the ObjectDataSource (ProductsByCategoryDataSource). Also, notice that our ObjectDataSource has an <asp:Parameter> element that specifies the categoryID value that will be passed into the GetProductsByCategoryID(categoryID) method. But how do we specify this value? Ideally, we d be able to just set the DefaultValue property of the <asp:Parameter> element using databinding syntax, like so:

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

Unfortunately, databinding syntax is only valid in controls that have a DataBinding event. The Parameter class lacks such an event and therefore the above syntax is illegal and will result in a runtime error.

To set this value, we need to create an event handler for the CategoryList Repeater s ItemDataBound event. Recall that the ItemDataBound event fires once for each item bound to the Repeater. Therefore, each time this event fires for the outer Repeater we can assign the current CategoryID value to the ProductsByCategoryDataSource ObjectDataSource s CategoryID parameter.

Create an event handler for the CategoryList Repeater s ItemDataBound event with the following code:

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

This event handler starts by ensuring that we re dealing with a data item rather than the header, footer, or separator item. Next, we reference the actual CategoriesRow instance that has just been bound to the current RepeaterItem. Finally, we reference the ObjectDataSource in the ItemTemplate and assign its CategoryID parameter value to the CategoryID of the current RepeaterItem.

With this event handler, the ProductsByCategoryList Repeater in each RepeaterItem is bound to those products in the RepeaterItem s category. Figure 5 shows a screen shot of the resulting output.

The Outer Repeater Lists Each Category; the Inner One Lists the Products for that Category

Figure 5: The Outer Repeater Lists Each Category; the Inner One Lists the Products for that Category (Click to view full-size image)

Accessing the Products by Category Data Programmatically

Instead of using an ObjectDataSource to retrieve the products for the current category, we could create a method in our ASP.NET page s code-behind class (or in the App_Code folder or in a separate Class Library project) that returns the appropriate set of products when passed in a CategoryID. Imagine that we had such a method in our ASP.NET page s code-behind class and that it was named GetProductsInCategory(categoryID). With this method in place we could bind the products for the current category to the inner Repeater using the following declarative syntax:

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

The Repeater s DataSource property uses the databinding syntax to indicate that its data comes from the GetProductsInCategory(categoryID) method. Since Eval("CategoryID") returns a value of type Object, we cast the object to an Integer before passing it into the GetProductsInCategory(categoryID) method. Note that the CategoryID accessed here via the databinding syntax is the CategoryID in the outer Repeater (CategoryList), the one that s bound to the records in the Categories table. Therefore, we know that CategoryID cannot be a database NULL value, which is why we can blindly cast the Eval method without checking if we re dealing with a DBNull.

With this approach, we need to create the GetProductsInCategory(categoryID) method and have it retrieve the appropriate set of products given the supplied categoryID. We can do this by simply returning the ProductsDataTable returned by the ProductsBLL class s GetProductsByCategoryID(categoryID) method. Let s create the GetProductsInCategory(categoryID) method in the code-behind class for our NestedControls.aspx page. Do so using the following code:

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

This method simply creates an instance of the ProductsBLL method and returns the results of the GetProductsByCategoryID(categoryID) method. Note that the method must be marked Public or Protected; if the method is marked Private, it will not be accessible from the ASP.NET page s declarative markup.

After making these changes to use this new technique, take a moment to view the page through a browser. The output should be identical to the output when using the ObjectDataSource and ItemDataBound event handler approach (refer back to Figure 5 to see a screen shot).

Note

It may seem like busywork to create the GetProductsInCategory(categoryID) method in the ASP.NET page s code-behind class. After all, this method simply creates an instance of the ProductsBLL class and returns the results of its GetProductsByCategoryID(categoryID) method. Why not just call this method directly from the databinding syntax in the inner Repeater, like: DataSource='<%# ProductsBLL.GetProductsByCategoryID((int)(Eval("CategoryID"))) %>'? Although this syntax won't work with our current implementation of the ProductsBLL class (since the GetProductsByCategoryID(categoryID) method is an instance method), you could modify ProductsBLL to include a static GetProductsByCategoryID(categoryID) method or have the class include a static Instance() method to return a new instance of the ProductsBLL class.

While such modifications would eliminate the need for the GetProductsInCategory(categoryID) method in the ASP.NET page s code-behind class, the code-behind class method gives us more flexibility in working with the data retrieved, as we'll see shortly.

Retrieving All of the Product Information at Once

The two pervious techniques we ve examined grab those products for the current category by making a call to the ProductsBLL class s GetProductsByCategoryID(categoryID) method (the first approach did so through an ObjectDataSource, the second through the GetProductsInCategory(categoryID) method in the code-behind class). Each time this method is invoked, the Business Logic Layer calls down to the Data Access Layer, which queries the database with a SQL statement that returns rows from the Products table whose CategoryID field matches the supplied input parameter.

Given N categories in the system, this approach nets N + 1 calls to the database one database query to get all of the categories and then N calls to get the products specific to each category. We can, however, retrieve all the needed data in just two database calls one call to get all of the categories and another to get all of the products. Once we have all of the products, we can filter those products so that only the products matching the current CategoryID are bound to that category s inner Repeater.

To provide this functionality, we only need to make a slight modification to the GetProductsInCategory(categoryID) method in our ASP.NET page s code-behind class. Rather than blindly returning the results of the ProductsBLL class s GetProductsByCategoryID(categoryID) method, we can instead first access all of the products (if they haven't been accessed already) and then return just the filtered view of the products based on the passed-in 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;
}

Note the addition of the page-level variable, allProducts. This holds information about all of the products and is populated the first time the GetProductsInCategory(categoryID) method is invoked. After ensuring that the allProducts object has been created and populated, the method filters the DataTable s results such that only those rows whose CategoryID matches the specified CategoryID are accessible. This approach reduces the number of times the database is accessed from N + 1 down to two.

This enhancement does not introduce any change to the rendered markup of the page, nor does it bring back fewer records than the other approach. It simply reduces the number of calls to the database.

Note

One might intuitively reason that reducing the number of database accesses would assuredly improve performance. However, this might not be the case. If you have a large number of products whose CategoryID is NULL, for example, then the call to the GetProducts method returns a number of products that are never displayed. Moreover, returning all of the products can be wasteful if you re only showing a subset of the categories, which might be the case if you have implemented paging.

As always, when it comes to analyzing the performance of two techniques, the only surefire measure is to run controlled tests tailored for your application s common case scenarios.

Summary

In this tutorial we saw how to nest one data Web control within another, specifically examining how to have an outer Repeater display an item for each category with an inner Repeater listing the products for each category in a bulleted list. The main challenge in building a nested user interface lies in accessing and binding the correct data to the inner data Web control. There are a variety of techniques available, two of which we examined in this tutorial. The first approach examined used an ObjectDataSource in the outer data Web control s ItemTemplate that was bound to the inner data Web control through its DataSourceID property. The second technique accessed the data via a method in the ASP.NET page s code-behind class. This method can then be bound to the inner data Web control s DataSource property through databinding syntax.

While the nested user interface examined in this tutorial used a Repeater nested within a Repeater, these techniques can be extended to the other data Web controls. You can nest a Repeater within a GridView, or a GridView within a DataList, and so on.

Happy Programming!

About the Author

Scott Mitchell, author of seven ASP/ASP.NET books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies since 1998. Scott works as an independent consultant, trainer, and writer. His latest book is Sams Teach Yourself ASP.NET 2.0 in 24 Hours. He can be reached at mitchell@4GuysFromRolla.com. or via his blog, which can be found at http://ScottOnWriting.NET.

Special Thanks To

This tutorial series was reviewed by many helpful reviewers. Lead reviewers for this tutorial were Zack Jones and Liz Shulok. Interested in reviewing my upcoming MSDN articles? If so, drop me a line at mitchell@4GuysFromRolla.com.