Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
298 CHAPTER 9 ■ DATA BINDING
■Tip When calling the DataBind() method, you can use either the current page instance, as in this.DataBind(), or the name of the class, as in Page.DataBind(). Both approaches are equivalent.
Figure 9-1 shows what you’ll see when you run this page.
Figure 9-1. Single-value data binding in various controls
You’ll see data binding expressions again when you create templates for more advanced controls in Chapter 10.
Other Types of Expressions
Data binding expressions are always wrapped in the <%# and %> characters. ASP.NET 2.0 also adds support for different types of expressions, commonly known as $ expressions because they incorporate the $ character. Technically, a $ expression is a code sequence that you can add to an .aspx page and that will be evaluated by an expression builder when the page is rendered. The expression builder processes the expression and replaces it with a string value in the final HTML.
Currently, ASP.NET includes a built-in expression builder that allows you to extract custom application settings and connection string information from the web.config file. For example, if you want to retrieve an application setting named appName from the <appSettings> portion of the web.config file, you can use the following expression:
<asp:Literal Runat="server" Text="<%$ AppSettings:appName %>" />
Several differences exist between $ expressions and data binding expressions:
•Data binding expressions start with the <%# character sequence, and $ expressions use <%$.
•Unlike data binding expressions, you don’t need to call the DataBind() method to evaluate $ expressions. Instead, they’re always evaluated when the page is rendered.
CHAPTER 9 ■ DATA BINDING |
299 |
•Unlike data binding expressions, $ expressions can’t be inserted anywhere in a page. Instead, you need to wrap them in a control tag and use the expression result to set a control property. That means if you just want to show the result of an expression as ordinary text, you need to wrap it in a Literal tag (as shown in the previous example). The Literal control outputs its text to plain, unformatted HTML.
The first part of a $ expression indicates the name of the expression builder. For example, the AppSettings:appName expression works because a dedicated AppSettingsExpression builder is registered to handle all expressions that begin with AppSettings. Similarly, ASP.NET includes a ResourceExpressionBuilder for inserting resources (see Chapter 17) and a ConnectionStringExpressionBuilder that retrieves connection information from the <connectionStrings> section of the web.config file. Here’s an example that uses the ConnectionStringExpressionBuilder:
<asp:Literal Runat="server" Text="<%$ ConnectionStrings:Northwind %>" />
Displaying a connection string isn’t that useful. But this technique becomes much more useful when you combine it with the SqlDataSource control you’ll examine later in this chapter, in which case you can use it to quickly supply a connection string from the web.config file:
<asp:SqlDataSource ConnectionString="<%$ ConnectionStrings:Northwind %>" ... />
Technically, $ expressions don’t involve data binding. But they work in a similar way and have a similar syntax.
Custom Expression Builders
One of the most innovative features of $ expressions is that you can create your own expression builders that plug into this framework. This is a specialized technique that, while neat, isn’t always practical. As you’ll see, custom $ expressions make the most sense if you’re developing a distinct feature that you want to incorporate into more than one web application.
For example, imagine you want a way to create a custom expression builder that allows you to insert random numbers. You want to be able to write a tag such as this to show a random number between 1 and 6:
<asp:Literal Runat="server" Text="<%$ RandomNumber:1,6 %>" />
Unfortunately, creating a custom expression builder isn’t quite as easy as you probably expect. The problem is how the code is compiled. When you compile a page that contains an expression, the expression evaluating the code also needs to be compiled with it. However, you don’t want the expression to be evaluated at that point—instead, you want the expression to be reevaluated each time the page is requested. To make this possible, your expression builder needs to generate a generic segment of code that performs the appropriate task.
The technology that enables this is CodeDOM (Code Document Object Model)—a model for dynamically generating code constructs. Every expression builder includes a method named
GetCodeExpression() that uses CodeDOM to generate the code needed for the expression. In other words, if you want to create a RandomNumberExpressionBuilder, you need to create a GetCodeExpression() method that uses CodeDOM to generate a segment of code for calculating random numbers. Clearly, it’s not that straightforward—and for anything but trivial code, it’s quite lengthy.
All expression builders must derive from the base ExpressionBuilder class (which is found in the System.Web.Compilation namespace). Here’s how you might declare an expression builder for random number generation:
public class RandomNumberExpressionBuilder : ExpressionBuilder { ... }
300 CHAPTER 9 ■ DATA BINDING
To simplify life, it helps to create a static method that performs the task you need (in this case, random number generation):
public static string GetRandomNumber(int lowerLimit, int upperLimit)
{
Random rand = new Random();
int randValue = rand.Next(lowerLimit, upperLimit + 1); return randValue.ToString();
}
The advantage of this approach is that when you use CodeDOM, you simply generate the single line of code needed to call the GetRandomNumber() method (rather than the code needed to generate the random number).
Now, you need to override the GetCodeExpression() method. This is the method that ASP.NET calls when it finds an expression that’s mapped to your expression builder (while compiling the page). At this point, you need to examine the expression, verify no errors are present, and then generate the code for calculating the expression result (using a CodeExpression object from the System.CodeDom namespace). This dynamically generated piece of code will be executed every time the page is requested.
Here’s the first part of the GetCodeExpression() method:
public override CodeExpression GetCodeExpression(BoundPropertyEntry entry, object parsedData, ExpressionBuilderContext context)
{
//entry.Expression is the number string
//without the prefix (for example "1,6"). if (!entry.Expression.Contains(","))
{
throw new ArgumentException(
"Must include two numbers separated by a comma.");
}
else
{
//Get the two numbers.
string[] numbers = entry.Expression.Split(',');
if (numbers.Length != 2)
{
throw new ArgumentException("Only include two numbers.");
}
else
{
int lowerLimit, upperLimit;
if (Int32.TryParse(numbers[0], out lowerLimit) && Int32.TryParse(numbers[1], out upperLimit))
{
...
So far, all the operations have been performed in normal code. That’s because the two numbers are specified in the expression, so they won’t change each time the page is requested. However, the random number should be allowed to change each time, so now you need to switch to CodeDOM to create a dynamic segment of code. The basic strategy is to construct a CodeExpression that calls the static GetRandomNumber() method.
CHAPTER 9 ■ DATA BINDING |
301 |
Here’s the rest of the code:
...
// Specify the class.
Type type = entry.DeclaringType; PropertyDescriptor descriptor =
TypeDescriptor.GetProperties(type)[entry.PropertyInfo.Name];
// Define the parameters.
CodeExpression[] expressionArray = new CodeExpression[2]; expressionArray[0] = new CodePrimitiveExpression(lowerLimit); expressionArray[1] = new CodePrimitiveExpression(upperLimit);
// Define the expression that invokes the method. return new CodeCastExpression(descriptor.PropertyType,
new CodeMethodInvokeExpression(
new CodeTypeReferenceExpression(base.GetType()), "GetRandomNumber", expressionArray));
}
else
{
throw new ArgumentException("Use valid integers.");
}
}
}
}
Now you can copy this expression builder to the App_Code folder (or compile it separately and place the DLL assembly in the Bin folder).
Finally, to use this expression builder in a web application, you need to register it in the web.config file and map it to the prefix you want to use:
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.web>
<compilation debug="true"> <expressionBuilders>
<add expressionPrefix="RandomNumber" type="RandomNumberExpressionBuilder"/>
</expressionBuilders>
</compilation>
...
</system.web>
</configuration>
Now you can use expressions such as <%$ RandomNumber:1,6 %>. These expressions are automatically handled by your custom expression builder, which generates a new random number in the desired range each time the page runs.
The possibilities for expression builders are intriguing. They enable many extensibility scenarios, and third-party tools are sure to take advantage of this feature. However, if you intend to use an expression in a single web application or in a single web page, you’ll find it easier to just use a data binding expression that calls a custom method in your page. For example, you could create
a data binding expression like this:
<%# GetRandomNumber(1,6) %>
302 CHAPTER 9 ■ DATA BINDING
And add a matching public or protected method in your page, like this:
protected string GetRandomNumber(int lowerLimit, int upperLimit) { ... }
Just remember to call Page.DataBind() to evaluate your expression.
Repeated-Value Binding
Repeated-value binding allows you to bind an entire list of information to a control. This list of information is represented by a data object that wraps a collection of items. This could be a collection of custom objects (for example, with an ordinary ArrayList or Hashtable) or a collection of rows (for example, with a DataReader or DataSet).
ASP.NET includes several basic list controls that support repeated-value binding:
•All controls that render themselves using the <select> tag, including the HtmlSelect, ListBox, and DropDownList controls
•The CheckBoxList and RadioButtonList controls, which render each child item with a separate check box or radio button
•The BulletedList control, which creates a list of bulleted or numbered points
All these controls display a single-value field of a property from each data item. When dealing with one of these controls, you’ll find the properties listed in Table 9-1.
Table 9-1. Data Properties for List Controls
Property |
Description |
DataSource |
This is a data object that contains the data to display. This data object |
|
must implement a supported interface, typically ICollection. |
DataSourceID |
Instead of supplying the data object programmatically (using code), |
|
you can link your list control to a data source control by setting this |
|
property. The data source control will generate the required data |
|
object automatically. You can use either the DataSource property or |
|
the DataSourceID property, but not both. |
DataTextField |
Every data source represents a collection of data items. A list control |
|
can display only a single value from each list item. The DataTextField |
|
indicates the field (in the case of a row) or property (in the case of an |
|
object) of the data item that contains the value to display in the page. |
DataTextFormatString |
Specifies an optional format string that the control will use to format |
|
each DataTextValue before displaying it. |
DataValueField |
This property is similar to the DataTextField property, but the value |
|
from the data item isn’t displayed in the page—instead, it’s stored in the |
|
value attribute of the underlying HTML tag. This allows you to retrieve |
|
the value later in your code. The primary use of this field is to store a |
|
unique ID or primary key field so you can use it later to retrieve more |
|
data when the user selects a specific item. |
|
|
All the list controls are essentially the same. The only differences are the way they render themselves in HTML and whether or not they support multiple selection.
Figure 9-2 shows a test page that displays all the list controls, along with some text that displays the current selection for the controls.
CHAPTER 9 ■ DATA BINDING |
303 |
Figure 9-2. Repeated-value data binding in list controls
The controls are declared as follows:
<select runat="server" ID="Select1" size="3" DataTextField="Key" DataValueField="Value" />
<select runat="server" ID="Select2" DataTextField="Key" DataValueField="Value" />
<asp:ListBox runat="server" ID="Listbox1" Size="3" DataTextField="Key" DataValueField="Value" /> <asp:DropDownList runat="server" ID="DropdownList1" DataTextField="Key" DataValueField="Value" /> <asp:RadioButtonList runat="server" ID="OptionList1"
DataTextField="Key" DataValueField="Value"/> <asp:CheckBoxList runat="server" ID="CheckList1" DataTextField="Key" DataValueField="Value" />
<br /><br />
<asp:Literal runat="server" ID="Result" EnableViewState="False"/>
The last control, the Literal control, displays information about the selected items. Its EnableViewState attribute is set to false so that its content will be cleared after every postback.
When the page loads for the first time, the code creates a data source and assigns it to all the list controls. In this example, the data object is a Hashtable collection, which contains a series of strings. The Value of the Hashtable collection item returns the actual item text (which is used for the DataTextField), while the Key of the Hashtable collection item returns the key under which the item is indexed.
Here’s the code for creating and binding the collection:
protected void Page_Load(object sender, System.EventArgs e)
{
if (!Page.IsPostBack)
{
// Create the data source. Hashtable ht = new Hashtable(3); ht.Add("Lasagna", "Key1");
304 CHAPTER 9 ■ DATA BINDING
ht.Add("Spaghetti", "Key2"); ht.Add("Pizza", "Key3");
//Set the DataSource property for the controls. Select1.DataSource = ht;
Select2.DataSource = ht; Listbox1.DataSource = ht; DropdownList1.DataSource = ht; CheckList1.DataSource = ht; OptionList1.DataSource = ht;
//Bind the controls.
Page.DataBind();
}
■Note Every control that supports repeated-value data binding includes a DataBind() method. You could call this method to bind a specific control. However, when you call the Page.DataBind() method, the page object calls DataBind() on every contained control, simplifying your life.
When the user clicks the button, the code adds the name and values of all the selected items to the label. Here’s the code that accomplishes this task:
protected void cmdGetSelection_Click(object sender, System.EventArgs e)
{
Result.Text += "- Item selected in Select1: " + Select1.Items[Select1.SelectedIndex].Text + " - " + Select1.Value + "<br />";
Result.Text += "- Item selected in Select2: " + Select2.Items[Select2.SelectedIndex].Text + " - " + Select2.Value + "<br />";
Result.Text += "- Item selected in Listbox1: " + Listbox1.SelectedItem.Text + " - " + Listbox1.SelectedItem.Value + "<br />";
Result.Text += "- Item selected in DropdownList1: " + DropdownList1.SelectedItem.Text + " - " + DropdownList1.SelectedItem.Value + "<br />";
Result.Text += "- Item selected in OptionList1: " + OptionList1.SelectedItem.Text + " - " + OptionList1.SelectedItem.Value + "<br />";
Result.Text += "- Items selected in CheckList1: "; foreach (ListItem li in CheckList1.Items)
{
if (li.Selected)
Result.Text += li.Text + " - " + li.Value + " ";
}
}
Binding to a DataReader
The previous example used a Hashtable as the data source. Basic collections certainly aren’t the only kind of data source you can use with list data binding. Instead, you can bind any data structure that implements the ICollection interface or one of its derivatives. The following list summarizes many of these data classes:
CHAPTER 9 ■ DATA BINDING |
305 |
•All in-memory collection classes, such as Collection, ArrayList, Hashtable, and Dictionary
•An ADO.NET DataReader object, which provides connection-based, forward-only, and readonly access to the database
•The ADO.NET DataView, which provides a view onto a single disconnected DataTable object
•Any other custom object that implements the ICollection interface
For example, imagine you want to fill a list box with the full name of all the employees contained in the Employees table of the Northwind database. Figure 9-3 shows the result you want to produce.
Figure 9-3. Data binding with a DataReader
The information in this example includes each person’s title of courtesy, first name, and last name, which are stored in three separate fields. Unfortunately, the DataTextField property expects the name of only a single field. You cannot use data binding to concatenate these three pieces of data and create a value for the DataTextField. However, you can solve this issue with an easy but powerful trick—using a calculated column. You simply need to modify the SELECT query so that it creates a calculated column that consists of the information in the three fields. You can then
use this column for the DataTextField. The SQL command that you need to accomplish this is as follows:
SELECT EmployeeID, TitleOfCourtesy + ' ' +
FirstName + ' ' + LastName As FullName FROM Employees
The data-bound list box is declared on the page as follows:
<asp:ListBox runat="server" ID="lstNames" Size="10" SelectionMode="Multiple" DataTextField="FullName" DataValueField="EmployeeID"/>
306 CHAPTER 9 ■ DATA BINDING
When the page loads, it retrieves the records from the database and binds them to the list control. This example uses a DataReader as the data source, as shown here:
protected void Page_Load(object sender, System.EventArgs e)
{
if (!Page.IsPostBack)
{
// Create the Command and the Connection.
string connectionString = WebConfigurationManager.ConnectionStrings[ "Northwind"].ConnectionString;
string sql = "SELECT EmployeeID, TitleOfCourtesy + ' ' + " + "FirstName + ' ' + LastName As FullName FROM Employees";
SqlConnection con = new SqlConnection(connectionString);
SqlCommand cmd = new SqlCommand(sql, con);
try
{
//Open the connection and get the DataReader. con.Open();
SqlDataReader reader = cmd.ExecuteReader();
//Bind the DataReader to the list. lstNames.DataSource = reader; lstNames.DataBind(); reader.Close();
}
finally
{
// Close the connection. con.Close();
}
}
}
The previous code sample creates a connection to the database, creates the command that will select the data, opens the connection, and executes the command that returns the DataReader. The returned DataReader is bound to the list box, and finally the DataReader and the connection are both closed. Note that the DataBind() method of the page or the control must be called before the connection is closed. It’s not until you call this method that the actual data is extracted.
The last piece of this example is the code for determining the selected items. As in the previous example, this code is quite straightforward:
protected void cmdGetSelection_Click(object sender, System.EventArgs e)
{
Result.Text += "<b>Selected employees:</b>"; foreach (ListItem li in lstNames.Items)
{
if (li.Selected)
Result.Text += String.Format("<li>({0}) {1}</li>", li.Value, li.Text);
}
}
If you want to use a DropDownList, a CheckListBox, or a RadioButtonList instead of a ListBox, you need to change only the control declaration. The rest of the code that sets up the data binding remains the same.
CHAPTER 9 ■ DATA BINDING |
307 |
The Rich Data Controls
In addition to the simple list controls, ASP.NET includes some rich data controls that support repeated-value binding. The rich data controls are quite a bit different from the simple list con- trols—for one thing, they are designed exclusively for data binding. They also have the ability to display several properties or fields from each data item, often in a table-based layout or according to a template you’ve defined; they support higher-level features such as editing; and they provide several events that allow you to plug into the control’s inner workings at various points.
The rich data controls include the following:
GridView: The GridView is an all-purpose grid control for showing large tables of information. It supports selecting, editing, sorting, and paging. The GridView is the heavyweight of ASP.NET data controls. It’s also the successor to the ASP.NET 1.x DataGrid.
DetailsView: The DetailsView is ideal for showing a single record at a time, in a table that has one row per field. The DetailsView supports editing and optional paging controls that allow you to browse through a sequence of records.
FormView: Like the DetailsView, the FormView shows a single record at a time, supports editing, and provides paging controls for moving through a series of records. The difference is that the FormView is based on templates, which allow you to combine fields in a much more flexible layout that doesn’t need to be based on a table.
■Note In addition to the controls in this list, some of ASP.NET’s more specialized controls support data binding. These include the Menu and TreeView controls (see Chapter 16) and the AdRotator control (Chapter 4).
You’ll explore the rich data controls in detail in Chapter 10. However, it’s worth taking a look at a quick example now with the GridView, because you’ll use it to work through a variety of examples in this chapter.
Like the list controls, the GridView provides a DataSource property for the data object and a DataBind() that triggers it to read the data object and display each record. However, you don’t need to use properties such as DataTextField and DataValueField, because the GridView automatically generates a column for every property (if you’re binding to a custom object) or every field (if you’re binding to a row). Here’s all you need to get this basic representation:
<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="True" />
Now, define a query that selects several fields from the Employees table:
string sql = "SELECT EmployeeID, FirstName, LastName, Title, City " + "FROM Employees";
You can bind the GridView to a DataReader in the same way you bound the list control in the previous example. Only the name of the control changes:
grid.DataSource = reader; grid.DataBind();
Figure 9-4 shows the GridView this code creates.
