INTRODUCTION
Questions regarding databinding, in one form or another, are probably the most asked in the ASP.NET newsgroups. It's clear everyone loves the idea of databinding but that more advanced functionality, such as event handling, conditional formatting and fine-tuning, aren't straightforward. The goal of this tutorial is to shed light on some of the more common and frequently asked questions about the capabilities of databinding.
THE SAMPLE PROGRAM
Throughout this tutorial, we'll use two separate data sources. The first will be your every-day
DataSet
, the other will be a strongly-typed custom collection containing strongly-typed objects.
Our
DataSet
will contain two tables,
Customers
and
Orders
:
Customer Structure | | Order Structure |
Name | Type | Description | Name | Type | Description |
CustomerId 1 | Int32 | Unique customer identifier | OrderId | Int32 | Unique order identifier |
Name | String | Name of the customer | CustomerId 1 | Int32 | Identifier of the customer who placed the order |
Zip | String | Customer's primary ZIP or Portal code | Ordered | DateTime | Date the order was placed on |
Enabled | Boolean | Whether the customer is currently active/enabled | Amount | Decimal | Dollar value of the order |
1A
DataRelation
exists between the
Customer.CustomerId
and
Order.CustomerId
columns.
Our
business entities will consist of an
Owner
and a
Pet
class:
Owner Structure | | Pets Structure |
Name | Type | Description | Name | Type | Description |
OwnerId | Int32 | Unique owner identifier | PetId | Int32 | Unique pet identifier |
YearOfBirth | Int32 | The year the owner was born in | Name | String | Name of the pet |
FirstName | String | Owner'sfirst name | IsNeutured | Boolean | Whether or not the pet is neutered |
LastName | String | Owner's last name | Type | PetType | Indicates the type of pet (Dog , Cat , Fish , Bird ,Rodent , Other ) |
Pets | PetCollection | Collection of pets the owner has | |
UNDERSTANDING DATAITEM
You've undoubtedly made frequent use of the
DataItem
property, namely when using the DataBinding syntax, to
output a value:
Hide Copy Code
1: <%# DataBinder.Eval(Container.DataItem, "customerId") %>
It's important to understand that
DataItem
is actually an object, and that when you use the
DataBinder.Eval
function, it basically needs to figure out what type of object it is and
how to get "
customerId
" from it. That's because your data source can be different things, such as a
DataSet
or
DataView
, an
ArrayList
or
HashTable
, a custom collection, and more.
Binding happens on a row-by-row basis, and
DataItem
actually represents the current row being
bound. For a
DataSet
,
DataTable
, or
DataView
,
DataItem
is actually an instance of
DataRowView
. (You might think that the
DataItem
for a
DataSet
or
DataTable
would be an instance of
DataRow
,
but when you bind either of these, the
DefaultView
is actually used, therefore
DataItem
will always be a
DataRowView
.) When you are
binding to a collection,
DataItem
is an instance of the
item within the collection. We can observe this more clearly with the following code:
Hide Shrink Copy Code
1: <%@ Import namespace="System.Data" %>
2: <%@ Import namespace="BindingSample" %>
3: <asp:Repeater id="dataSetRepeater" Runat="server">
4: <ItemTemplate>
5: <%# ((DataRowView)Container.DataItem)["customerId"] %> -
6: <%# ((DataRowView)Container.DataItem)["Name"] %> <br />
7: </ItemTemplate>
8: <AlternatingItemTemplate>
9: <%# DataBinder.Eval(Container.DataItem, "customerId") %> -
10: <%# DataBinder.Eval(Container.DataItem, "Name") %> <br />
11: </AlternatingItemTemplate>
12: </asp:Repeater>
13:
14: <br><br>
15:
16: <asp:Repeater id="collectionRepeater" Runat="server">
17: <ItemTemplate>
18: <%# ((Owner)Container.DataItem).OwnerId %> -
19: <%# ((Owner)Container.DataItem).FirstName %> <br />
20: </ItemTemplate>
21: <AlternatingItemTemplate>
22: <%# DataBinder.Eval(Container.DataItem, "OwnerId") %> -
23: <%# DataBinder.Eval(Container.DataItem, "FirstName") %> <br />
24: </AlternatingItemTemplate>
25: </asp:Repeater>
In the first
Repeater
, we are binding to a
DataSet
, the
ItemTemplate
shows how to access values by casting
DataItem
to a
DataRowView
[5, 6], the
AlternateItemTemplate
will output the same information but through
DataBinder.Eval
[9, 10].
In
the second Repeater
, we bind to a custom collection. Again, the
ItemTemplate
showshow to cast
DataItem
to the right type and access the fields
directly [18, 19] while the
AlternateItemTemplate
shows how the same is accomplished with
DataBinder.Eval
[22, 23].
In both cases, the
ItemTemplate
and
AlternateItemTemplate
will output the exact same information. The only difference is how the information is
retrieved.
DataBinder.Eval
is far less performing, but has the
benefit of being ignorant of the underlying structure, making it both quicker to develop and more likely to resist future changes. The goal here isn't to discuss the merits of these approaches, but simply show what
DataItem
truly is in order to build a proper foundation of understanding.
FORMATTING
Inline
While binding, it's possible to do simple formatting directly in the databinding expression or by calling functions which reside in code-behind.
Hide Shrink Copy Code
1: <asp:Repeater id="dataSetRepeater" Runat="server">
2: <ItemTemplate>
3: <%# DataBinder.Eval(Container.DataItem, "OrderId")%> -
4: <%# FormatDate(DataBinder.Eval(Container.DataItem, "Ordered"))%> -
5: <%# FormatMoney(DataBinder.Eval(Container.DataItem,
"Amount"))%> <br />
6: </ItemTemplate>
7: </asp:Repeater>
8:
9: <br ><br >
10:
11: <asp:Repeater id="collectionRepeater" Runat="server">
12: <ItemTemplate>
13: <%# DataBinder.Eval(Container.DataItem, "OwnerId") %> -
14: <asp:literal ID="see" Runat="server"
15: Visible='<%# (int)DataBinder.Eval(Container.DataItem,
"Pets.Count") > 0 %>'>
16: see pets
17: </asp:Literal>
18: <asp:literal ID="nopets" Runat="server"
19: Visible='<%# (int)DataBinder.Eval(Container.DataItem,
"Pets.Count") == 0 %>'>
20: no pets
21: </asp:Literal>
22: <br />
23: </ItemTemplate>
24: </asp:Repeater>
The second
Repeater
makes use of directly embedded expressions to toggle the visibility of certain controls [15, 19]. The first
Repeater
, which is
bound to all Orders, makes use of two functions:
FormatDate
[4] and
FormatMoney
[5]. These methods could look something like:
Hide Copy Code
1: protected string FormatDate(object date) {
2: if (date == DBNull.Value){
3: return "n/a";
4: }
5: try{
6: return ((DateTime)date).ToShortDateString();
7: }catch{
8: return "n/a";
9: }
10: }
11: protected string FormatMoney(object amount) {
12: if (amount == DBNull.Value){
13: return String.Format("{0:C}", 0);
14: }
15: return String.Format("{0:C}", amount);
16: }
OnItemDataBound
While the above method is suitable for quick and simple problems, it lacks in
elegance and capacity. Indeed, the 2
nd example shows a serious lack of grace, and dangerously blends presentation logic with UI. Avoiding burdening your presentation layer with any code is a practice worth eternal vigilance. To help accomplish this, the
Repeater
,
DataList
and
DataGrid
all expose a very powerful and useful event:
OnItemDataBound
.
OnItemDataBound
is
fired for each row being
bound to your datasource (in addition to when other templates are bound (header, footer,
pager, ..)). It not only exposes the
DataItem
being used in binding, but also the
complete template.
OnItemDataBound
starts to fire as
soon as the
DataBind()
method is called on the
Repeater
/
DataList
/
DataGrid
.
Using
OnItemDataBound
lets us
exercise fine control over exactly what happens during binding
in a clean and robust framework. For example, reworking the 2
nd Repeater
from above, we get:
Hide Copy Code
1: <asp:Repeater OnItemDataBound="itemDataBoundRepeater_ItemDataBound"
id="itemDataBoundRepeater" Runat="server">
2: <ItemTemplate>
3: <%# DataBinder.Eval(Container.DataItem, "OwnerId") %> -
4: <asp:Literal ID="see" Runat="server" /> <br />
5: </ItemTemplate>
6: </asp:Repeater>
Notice that our previously code-cluttered
ItemTemplate
is now considerably cleaner - this is because we've pushed the logic to the
itemDataBoundRepeater_ItemDataBound
function in code-behind:
Hide Copy Code
1: protected void itemDataBoundRepeater_ItemDataBound(object source,
RepeaterItemEventArgs e) {
2: if (e.Item.ItemType == ListItemType.AlternatingItem ||
e.Item.ItemType == ListItemType.Item){
3: Literal lit = (Literal)e.Item.FindControl("see");
4: if (lit != null){
5: Owner owner = (Owner)e.Item.DataItem;
6: if (owner.Pets.Count == 0){
7: lit.Text = "no pets";
8: }else{
9: lit.Text = "see pets";
10: }
11: }
12: }
13: }
Since we are dealing with
Repeater
s,
e.Item
returns a
reference to the current
RepeaterItem
. If this was a
DataList
, it would return a
reference to a
DataListItem
, or a
DataGridItem
if it were a
DataGrid
. For the most part however, all three provide the same capabilities. The first
thing to do is check the
ItemType
and make sure we are currently dealing with an
AlternateItem
or an
Item
[2]. Next, get a reference to our
Literal
[3],
this is an extremely powerful capability which allows us to really keep our UI clean. As we saw in a previous section, we can cast
DataItem
directly to the individual item being bound (in this case
Owner
, but again, if we bind to a
DataSet
, it would be a
DataRowView
) [5]. Finally, all the pieces are in place to apply our presentation logic [6-10].
An alternative to using
e.Item.FindControl()
is to refer to the controls by position via
e.Item.Controls[INDEX]
. While this may be considerably faster, it really makes the UI inflexible to basic changes (else you face constantly changing the code). Additionally, white spaces and newlines are actually controls. So in the above code, you'd get:
Hide Copy Code
1: e.Item.Controls[0] 2: e.Item.Controls[1]
Which is both an unexpected behavior and one very hard to cleanly deal with.
When it comes to
OnItemDataBound
, the
sky is the limit. Here, we've only shown a basic example of what can be done, and though we will see other, more complex examples, we won't cover every possibility.
OnItemCreated
Another useful event
exposed by these controls is
OnItemCreated
. The key difference between the two is that
OnItemDataBound
only
fires when the control is bound - that is, when you are posting back and the control is recreated from the viewstate,
OnItemDataBound
doesn't fire.
OnItemCreated
, on the other hand, fires when a control is bound
as well as when the control is recreated from the viewstate. The following example shows this subtle difference:
Hide Copy Code
1: <asp:Repeater OnItemCreated="repeater_ItemCreated"
OnItemDataBound="repeater_ItemDataBound"
id="repeater" Runat="server">
2: <ItemTemplate>
3: <asp:Literal EnableViewState="False" ID="event" Runat="server" /> <br />
4: </ItemTemplate>
5: </asp:Repeater>
6:
7: <asp:Button ID="btn" Runat="server" Text="Click Me!" />
Here, we have a
Repeater
with both the
OnItemCreated
and
OnItemDataBound
events hooked [1]. Additionally, we have a
single Literal
whose viewstate is
disabled (if it was enabled, we couldn't see the difference) [3]. And, we have a button that'll do nothing but postback [7]. Our code-behind looks like:
Hide Copy Code
1: private void Page_Load(object sender, EventArgs e) {
2: if (!Page.IsPostBack){
3: repeater.DataSource = CustomerUtility.GetAllOrders();
4: repeater.DataBind();
5: }
6: }
7: protected void repeater_ItemDataBound(object source,
RepeaterItemEventArgs e) {
8: if (e.Item.ItemType == ListItemType.AlternatingItem
|| e.Item.ItemType == ListItemType.Item){
9: Literal lit = (Literal)e.Item.FindControl("event");
10: if (lit != null){
11: lit.Text += " - ItemDataBound";
12: }
13: }
14: }
15: protected void repeater_ItemCreated(object source,
RepeaterItemEventArgs e) {
16: if (e.Item.ItemType == ListItemType.AlternatingItem ||
e.Item.ItemType == ListItemType.Item){
17: Literal lit = (Literal)e.Item.FindControl("event");
18: if (lit != null){
19: lit.Text += "ItemCreated";
20: }
21: }
22: }
When the page is first loaded,
Page.IsPostBack
returns
false
[2] and our
Repeater
is bound to all orders [3, 4]. Calling
DataBind()
causes the
ItemCreated
event to fire for the first row, followed by the
ItemDataBound
event - in our example, each will fire, one after the other, 11 times (since there are 11 orders). As we can see,
ItemCreated
and
ItemDataBound
merely take the
Literal
and append the texts "ItemCreated" and "ItemDataBound" respectively. The difference happens when our button is clicked. This causes
Page_Load
to fire, but this time
Page.IsPostBack
evaluates to
true
, thus skipping the binding [3, 4]. Only when the page enters its
Begin PreRender
stage will the
ItemCreated
event fire (again, once for each row), but this time it won't be followed by the
ItemDataBound
.
The really important thing to keep in mind is that when
ItemCreated
fires because of databinding,
e.Item.DataItem
will be what you expect - a reference to the individual row being bound.
However, when ItemCreated
is fired from being re-created from the viewstate,e.Item.DataItem
will be NULL
. If you think about it, this makes sense. The entire data source isn't stored in the viewstate, only the individual controls and their values. As such, it's impossible to have access to the individual rows of data originally used when binding. Of course, this can lead to very buggy code. For example, if we took our previous
ItemDataBound
example and moved it to the
ItemCreated
event:
Hide Copy Code
1: protected void itemCreatedRepeater_ItemCreatedobject source,
RepeaterItemEventArgs e) {
2: if (e.Item.ItemType == ListItemType.AlternatingItem
|| e.Item.ItemType == ListItemType.Item){
3: Literal lit = (Literal)e.Item.FindControl("see");
4: if (lit != null){
5: Owner owner = (Owner)e.Item.DataItem;
6: if (owner.Pets.Count == 0){
7: lit.Text = "no pets";
8: }else{
9: lit.Text = "see pets";
10: }
11: }
12: }
13: }
When the page first loads, the above code will work fine. But if the page is posted back,
e.Item.DataItem
will be
null
, resulting in a runtime null reference error.
NESTED BINDING
Another common requirement is to nest controls within each other. Both of our sample data has a one to many relationship and are therefore ideal candidates. Our
Customers
DataSet
has a
DataRelation
set up between the
Customer
's
customerId
and the
Order
's
customerId
:
Hide Copy Code
1: ds.Relations.Add(new DataRelation("CustomerOrders",
ds.Tables[0].Columns["CustomerId"],
ds.Tables[1].Columns["CustomerId"]));
And our
Owner
s have a
Pets
property which is a collection of all the pets they own.
The two ways that we'll look at nesting
Repeater
s is via inline binding and using
OnItemDataBound
.
Inline
Hide Copy Code
1: <asp:Repeater id="dataSetCasting" Runat="server">
2: <HeaderTemplate>
3: <ul>
4: </HeaderTemplate>
5: <ItemTemplate>
6: <li><%# ((DataRowView)Container.DataItem)["Name"]%>
7: <ul>
8: <asp:Repeater ID="orders" DataSource='<%#
((DataRowView)Container.DataItem).CreateChildView("CustomerOrders")%>'
Runat="server">
9: <ItemTemplate>
10: <li><%# ((DataRowView)Container.DataItem)["Amount"]%></li>
11: </ItemTemplate>
12: </asp:Repeater>
13: </ul>
14: </li>
15: </ItemTemplate>
16: <FooterTemplate>
17: </ul>
18: </FooterTemplate>
19: </asp:Repeater>
The important part being when we set the
DataSource
of our inner
Repeater
[8]. The
CreateChildView
function in our
DataRowView
is used in conjunction with the name of our
DataRelationship
to return a
DataView
of all child records. Alternatively, using the
DataBinder.Eval
, we could simply use:
Hide Copy Code
1: <asp:Repeater ID="orders"
DataSource='<%# DataBinder.Eval(Container.DataItem, "CutomerOrders")%>'
Runat="server">
Again, we use the
CustomerOrders
DataRelation
which we created, but let the
DataBinder.Eval
handle everything else.
Nesting with custom collections is even easier. Since
Owner
s have a property called
Pets
which is a custom collection of all the pets they own, we can simply:
Hide Copy Code
1: <asp:Repeater id="collectionCasting" Runat="server">
2: <HeaderTemplate>
3: <ul>
4: </HeaderTemplate>
5: <ItemTemplate>
6: <li><%# ((Owner)Container.DataItem).FirstName%>
7: <ul>
8: <asp:Repeater ID="pets"
DataSource="<%# ((Owner)Container.DataItem).Pets%>"
Runat="server">
9: <ItemTemplate>
10: <li><%# ((Pet)Container.DataItem).Name%></li>
11: </ItemTemplate>
12: </asp:Repeater>
13: </ul>
14: </li>
15: </ItemTemplate>
16: <FooterTemplate>
17: </ul>
18: </FooterTemplate>
19: </asp:Repeater>
Or using
DataBinder.Eval
:
Hide Copy Code
1: <asp:Repeater ID="pets"
DataSource='<%# DataBinder.Eval(Container.DataItem, "Pets")%>'
Runat="server">
OnItemDataBound
If something is doable using inline ASPX, it's doable via
onItemDataBound
. Deciding which method to use often depends on which you feel is cleaner and more flexible. We'll only look at one example, since it's basically the same as the above code, except the binding logic is moved to code-behind:
Hide Copy Code
1: <asp:Repeater OnItemDataBound="dataSetCasting_ItemDataBound"
id="dataSetCasting" Runat="server">
2: <HeaderTemplate>
3: <ul>
4: </HeaderTemplate>
5: <ItemTemplate>
6: <li><%# ((DataRowView)Container.DataItem)["Name"]%>
7: <ul>
8: <asp:Repeater ID="orders" Runat="server">
9: <ItemTemplate>
10: <li><%# ((DataRowView)Container.DataItem)["Amount"]%></li>
11: </ItemTemplate>
12: </asp:Repeater>
13: </ul>
14: </li>
15: </ItemTemplate>
16: <FooterTemplate>
17: </ul>
18: </FooterTemplate>
19: </asp:Repeater>
Notice that our inner
Repeater
doesn't have a
DataSource
property [8], however our outer
Repeater
does specify an
OnItemDataBound
function [1], let's look at it:
Hide Copy Code
1: protected void dataSetCasting_ItemDataBound(object s,
RepeaterItemEventArgs e) {
2: if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType
== ListItemType.AlternatingItem){
3: Repeater rpt = (Repeater)e.Item.FindControl("orders");
4: if (rpt != null){
5: rpt.DataSource =
((DataRowView)e.Item.DataItem).CreateChildView("CustomerOrders");
6: rpt.DataBind();
7: }
8: }
9: }
Basically, the same thing is happening as we saw before, except this is happening out of the UI.
HANDLING EVENTS
The last thing to discuss is how to handle events raised by controls inside your
Repeater
/
DataList
/
DataGrid
. Events raised from controls inside your
Repeater
bubble up to the
Repeater
and are exposed via the
OnItemCommand
event.
LinkButton
s and
Button
s have a
CommandArgument
and
CommandName
property which lets the
OnItemCommand
handler figure out which button was clicked, for example:
Hide Copy Code
1: <asp:Repeater OnItemCommand="eventRepeater_ItemCommand"
id="eventRepeater" Runat="server">
2: <ItemTemplate>
3: <%# DataBinder.Eval(Container.DataItem, "Name")%>
4: <asp:LinkButton ID="delete"
5: Runat="server"
6: CommandName="Delete"
7: CommandArgument='<%# DataBinder.Eval(Container.DataItem,
"CustomerId") %>'>
8: Delete Customer
9: </asp:LinkButton>
10: -
11: <asp:LinkButton ID="addOrder"
12: Runat="server"
13: CommandName="Add"
14: CommandArgument='<%# DataBinder.Eval(Container.DataItem,
"CustomerId") %>'>
15: Add Order
16: </asp:LinkButton>
17: <br />
18: </ItemTemplate>
19: </asp:Repeater>
In the above code, two
LinkButton
s can raise events, either deleting the customer [4-9] or adding an order [11-16]. Also note that the
ItemCommand
is hooked up [1]:
Hide Copy Code
1: protected void eventRepeater_ItemCommand(object s,
RepeaterCommandEventArgs e) {
2: int customerId = Convert.ToInt32(e.CommandArgument);
3: switch (e.CommandName.ToUpper()){
4: case "DELETE":
5: CustomerUtility.DeleteCustomer(customerId);
6: BindEventRepeater(false);
7: break;
8: case "Add":
9: 10: break;
11: }
12: }
Depending on what the
commandName
is [3], we know different actions were requested. It's important to note that if you change the underlying data source (like deleting a row) and want that to be visible to the user, you need to rebind your
Repeater
/
DataList
/
DataGrid
. Also note that if you are caching your data, like I am here, you'll need to invalidate the cache so that the new data source (with the delete/added/updated rows) is used.
DOWNLOAD
This sample web application simply contains a number of pages which do various things with
Repeater
s. It should provide a playground for trying different things and simply messing around with data binding:
0 comments:
Post a Comment