Nested Data Web Controls (C#)
DOWNLOAD ASSETS: Sample App or 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.
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
.
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.
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.
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 the ItemDataBound
Event 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.
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 [email protected]. 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 [email protected].
Comments (0) RSS Feed