
Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]
.pdf
348 C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S
This example defines an object data source that uses a method named GetEmployeeRegions(). This method requires a single parameter—the EmployeeID of the selected employee record.
The EmployeeID value is retrieved from the SelectedDataKey.Values collection. You can look up the EmployeeID field by its index position (which is 0 in this example, because there’s only one field in the DataKeyNames list) or by name. The only trick when performing a name lookup is that you need to replace the quotation marks with the corresponding HTML character entity (").
Figure 10-6 shows this master-details form, which contains the regions assigned to an employee whenever an employee record is selected.
Figure 10-6. A master-details page
The SelectedIndexChanged Event
As the previous example demonstrates, you can set up master-details forms declaratively, without needing to write any code. However, there are many cases when you’ll need to react to the SelectedIndexChanged event. For example, you might want to redirect the user to a new page (possibly with the selected value in the query string). Or, you might want to adjust other controls on the page.
For example, here’s the code you need to add a label describing the child table shown in the previous example:
protected void gridEmployees_SelectedIndexChanged(object sender, EventArgs e)
{
int index = gridEmployees.SelectedIndex;
//You can retrieve the key field from the SelectedDataKey property. int ID = (int)gridEmployees.SelectedDataKey.Values["EmployeeID"];
//You can retrieve other data directly from the Cells collection,
//as long as you know the column offset.

C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S |
349 |
string firstName = gridEmployees.SelectedRow.Cells[2].Text; string lastName = gridEmployees.SelectedRow.Cells[3].Text;
lblRegionCaption.Text = "Regions that " + firstName + " " + lastName + " (employee " + ID.ToString() + ") is responsible for:";
}
Figure 10-7 shows the result.
Figure 10-7. Handling the SelectedIndexChanged event
Using a Data Field As a Select Button
You don’t need to create a new column to support row selection. Instead, you can turn an existing column into a link. This technique is commonly used to allow users to select rows in a table by the unique ID value.
To use this technique, remove the CommandField column and add a ButtonField column instead. Then, set the DataTextField to the name of the field you want to use.
<asp:ButtonField ButtonType="Button" DataTextField="EmployeeID" />
This field will be underlined and turned into a link that, when clicked, will post back the page and trigger the GridView.RowCommand event. You could handle this event, determine which row has been clicked, and programmatically set the SelectedIndex property. However, you can use an easier method. Instead, just configure the link to raise the SelectedIndexChanged event by specifying a CommandName with the text Select, as shown here:
<asp:ButtonField CommandName="Select" ButtonType="Button" DataTextField="EmployeeID" />
Now clicking the data field automatically selects the record.

350 C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S
Sorting the GridView
The GridView sorting features allow the user to reorder the results in the GridView by clicking a column header. It’s convenient—and easy to implement.
To enable sorting, you must set the GridView. AllowSorting property to true. Next, you need to define a SortExpression for each column that can be sorted. In theory, a sort expression can use any syntax that’s understood by the data source control. In practice, a sort expression almost always takes the form used in the ORDER BY clause of a SQL query. That means the sort expression can include a single field or a list of comma-separated fields, optionally with the word ASC or DESC added after the column name to sort in ascending or descending order.
Here’s how you could define the FirstName column so it sorts by alphabetically ordering rows by first name:
<asp:BoundField DataField="FirstName" HeaderText="First Name" SortExpression="FirstName"/>
Note that if you don’t want a column to be sort-enabled, you simply don’t set its SortExpression property.
■Tip If you use autogenerated columns, each bound column has its SortExpression property set to match the DataField property.
Once you’ve associated a sort expression with the column and set the AllowSorting property to true, the GridView will render the headers with clickable links. However, it’s up to the data source control to implement the actual sorting logic. How the sorting is implemented depends on the data source you’re using. Not all data sources support sorting, but both the SqlDataSource and the ObjectDataSource do.
Sorting with the SqlDataSource
In the case of the SqlDataSource, sorting is performed using the built-in sorting capabilities of the DataView class. Essentially, when the user clicks a column link, the DataView.Sort property is set to the sorting expression for that column.
■Note As explained in Chapter 8, every DataTable is linked to a default DataView. The DataView is a window onto the DataTable, and it allows you to apply sorting and filtering without altering the structure of the underlying table. You can use a DataView programmatically, but when you use the SqlDataSource it’s used implicitly, behind the scenes. However, it’s available only when the DataSourceMode property is set to SqlDataSourceMode.DataSet.
With DataView sorting, the data is retrieved unordered from the database, and the results are sorted in memory. This is not the speediest approach (sorting in memory requires more overhead and is slower than having SQL Server do the same work), but it is more scalable when you add caching to the mix. That’s because you can cache a single copy of the data and sort it dynamically in several different ways. (Chapter 11 has much more about this essential technique.) Without DataView sorting, a separate query is needed to retrieve the newly sorted data.
Figure 10-8 shows a sortable GridView with column links. Note that no custom code is required for this scenario.


352 C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S
Now you have to implement the GetEmployees() method and decide how you want to perform the sorting. The easiest approach is to fill a disconnected DataSet so you can rely on the sorting functionality of the DataView. Here’s an example of a GetEmployess() method in a database component that performs the sorting in this way:
public EmployeeDetails[] GetEmployees(string sortExpression)
{
SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetAllEmployees", con); cmd.CommandType = CommandType.StoredProcedure; SqlDataAdapter adapter = new SqlDataAdapter(cmd);
DataSet ds = new DataSet(); try
{
con.Open();
adapter.Fill(ds, "Employees");
}
catch (SqlException err)
{
//Replace the error with something less specific.
//You could also log the error now.
throw new ApplicationException("Data error.");
}
finally
{
con.Close();
}
// Apply sort.
DataView view = ds.Tables[0].DefaultView; view.Sort = sortExpression;
// Create a collection for all the employee records. ArrayList employees = new ArrayList();
foreach (DataRowView row in view)
{
EmployeeDetails emp = new EmployeeDetails( (int)row["EmployeeID"], (string)row["FirstName"], (string)row["LastName"], (string)row["TitleOfCourtesy"]); employees.Add(emp);
}
return (EmployeeDetails[])employees.ToArray(typeof(EmployeeDetails));
}
Another approach is to change the actual query you’re executing in response to the sort expression. This way, your database can perform the sorting. This approach is a little more complicated, and no perfect option exists. Here are the two most common possibilities:
•You could dynamically construct a SQL statement with an ORDER BY clause. However, this risks SQL injection attacks, unless you validate your input carefully.
•You could write conditional logic to examine the sort expression and execute different queries accordingly (either in your select method or in the stored procedure). This code is likely to be fragile and involves a fair bit of string parsing.

C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S |
353 |
Sorting and Selection
If you use sorting and selection at the same time, you’ll discover another issue. To see this problem in action, select a row, and then sort the data by any column. You’ll see that the selection will remain, but it will shift to a new item that has the same index as the previous item. In other words, if you select the second row and perform a sort, the second row will still be selected in the new page, even though this isn’t the record you selected. The only way to solve this problem is to programmatically change the selection every time a header link is clicked.
The simplest option is to react to the GridView.Sorted event to clear the selection, as shown here:
protected void GridView1_Sorted(object sender, GridViewSortEventArgs e)
{
// Clear selected index. GridView1.SelectedIndex = -1;
}
In some cases you’ll want to go even further and make sure a selected row remains selected when sorting changes. The trick here is to store the selected value of the key field in view state each time the selected index changes:
protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
{
// Save the selected value.
if (GridView1.SelectedIndex != -1)
{
ViewState["SelectedValue"] = GridView1.SelectedValue.ToString();
}
}
Now, when the grid is bound to the data source (for example after a sort operation), you can reapply to the last selected index:
protected void GridView1_DataBound(object sender, EventArgs e)
{
if (ViewState["SelectedValue"] != null)
{
string selectedValue = (string)ViewState["SelectedValue"];
// Reselect the last selected row.
foreach (GridViewRow row in GridView1.Rows)
{
string keyValue = GridView1.DataKeys[row.RowIndex].Value.ToString(); if (keyValue == selectedValue)
{
GridView1.SelectedIndex = row.RowIndex; return;
}
}
}
}
Keep in mind that this approach can be confusing if you also have enabled paging (which is described later in the section “Paging the GridView”). That’s because a sorting operation might move the current row to another page, rendering it not visible but keeping it selected. This is a perfectly valid situation from a code standpoint but confusing in practice.

354 C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S
Advanced Sorting
The GridView’s sorting is straightforward—it supports sorting by any sortable column in ascending order. In some applications, the user has more sorting options or can order lengthy result sets with more complex sorting expressions.
Your first avenue for improving sorting with the GridView is to handle the GridView.Sorting event, which occurs just before the sort is applied. At this point, you can change the sorting expression. For example, you could use this to implement an ascending/descending sort pattern. With this pattern, you click a column once to apply an ascending sort and a second time to apply a descending sort. This is similar to the sorting that’s built into Windows Explorer.
Here’s the code you need to implement this approach:
protected void GridView1_Sorting(object sender, GridViewSortEventArgs e)
{
//Check to see the if the current sort (GridView1.SortExpression)
//matches the requested sort (e.SortExpression).
//This code tries to match the beginning of the GridView
//sort expression. The final ASC or DESC part is ignored.
if (GridView1.SortExpression.StartsWith(e.SortExpression))
{
//This sort is being applied to the same field for the second time.
//Reverse it.
if (GridView1.SortDirection == SortDirection.Ascending)
{
//This takes care of automatically adding the "DESC"
//to the end of the sort expression. e.SortDirection = SortDirection.Descending;
}
}
}
You could use similar logic to turn clicks on different columns into a compound sort. For example, you might want to check if the user clicks LastName and then FirstName. In this case, you could apply a LastName+FirstName sort.
protected void GridView1_Sorting(object sender, GridViewSortEventArgs e)
{
if (e.SortExpression == "FirstName" && GridView.SortExpression == "LastName")
{
//Based on the current sort and the requested sort, a compound
//sort makes sense.
e.SortExpression = "LastName, FirstName";
}
}
You could take this sorting approach one step further and cascade searches over any arbitrary collection of columns by storing the user’s past sort selections in view state and using them to build a larger sort expression.
One more technique is available to you. You can sort the GridView programmatically by calling the GridView.Sort() method and supplying a sort expression. This could come in handy if you want to presort a lengthy data report before presenting it to the user. It also makes sense if you want to allow the user to choose from a list of predefined sorting options (listed in another control) rather than use column-header clicks.
Figure 10-9 shows an example. When an item is selected from the list, the sort is applied with this code:

C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S |
355 |
protected void lstSorts_SelectedIndexChanged(object sender, EventArgs e)
{
GridView1.Sort(lstSorts.SelectedValue, SortDirection.Ascending);
}
Figure 10-9. Giving sorting options through another control
Paging the GridView
All the examples of repeated-value binding that you’ve seen so far show all the records of the data source on a single web page. However, this isn’t always ideal in real-world situations. Connecting to a data source that contains hundreds or even thousands of records would produce an extremely large page that would take a prohibitively long amount of time to render and transmit to the client browser.
Most websites that display data in tables or lists support record pagination, which means showing a fixed number of records per page and providing links to navigate to the previous or next pages to display other records. For example, you have no doubt seen this functionality in search engines that can return thousands of matches.
The GridView control has built-in support for pagination. You can use simple pagination with both the SqlDataSource and ObjectDataSource. If you’re using the ObjectDataSource, you also have the ability to customize the way the paging works for a more efficient and scalable solution.
Automatic Paging
By setting a few properties and handling an event, you can make the GridView control manage the paging for you. The GridView will create the links to jump to the previous or next pages and will display the records for the current page without requiring you to manually extract the records by yourself. Before discussing the advantages and disadvantages of this approach, let’s see what you need to get this working.


C H A P T E R 1 0 ■ R I C H D ATA C O N T R O L S |
357 |
Automatic paging works with any data source that implements ICollection. This means that the SqlDataSource supports automatic paging, as long as you use DataSet mode. (The DataReader mode won’t work.) Additionally, the ObjectDataSource also supports paging, assuming your custom data access class returns an object that implements ICollection—arrays, strongly typed collections, and the disconnected DataSet are all valid options.
Automatic paging is a simulation—it doesn’t reduce the amount of data you need to query from the database. One problem with paging is that all of the data must be bound every time the user changes the current page. In other words, if you split a table into ten pages and the user steps through each one, you will end up performing the same work ten times (and multiplying the overall database workload for the page by a factor of ten).
Fortunately, you can make automatic paging much more efficient by implementing caching (see Chapter 11). This allows you to reuse the same data object for multiple requests. Of course, storing the DataSet in the cache may not be the ideal solution if you’re using paging to deal with an extremely large query. In this case, the amount of memory required to keep the full DataSet in the cache is prohibitively large. That’s when custom pagination enters the scene.
Custom Pagination with the ObjectDataSource
Custom pagination requires you to take care of extracting and binding only the current page of records for the GridView. The GridView no longer selects the rows that should be displayed automatically. However, the GridView still provides the pager bar with the autogenerated links that allow the user to navigate through the pages.
Although custom pagination is more complex than automatic pagination, it also allows you to minimize the bandwidth usage and avoid storing a large data object in server-side memory. On the other hand, most custom pagination strategies require the database with each postback, which means you may be creating more work for the database.
■Tip To determine whether custom pagination is better than automatic paging with caching, you need to evaluate the way you use data. The larger the amount of data the GridView is using, the more likely you’ll need to use custom pagination. On the other hand, the slower the database server and the heavier its load, the more likely you’ll want to reduce repeated calls by caching the full data object. Ultimately, you may need to profile your application to determine the optimum paging strategy.
The ObjectDataSource is the only data source to support custom pagination. The first step to take control of custom paging is to set ObjectDataSource.EnablePaging to true. You can then implement paging through three more properties: StartRowIndexParameterName, MaximumRowsParameterName, and SelectCountMethod.
Counting the Records
To have the GridView create the correct number of page links for you, it must know the total number of records and the number of records per page. The records-per-page value is set with the PageSize property, as in the previous example. The total number of pages is a little trickier.
When using automatic pagination, the total number of records is automatically determined by the GridView based on the number of records in the data source. In custom paging, you must explicitly calculate the total number using a dedicated method. The following procedure shows how you can retrieve the number of records of the Employees table and return the count: