Data Binding and Data Controls
|
This section documents ASP.NET Web Forms on .NET Framework 4.8.1, the last and permanent version of Web Forms — the page life cycle and postback model, ViewState and control state, server controls, validation controls, master pages and themes, data-bound controls, and the provider-based security model — as described by the official documentation at Microsoft Learn and the ASP.NET previous-versions archive, which are the reference these pages are written and verified against. Web Forms receives security fixes only and has no forward path onto modern .NET (.NET Framework 4.8.1 is Microsoft’s last version of .NET Framework; Web Forms itself never shipped on .NET Core/.NET 5+). It remains supported for existing applications running on Windows but is not recommended for new development — see Choosing an ASP.NET Framework and Migrating to Modern ASP.NET for what that means in practice. This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Data binding is Web Forms' declarative link between a data source and a repeating or detail UI, and it is the area where the framework’s "close to configuration, not code" promise is most visible — and where the binding-order pitfall covered at the end of this page is most commonly hit.
Data-binding expressions
<asp:Repeater ID="ProductsRepeater" runat="server">
<ItemTemplate>
<li>
<%# Eval("Name") %> -- <%# Eval("Price", "{0:C}") %>
<%-- Bind() is required (not Eval()) wherever the control writes the value back, e.g. FormView edit mode --%>
<asp:TextBox runat="server" Text='<%# Bind("Quantity") %>' />
</li>
</ItemTemplate>
</asp:Repeater>
<asp:Repeater ID="CatalogRepeater" runat="server">
<ItemTemplate>
<%-- XPath() reads from an XmlDataSource-bound XPathNavigator item instead of a property bag --%>
<span><%# XPath("@name") %></span>
</ItemTemplate>
</asp:Repeater>
Eval is one-way (data → control); Bind is two-way and only valid inside a control that supports updating
(a FormView/DetailsView edit template, or paired with a data source control’s Update command); XPath
is Eval’s XML-specific sibling, valid when the bound item is an `XmlDataSource node. All three only
evaluate when DataBind() actually runs on the containing control — they are not live bindings.
Data source controls
Data source controls are the declarative, no-code-behind way to wire a data-bound control to data:
| Control | Backs onto |
|---|---|
|
A raw ADO.NET connection/command — SQL text or stored procedure, parameterized. |
|
An arbitrary business/data-access class following a method-name convention
( |
|
A LINQ to SQL |
|
An Entity Framework (EF6-era, |
|
An XML document/file, queryable via XPath. |
|
The site’s |
<asp:SqlDataSource ID="ProductsSource" runat="server"
ConnectionString="<%$ ConnectionStrings:CatalogDb %>"
SelectCommand="SELECT Id, Name, Price FROM Products WHERE CategoryId = @CategoryId"
UpdateCommand="UPDATE Products SET Name=@Name, Price=@Price WHERE Id=@Id AND RowVersion=@original_RowVersion"
ConflictDetection="CompareAllValues" OldValuesParameterFormatString="original_{0}">
<SelectParameters>
<asp:QueryStringParameter Name="CategoryId" QueryStringField="cat" Type="Int32" />
</SelectParameters>
</asp:SqlDataSource>
<asp:ObjectDataSource ID="ProductsObjectSource" runat="server"
TypeName="WebFormsApp.Data.ProductRepository"
SelectMethod="GetByCategory" UpdateMethod="Update" InsertMethod="Insert" DeleteMethod="Delete" />
SqlDataSource embedding SQL directly in markup is functional but tightly couples presentation to persistence
and is the pattern most Web Forms teams eventually replace with ObjectDataSource over a repository, or with
model binding against a plain method.
Data-bound controls
| Control | Shape |
|---|---|
|
Tabular, one row per record, built-in paging/sorting/editing/deleting via declarative fields. |
|
A single record, field-per-row, with built-in paging between records and insert/edit/delete. |
|
A single record, fully templated (no default rendering) — the templated counterpart to
|
|
Fully templated, minimal built-in behavior — no default styling, paging, or editing; the most "just render this" of the group. |
|
Templated, like |
|
Templated like |
<asp:GridView ID="ProductsGrid" runat="server" DataSourceID="ProductsSource"
AutoGenerateColumns="false" DataKeyNames="Id"
AllowPaging="true" PageSize="20" AllowSorting="true"
OnRowEditing="ProductsGrid_RowEditing">
<Columns>
<asp:BoundField DataField="Name" HeaderText="Name" SortExpression="Name" />
<asp:TemplateField HeaderText="Price">
<ItemTemplate><%# Eval("Price", "{0:C}") %></ItemTemplate>
<EditItemTemplate><asp:TextBox runat="server" Text='<%# Bind("Price") %>' /></EditItemTemplate>
</asp:TemplateField>
<asp:CommandField ShowEditButton="true" ShowDeleteButton="true" />
</Columns>
</asp:GridView>
TemplateField (used for Price above) is how any column beyond simple text — a formatted value, a nested
control, conditional markup — is expressed; BoundField covers the plain-text case with less markup.
Paging, sorting, editing, inserting, deleting
GridView/DetailsView/ListView implement these against the bound IDataSource’s own capabilities: paging
re-queries (`SelectCommand/SelectMethod) with new offset/size parameters, sorting appends an ORDER BY
(SQL) or reflection-based sort (Object/Linq data sources), and edit/insert/delete map directly onto the data
source’s UpdateCommand/InsertMethod/DeleteCommand. With a SqlDataSource/ObjectDataSource wired up,
none of this requires code-behind at all beyond handling result/exception events for user feedback:
protected void ProductsSource_Updated(object sender, SqlDataSourceStatusEventArgs e)
{
if (e.Exception != null)
{
e.ExceptionHandled = true;
StatusLabel.Text = "Save failed: " + e.Exception.Message;
}
}
Optimistic concurrency (ConflictDetection)
SqlDataSource/ObjectDataSource support optimistic concurrency via ConflictDetection="CompareAllValues"
(compare every original column against current database state before applying the update, as in the
SqlDataSource example above) or "OverwriteChanges" (last write wins, the default). CompareAllValues
requires the data-bound control to keep the original row values around (via OldValuesParameterFormatString
and, for GridView, DataKeyNames plus the row’s originally bound values) to compare against at update time,
surfacing a conflict as a failed UpdateCommand/zero-rows-affected rather than a thrown ADO.NET concurrency
exception.
Nested data-bound controls and the binding-order pitfall
Nesting one data-bound control inside another’s ItemTemplate (a Repeater of categories, each containing a
nested Repeater of products) is common, but the outer control’s items — and therefore the inner controls — are only created once the outer control’s DataBind()/ItemDataBound has run. The classic mistake is
calling .DataBind() on the inner control too early (e.g. in Page_Load, before the outer control has even
created it) or forgetting to bind it at all inside ItemDataBound:
protected void CategoriesRepeater_ItemDataBound(object sender, RepeaterItemEventArgs e)
{
if (e.Item.ItemType != ListItemType.Item && e.Item.ItemType != ListItemType.AlternatingItem)
{
return;
}
var category = (Category)e.Item.DataItem;
var innerRepeater = (Repeater)e.Item.FindControl("ProductsRepeater");
innerRepeater.DataSource = category.Products;
innerRepeater.DataBind(); // must happen here -- the inner control does not exist before this fires
}
The general rule: bind the innermost control from within the outer control’s ItemDataBound (or the
equivalent RowDataBound for GridView), never from Page_Load, because the inner control instance is
created fresh on every outer-row bind and does not persist identity the way a page-level control does.