ASP.NET Iterative Controls

Overview

Be sincere; be brief; be seated.

-Franklin D. Roosevelt

Iterative controls are a special type of data-bound controls that combine two key capabilities. They support ADO.NET-compliant data sources and supply a template-based mechanism to create free-form user interfaces.

In Part II of the book, while covering database programming and techniques, we covered the DataGrid control in great detail. Technically speaking, the DataGrid control is certainly an iterative control in the sense that it iterates over a bound data source to generate its user interface, one row after the next. Although the DataGrid is highly customizable, it can't go beyond the boundaries of a column-based representation of the data. For example, the DataGrid supports templates, but only within the cells of a column. Other tasks are only for the bravest—for example, redesigning the layout of an individual row, or altering the default template of the control's output to employ multiple rows per record or multiple columns per data source. In some cases, by resorting to dirty and smart tricks you can customize the DataGrid, such as displaying subtotals as we did in Chapter 8, "Real-World Data Access." Generally, though, the DataGrid is not the ideal tool to use when you need a more free-form display of data. For this purpose, the Repeater and DataList controls, each with its own set of pros and cons, seem to be more appropriate.

What s an Iterative Control, Anyway?

Before looking under the hood of the Repeater and DataList controls, let's discuss in general terms the expected features of iterative and templated controls. A control is said to be iterative if it provides the ability to loop over a set of data items in order to apply some layout elements to each of them. In addition, an iterative control works by applying an ASP.NET template to each data item. It exposes the individual data item as a customizable and configurable stand-alone object that holds its own set of properties, methods, and events.

According to this definition, what is the role of all controls that show a list of data rows? Are the DropDownList and CheckBoxList controls, just to name a few, iterative controls? Is the DataGrid iterative as well?

Iterative Controls vs List Controls

The big difference between, say, a DropDownList control and what we recognize here as iterative controls—that is, the Repeater and DataList—is all in how each control shows its contents. The DropDownList control uses a drop-down list and doesn't provide relevant facilities for you to change the user interface of each displayed item.

Similarly, the CheckBoxList control displays a list of selectable options whose value and displayed text can be bound to a data source. With this list control, you can apply some relatively simple HTML formatting to each item. However, you can't do that with a stand-alone object that represents the list item. More specifically, you have no object that is equivalent to the DataGridItem class of the DataGrid control. If you want to render, say, some items of the check-box list in boldface type, you can only modify the text of the single item being displayed and wrap it in a element. Although this is (much) better than nothing, it's still too little for complex applications that need more advanced display features.

In summary, iterative controls differ from list controls because of their greater rendering flexibility. An iterative control lets you apply an ASP.NET template to each row in the bound data source. A list control, on the other hand, provides a fixed and built-in template for each data item. List controls are customizable to some extent, but you can't change anything other than the text displayed. No changes to layout are supported.

On the other hand, using a list control is considerably easier than setting up an iterative control, as we'll see in a moment. Defining templates requires quite a bit of declarative code, and if accomplished programmatically, it requires that you write a class that implements the ITemplate interface. A list control needs only data binding.

Iterative Controls vs the DataGrid Control

The DataGrid is an iterative control because it displays the contents of a data source by using customizable templates in which the programmer can easily access and configure each constituent element—from the data rows to the header and from the footer to the pager. However, the DataGrid can also be considered a list control because the customization facilities it provides don't allow you to break the rigid scheme of a tabular, multicolumn representation of data.

The DataGrid has more advanced features than the Repeater and DataList—particularly, paging. On the other hand, it doesn't offer the same level of flexibility as iterative controls. In a certain sense, it's a sort of highly specialized, off-the-shelf version of an iterative control.

Using a Repeater control, and even more easily using a DataList, you can build a grid of data. Note also that you can't always use a DataGrid control to reproduce the same output you can with either a Repeater or DataList. Table 9-1 summarizes the differences among the DataGrid, iterative controls, and list controls.

Table 9-1: Outlining Iterative and List Controls
 

Rendering

Usage

DataGrid Control

Loop over data, and create a tabular representation of data allowing for column-specific templates.

Use when you have tabular data to display. If the data is not naturally tabular but can be rendered as such, consider iterative controls too.

Iterative Controls

Loop over data-bound items, and apply a user-defined template.

Use when you have data to render as a monolithic structure in a free-form way.

List Controls

Loop over data-bound items, and apply an immutable, control-specific template.

Use when you don't need item-specific customization and you like the standard user interface.

The Repeater Control

The Repeater is a data-bound control that displays data using a custom layout. It works by repeating a specified ASP.NET template for each item displayed in the list. The Repeater is a rather basic templated data-bound control. It has no built-in layout or styling capabilities. All formatting and layout information must be explicitly declared and coded using HTML tags and ASP.NET classes.

Programming Interface of the Repeater Control

The Repeater class inherits from Control and acts as a naming container by implementing the marker interface INamingContainer. Table 9-2 lists the properties exposed by the control, not counting those inherited from the base class Control.

Table 9-2: Properties of the Repeater Control

Property

Description

AlternatingItemTemplate

Gets or sets the template that defines how every other item is to be rendered.

DataMember

Gets or sets the specific table in the DataSource that is to be bound to the control.

DataSource

Gets or sets the data source that provides any data for populating the list.

FooterTemplate

Gets or sets the template that defines how the footer is rendered.

HeaderTemplate

Gets or sets the template that defines how the header is rendered.

Items

Gets a RepeaterItemCollection object—that is, a collection of RepeaterItem objects. Each element of the collection represents a displayed data row in the Repeater.

ItemTemplate

Gets or sets the template that defines how items are rendered.

SeparatorTemplate

Gets or sets the template that defines how separators between items are rendered.

For the most part, properties are the template elements that form the control's user interface. Aside from that, the Items property contains the list of child items that are associated with each displayed data row. The RepeaterItem object is the Repeater's counterpart of the DataGridItem object we discussed in earlier chapters. The Repeater populates the Items collection by enumerating all the data objects in its bound data source. In no way does the control filter the contents of the Items collection. All the rows contained in the data source are mapped onto a RepeaterItem object and inserted in the collection. The RepeaterItemCollection class is a plain collection class with no special or peculiar behavior.

Just as in the DataGridItem class, the RepeaterItem class is used to represent any type of constituent item and is not limited to data items. You distinguish between types using the familiar ListItemType enumeration. A few properties characterize the programming interface of the RepeaterItem class. The RepeaterItem class is self-explanatory once you know about the DataGridItem class and have a look at its properties—ItemIndex, ItemType, and DataItem.

Binding a Repeater to Data

Just like any other data-bound ASP.NET control, the Repeater generates its user interface only when a call to its DataBind method is made. DataBind is the only method in the control's interface aside from those directly inherited from parent classes. A call to DataBind causes the control to rebuild its control hierarchy and related view state. Before the whole process starts, the DataBinding event fires.

The call to DataBind is necessary to have the HTML code for the control be generated and added to the output stream. In addition to the events defined on the Control class, the Repeater exposes three additional events—ItemCreated, ItemCommand, and ItemDataBound. (See Table 9-3.) Their overall behavior is nearly identical to that of the analogous events in the DataGrid.

Table 9-3: Events of the Repeater Control

Event

Description

ItemCommand

Fires when a button is clicked within the Repeater control.

ItemCreated

Fires when a repeater item is being created.

ItemDataBound

Fires after an item in the Repeater has been bound to underlying data. The event occurs before the data item is added to the Items collection. (Note that only items and alternating items are bindable.)

The DataBinding event fires before all the events listed in Table 9-3. The sequence of data-binding events is triggered by the call to DataBind. As the very first step, the Repeater fires the DataBinding event and then begins building the control's hierarchy. Events in Table 9-3 occur only at this time, in the order we're going to discuss in the next section.

How the Repeater Builds the Output

An internal, protected method of the Control class named CreateControlHierarchy is responsible for the generation of the HTML code sent to the browser. This method connects to the data source and then runs a loop over the items in the bound collection. (See the "Resolving the Data Source" sidebar later in the chapter for more information on how the Repeater resolves the reference to the run-time data-bound object.)

The following pseudocode illustrates the process that leads to the creation of the Repeater output.

if (headerTemplate != null) CreateItem(ListItemType.Header); int _counter = 0; RepeaterItem _repItem; foreach(object _dataItem in DataSource) { if (useSeparator && _counter > 0) CreateItem(ListItemType.Separator); if (_counter % 2) _repItem = CreateItem(_counter, ListItemType.Item, _dataItem); else _repItem = CreateItem(_counter, ListItemType.AlternatingItem, _dataItem); Items.Add(_repItem); _counter ++; } if (footerTemplate != null) CreateItem(ListItemType.Footer);

The Repeater first applies the header's template, if any. Next, it walks its way through the collection of bound objects and applies the ItemTemplate or the AlternatingItemTemplate as appropriate. Each item template might be interspersed with a separator template, if any has been provided. Finally, at the end of the loop, the footer's template is applied only once.

Within the CreateItem placeholder, a new RepeaterItem object is created and initialized to default values. Next, the ItemCreated event fires and then the object is added to the Items collection. As explained for DataGrid controls in Chapter 6, "Creating Bindable Grids of Data," the ItemCreated event occurs twice, the first time of which is before the whole data binding process begins. This just means that the CreateItem placeholder is invoked twice. The second time, the DataBind method is invoked on the newly created RepeaterItem object and then ItemDataBound fires. The following listing shows the pseudocode for the CreateItem placeholder.

RepeaterItem CreateItem(int index, ListItemType itemType, bool dataBind, object dataItem) { RepeaterItem _repItem; RepeaterItemEventArgs e; _repItem = new RepeaterItem(itemIndex, itemType); e = new RepeaterItemEventArgs(_repItem); ApplyItemTemplate(_repItem); if (dataBind) _repItem.DataItem = dataItem; OnItemCreated(e); Controls.Add(_repItem); if (dataBind) { _repItem.DataBind(); OnItemDataBound(e); } return _repItem; }

The ApplyItemTemplate placeholder renders a piece of code that instantiates all the controls and the literals contained in a bound template. This point leads us straight to the question: what's a template, anyway?

Templates of the Repeater Control

We've already met ASP.NET templates in Chapter 6 while examining the templated columns of DataGrid controls. Speaking abstractly, a template is a block of server controls, literals, and HTML text that is used as a monolithic output element. To some extent, you can think of a template as a dynamically defined ASP.NET control with a composite interface that can be defined either declaratively using client and server tags or programmatically via ad hoc classes.

At a minimum, the Repeater must provide the ItemTemplate template. Unspecified templates are simply ignored; however, if you also omit the ItemTemplate template, the Repeater will have blank output.

In most cases, you define a template using declarative code. For example, the following code shows how to generate output that looks like that of a DataGrid:

 
ID   First Name   Last Name  
     
  <%# ((DataRowView)Container.DataItem)["EmployeeID"] %>   <%# ((DataRowView)Container.DataItem)["FirstName"] %>   <%# ((DataRowView)Container.DataItem)["LastName"] %>  
 
  <%# ((DataRowView)Container.DataItem)["EmployeeID"] %>   <%# ((DataRowView)Container.DataItem)["FirstName"] %>   <%# ((DataRowView)Container.DataItem)["LastName"] %>  
 
 
<%# "" + ((DataView)Repeater1.DataSource).Count + " employees found."%>  

Figure 9-1 shows what this Repeater control looks like.

Figure 9-1: The Repeater control can do whatever a DataGrid can do, even though it might require a lot of coding. The opposite is not true.

In this case, the data source of the Repeater control is a DataView object.

DataSet _data = new DataSet(); string _conn = "SERVER=localhost;DATABASE=Northwind;UID=sa;"; string _query = "SELECT employeeid, firstname, lastname FROM employees"; SqlDataAdapter _adapter = new SqlDataAdapter( _query, _conn); _adapter.Fill(_data); Repeater1.DataSource = _data.Tables[0].DefaultView; Repeater1.DataBind();

As you can see, a lot of casting is needed to keep the compiler quiet and avoid errors. Many of those explicit casts can be avoided if you resort to the DataBinder object and its late-binding Eval method.

The final output of the Repeater is simply the summation of the HTML generated by each involved template. As illustrated in the aforementioned example, the Repeater control allows you to split HTML tags across the various templates.

The ITemplate Interface

The Repeater class references a template through the ITemplate interface, meaning that a template is always treated as an instance of any class that implements the ITemplate interface. In spite of this, you are allowed to specify a template by using server-side tags and HTML tags. What's up?

When the page is compiled, all the fragments defined within a template block are compiled into an internal object that implements the ITemplate interface. Set the debug attribute of the @Page directive to true, and look into the actual code being generated for the page shown in Figure 9-1. (See Chapter 2, "Web Forms Internals," for instructions on how to do this and retrieve the correct source file.) The Repeater is built using the following code. Note that the code shown here is the same code generated by the ASP.NET compiler—it's just made more readable.

// Instantiate the Repeater object Repeater __ctrl = new Repeater(); Repeater1 = __ctrl; __ctrl.ID = "Repeater1"; // Build the template for the header BuildTemplateMethod _builder; _builder = new BuildTemplateMethod(__BuildControl__control3); CompiledTemplateBuilder _template = new CompiledTemplateBuilder(_builder); __ctrl.HeaderTemplate = _template; // Build other templates

The CompiledTemplateBuilder class is listed in the ASP.NET documentation, but it's not documented. The same holds true for the BuildTemplateMethod delegate.

public delegate void BuildTemplateMethod(Control control);

The constructor of the CompiledTemplateBuilder class takes an instance of the delegate and calls it back to build a template class based on the source of the specified control. What's the _control3 in the source code? It's simply the literal control that contains all the source code of the tag.

void __BuildControl__control3(Control __ctrl) { IParserAccessor __parser = (IParserAccessor) __ctrl; LiteralControl _header = new LiteralControl("text of "); __parser.AddParsedSubObject(_header); }

When a template is instantiated, all the child controls are instantiated and then rendered to HTML. This is what happens in the preceding procedure using the methods on the IParserAccessor interface. If you want to create templates programmatically, there are simpler ways, and we'll look at a couple of them in a moment. In the meantime, let's review the definition of the ITemplate interface.

The ITemplate interface consists of a single method, named InstantiateIn.

void InstantiateIn(Control container);

The expected behavior of InstantiateIn is intuitive: it programmatically creates all the child controls to be used in the template. All the controls are added to the container's Controls collection. If needed, the newly created controls can be bound to the data source by adding a handler for the DataBinding event.

Creating Templates Programmatically

There are basically two ways to set template properties programmatically. The simplest way is by using the LoadTemplate method of the Page class. LoadTemplate takes only one argument, that being the name of the .ascx file that describes the template. The file name must have an .ascx extension. .ascx is the typical extension of Web user control files. (We'll cover Web user controls in the Chapter 10, "Web Forms User Controls.") You create a file-based template using the following code:

Repeater1.ItemTemplate = Page.LoadTemplate("item_template.ascx");

The template file can be written in any .NET language and doesn't necessarily have to be written in the language of the page. The LoadTemplate method can be used to load the layout code for any template property, including HeaderTemplate and FooterTemplate. The following code shows how to write an .ascx control to emulate the behavior of the page shown in Figure 9-1:

<%@ Control Language="C#" %> <%# DataBinder.Eval(((RepeaterItem)Container).DataItem, "EmployeeID")%> <%# DataBinder.Eval(((RepeaterItem)Container).DataItem, "FirstName") %> <%# DataBinder.Eval(((RepeaterItem)Container).DataItem, "LastName") %>

The use of DataBinder in this case contributes significantly to clarifying the code. Without it, you would have to cast Container to RepeaterItem (unnecessary within the page) and then DataItem to DataRowView.

The second way to set template properties programmatically is by using a class that implements the ITemplate interface. Using a class that implements ITemplate achieves the same effect as using LoadTemplate and an .ascx file. Here is an example the class:

public class MyItemTemplate : ITemplate { public void InstantiateIn(Control container) { // Create the element TableRow _row = new TableRow(); // Add the employeeID TableCell _empID = new TableCell(); _empID.DataBinding += new EventHandler(BindEmpID); _row.Cells.Add(_empID); // Add the lastname TableCell _lname = new TableCell(); _lname.DataBinding += new EventHandler(BindLName); _row.Cells.Add(_lname); // Add the firstname TableCell _fname = new TableCell(); _fname.DataBinding += new EventHandler(BindFName); _row.Cells.Add(_fname); // Add the row to the container container.Controls.Add(_row); } public void BindEmpID(object sender, EventArgs e) { TableCell cell = (TableCell) sender; RepeaterItem container = (RepeaterItem) cell.NamingContainer; DataRowView _row = (DataRowView) container.DataItem; cell.Text = _row["EmployeeID"].ToString(); } public void BindLName(object sender, EventArgs e) { TableCell cell = (TableCell) sender; RepeaterItem container = (RepeaterItem) cell.NamingContainer; DataRowView _row = (DataRowView) container.DataItem; cell.Text = _row["LastName"].ToString(); } public void BindFName(object sender, EventArgs e) { TableCell cell = (TableCell) sender; RepeaterItem container = (RepeaterItem) cell.NamingContainer; DataRowView _row = (DataRowView) container.DataItem; cell.Text = _row["FirstName"].ToString(); } }

Resolving the Data Source

You should note that the DataGrid control, as well as the Repeater and the DataList, manage the data source through the generic interface of the object class. However, underneath this common and somewhat too simple interface, quite different objects can be used. How can iterative controls distinguish between DataTable, DataSet, DataView, and even custom collections?

Iterative controls use an internal class named DataSourceHelper to resolve any issues related to the actual data source to bind to. In a certain way, the helper class straightens differences out and normalizes all possible sources to the root collection interface IEnumerable. (ICollection descends from IEnumerable, and IList, in turn, inherits from ICollection.)

DataSet and DataTable, for example, don't implement IList directly. They implement IListSource, from which the helper class extracts an IList-compliant array. A similar pattern can be observed with the DataView, DataViewManager, and PagedDataSource classes. All these classes act as proxies between the control and an embedded object—for example, a DataTable. Although they implement IList, an intermediate interface is needed to bind and publish the schema of the underlying data source. The ITypedList interface provides the tools to extract a valid enumerable list from these objects too.

In summary, iterative controls access and manipulate their data items through the IEnumerable interface. However, an extra step is necessary to normalize all possible data sources to IEnumerable. This is a subtle problem that arises if you're going to write a list-bound control yourself. We'll tackle this problem in Chapter 20, "Data-Bound and Templated Controls."

Note that the type of the data item depends on the data source that has been bound to the Repeater. The sender argument refers to the control that is being bound to the data—the TableCell. The NamingContainer property is only a shortcut to obtain a reference to the parent Repeater. In this case, the Repeater is the naming container of the cell; however, this technique should be used with a grain of salt. If the bound control or its parent is a naming container itself, the technique will fail.

Working with the Repeater Control

The output that a Repeater control can generate for you is any HTML code that can be obtained by repeating ASP.NET templates for all rows in a data source. The overall output can be bound within a table as well as flow as plain HTML text. The Repeater control doesn't provide any facility for formatting or styling the output, so whatever effect you need must be manually coded. Note also that you don't necessarily have to bind an ADO.NET object such as DataTable or DataView. Arrays of custom classes, hash tables, and, in general, any objects that implement the IEnumerable interface (or any of its derivates) are acceptable. Of course, you are responsible for casting the Container.DataItem expression to the right type.

Let's build a page that lists some information about the customers stored in the Microsoft SQL Server Northwind database. We did this several times in past chapters, but we did it by using a DataGrid control or a list control. In those cases, though, we were limited by the fixed template of the controls and could show only text. The greater flexibility of the Repeater control allows us to create a sort of Web form for each customer. In particular, our goal is to list minimal information about customers along with a check box. When the page is refreshed, all checked items will display with different colors and more information.

Designing the Templates

For a realistic Repeater control, the item template is a fundamental tool for showing the actual data. The Repeater also needs to have the header template, which is normally used to introduce the user to the information displayed below. The separator template is effective to graphically separate one item from the next. It's especially needed if you use the alternating item template. In this example, we provide a simple implementation of the separator, making it include only a thin horizontal ruler—an


element. However, the separator template might be the ideal tool to use when you need to display summary rows or subtotals.

Building the Page

The following listing illustrates the underpinnings of the page shown in Figure 9-2. It contains a few templates for the header, data items, and separator.

Figure 9-2: The Repeater in action, showing information about customers.

 

Customers

Check to see more information <%# m_checkBox = (CheckBox) FindCheckBox(Container) %> <%# ShowHeader(Container.DataItem) %> <%# ShowInfoBar(Container.DataItem) %> <%# ShowAddressBar(Container.DataItem) %>


The header template is applied only once, whereas the separator is applied before every displayed item except the first. We modified the default style of the


element to make it one-pixel thin. The core of the page is in the section. Let's examine its constituent pieces.

You see that the item's template begins with a rather odd hidden tag. Next, it includes a check box and three additional functions, each run within a data-bound code block, <%# ... %>. As clearly visible in Figure 9-2, address information is displayed for checked customers and all information appears in blue, even though this last effect is not perceivable from a grayscale picture.

The ShowHeader, ShowInfoBar, and ShowAddressBar output the various blocks of information that make up the customer's form. ShowHeader outputs the boldface line with the customer code and name. ShowInfoBar generates the italicized line with contact details. Finally, ShowAddressBar shows the address of the customer and other personal information such as the phone number. All these functions take the data item and return the HTML-formatted string to add to the page's output stream.

private string ShowHeader(object dataItem) { // Determine the color to use string color = (m_checkBox.Checked ?"blue" :"black"); // Set up the pattern of the final HTML string string _output = "{1} - {2}"; // Fill in the placeholders DataRowView _row = (DataRowView) dataItem; _output = String.Format(_output, color, _row["customerid"], _row["companyname"]); // Return the output string return _output; }

The other helper routines have a similar structure. (See this book's sample code.) The key thing going on here is the how the function determines the color to use. Aside from that, it simply casts the Container.DataItem expression to the actual type—DataRowView in this case. Let's see the initialization code of the page.

CheckBox m_checkBox; public void Page_Load(object sender, EventArgs e) { if (!IsPostBack) BindData(); } private void BindData() { DataSet _data = new DataSet(); string _conn = "SERVER=localhost;DATABASE=Northwind;UID=sa;"; string _query = "SELECT TOP 5 * FROM customers"; SqlDataAdapter _adapter = new SqlDataAdapter(_query, _conn); _adapter.Fill(_data); // Bind to a DataView // The type of the data item will be DataRowView Repeater1.DataSource = _data.Tables[0].DefaultView; Repeater1.DataBind(); }

The m_checkBox global variable is reinitialized whenever the item template is processed. As a global variable, it is visible and accessible from within any methods on the page. The check-box variable is initialized when the data item is created.

The Initialization Step

While the repeater's output is being generated, the check-box control is instantiated, but it has yet to be updated with the user's information. If you need to do something when an item is created, you could write either an ItemCreated or ItemDataBound handler. You can also resort to the following trick, in which we run initialization code at the beginning of the template.

<%# m_checkBox = (CheckBox) FindCheckBox(Container) %>

The solutions are not completely equivalent because the various events occur at different times. During the initialization step, the first event that hits the page is ItemCreated. At this point, all controls in the templates have been created and initialized. However, they have yet to be bound to the data.

A detail that often goes unnoticed is that when ItemCreated fires, the ID of each child control has not yet been associated with the correct naming container—the RepeaterItem. If you watch the UniqueID property of, say, the check-box control during the ItemCreated event, you'll see that it equals ExpandButton. Later, when the DataBind method has been called on the RepeaterItem object, each child control is mapped to the actual container. The UniqueID property becomes Repeater1:_ctlX:ExpandButton, where _ctlX indicates the particular item. This is the situation observed from within the FindCheckBox helper function we used in the preceding code. Depending on what you really need to do during the initialization of data items, using ItemCreated or the tricks discussed might lead to significantly different results. On the other hand, the ItemDataBound event occurs as the final step in the initialization process and hits the application at the end of the template data binding.

Retrieving Child Controls

We need to know whether each item's CheckBox is checked, because this affects the way in which the item itself is rendered. At the time we need that information, the most reliable way to get it is by reading the value directly from Request.Form. To do this, we should know the unique ID of the check box.

The ID of the check box is not yet known when ItemCreated fires, and ItemDataBound occurs when the HTML output of the template has already been generated. (Look back at the "How the Repeater Builds the Output" section earlier, and you'll see that OnItemBound fires immediately after the call to DataBind for each repeater's item.) That's why a piece of code placed at the beginning of the item template is an effective trick. There might be other ways of accomplishing the same task, but I find this approach both easy to understand and functional.

The global variable m_checkBox is not strictly needed. However, if it's used, it optimizes your code because there's no need to call FindControl each time you need to access the item-specific instance of the ExpandButton check box.

Handling Postbacks

To retrieve the item-specific instance of a child control, you use the FindControl method from within the actual container—the RepeaterItem object.

CheckBox ctl = (CheckBox) container.FindControl("ExpandButton");

Note that calling the FindControl method exposed by the Page wouldn't work because FindControl explores only the subtree rooted in the naming container. You pass the identifier for the control to be found. Note that the iden tifier doesn't have to be the client-side and unique ID that is used within the page. The contents of the ASP.NET ID property is just fine. The following listing illustrates how we retrieve the item-specific instance of the CheckBox control and update its properties reading from the request's Form collection.

private CheckBox FindCheckBox(Control container) { // Retrieve the item-specific instance of the checkbox CheckBox ctl = (CheckBox) container.FindControl("ExpandButton"); if (ctl == null) return null; // Update the state of the control ctl.Checked = (Page.Request.Form[ctl.UniqueID]=="on"); // Return the instance of the control return ctl; }

Although at this time the state of the child controls has not been restored, the correct value is held in the Request.Form collection. The FindCheckBox method anticipates the restoration of the current logical state of the check-box field. In addition, it contributes to making the instance globally available within the template. Accessing the check box directly through the m_checkBox variable is faster than running FindControl each time.

A Scrollable List of Data Items

In Chapter 6 and Chapter 7 ("Paging Through Data Sources"), we discussed how to page through the records bound to a DataGrid control. Both the Repeater and DataList have no built-in support for pagination. However, the template-based nature of such controls lends itself well to implementing a scrolling mechanism. Bear in mind that any scrolling capabilities rely on the capabilities of the underlying browser and in particular are tied to the level of support the browser provides for version 2 of cascading style sheets (CSS2).

The page shown in Figure 9-3 employs two Repeater controls. The first one displays a push button for each customer initial; the other one populates the page with information about all the customers whose company name starts with the selected letter. The second Repeater is nearly identical to the one we discussed in the previous section. The only significant change has to do with the layout of the HTML output, which now supports text overflow.

Figure 9-3: A page that features two Repeater controls; one to pick the initial and one to display the related customers in a scrollable area.

Handling the Click Event

As mentioned earlier, the Repeater control doesn't have any built-in mechanisms to render its contents according to special formatting rules. The resulting text, therefore, flows as you write it—either horizontally or vertically. In the previous example, we built a vertical list of data items. Let's see how to create a horizontal strip of data items. We display a push button for each distinct initial found in the companyname column of the Customers table.

SELECT DISTINCT substring(companyname, 1, 1) AS Initial FROM customers

To display items horizontally, the Repeater must build an HTML table with one row and as many cells as there are items in the query's result set.

     

Each data item is rendered as a push button with a few parametric properties—BackColor, Text, and CommandName. Text and CommandName, in particular, are bound to the same value—the contents of the Initial column in the data set. The CommandName property represents the keyword associated with a command object within an iterative control.

Whenever the user clicks any of the letter buttons, the ItemCommand event fires. The sample page handles the ItemCommand event as follows:

private void ItemCommand(object sender, RepeaterCommandEventArgs e) { BindData(e.CommandName); } private void BindData(string initial) { // Store the current initial ViewState["Initial"] = initial; // Get the necessary data with a single SQL batch string _cmdText = m_cmdInitial + ";"; _cmdText += String.Format(m_cmdCustmrs, initial); SqlDataAdapter _adapter = new SqlDataAdapter(_cmdText, m_connString); DataSet _data = new DataSet(); _adapter.Fill(_data); // Rename tables _data.Tables[0].TableName = "Initials"; _data.Tables[1].TableName = "Customers"; // Bind data Picker.DataSource = _data.Tables["Initials"].DefaultView; Picker.DataBind(); Customers.DataSource = _data.Tables["Customers"].DefaultView; Customers.DataBind(); }

The current initial letter is cached in the ViewState and persists across successive requests of the same page. Note that the result set with all the initials could have effectively been cached in Cache or Application because of its relatively low level of volatility.

Making the Control Scroll

The trick to making a portion of an HTML page scroll vertically and horizontally is the overflow CSS style. Most popular browsers recognize about a hundred style-sheet properties, most of which are CSS1 recommendations. The overflow attribute is specific to CSS2 and supported by Microsoft Internet Explorer 5.0 and later. The overflow attribute controls how a container element (for example, a

) displays its content if it's not the same size as the container.

 

<%# SetHeader() %>

Possible values for the overflow attribute are listed in Table 9-4.

Table 9-4: Possible Values for the overflow CSS Attribute

Value

Description

auto

Uses scrollbars only if needed.

hidden

Clips any text that exceeds the fixed container's size.

visible

Default value, ignores any difference between the fixed size of the container and the contained text.

scroll

Uses both horizontal and vertical scrollbars. A scrollbar that isn't really needed is disabled.

The header template defines two

tags, the latter of which is given a fixed height and will contain the output generated by the items. Nothing is to be changed in the section. A section, conversely, is needed to close the pending tag and provide a footer for summary information.

<%# SetFooter() %>

The following code is used to show the count of customers displayed in the table:

private string SetFooter() { string _text = "{0} customer(s) found."; ICollection _coll = (ICollection) Customers.DataSource; return String.Format(_text, _coll.Count); }

Repeating with Custom Classes

As mentioned earlier, you don't necessarily bind an ADO.NET object to an iterative control. If you use your own classes to store data, those classes can also be used to populate a Repeater as well as a DataList control. The data source must be an array of class instances. To access individual properties, you then use DataBinder.Eval as you would with a DataTable.

private void BindData() { string _conn = "SERVER=localhost;DATABASE=Northwind;UID=sa;"; string _query = "SELECT firstname, lastname FROM employees"; SqlDataAdapter _adapter = new SqlDataAdapter(_query, _conn); DataTable _data = new DataTable(); _adapter.Fill(_data); ArrayList values = new ArrayList(); foreach(DataRow _row in _data.Rows) values.Add(new Employee( _row["LastName"].ToString(), _row["FirstName"].ToString())); Repeater1.DataSource = values; Repeater1.DataBind(); } public class Employee { private string _lastname; private string _firstname; public Employee(string lname, string fname) { _lastname = lname; _firstname = fname; } public string LastName { get {return _lastname;} } public string FirstName { get {return _firstname;} } }

As you can guess, with the DataBinder.Eval method you can access only public properties on your class.

<%# DataBinder.Eval(Container.DataItem, "LastName") %> <%# DataBinder.Eval(Container.DataItem, "FirstName") %>

If you bind an iterative control to an array of classes, you can't rely on any ready-made tool to sort data. You must sort the array yourself using the IComparer interface to define your own rules. (See the MSDN documentation for the IComparer interface.)

The DataList Control

Functionally speaking, the DataList is a data-bound control that falls somewhere in between the Repeater and the DataGrid. It begins where the Repeater ends and terminates a little before the starting point of the DataGrid control. In some unrealistically simple cases, you can even take some code that uses a Repeater, replace the control, and not even notice any difference. Looking at the programming interface, though, the DataList is much more similar to the DataGrid.

In spite of such deep similarities, the DataList is a control with its own personality and a set of features that make it unique and ideal in a number of situations. Compared to the Repeater, the DataList control comes with a full bag of goodies and new features, mostly in the area of graphical layout. The control overtakes the Repeater in several respects. For example, it supports directional rendering, meaning that items can flow horizontally or vertically to match a specified number of columns. Furthermore, just as with the DataGrid, it provides facilities to retrieve a key value associated with the current data row and has built-in support for selection and in-place editing. In addition, the DataList control supports more templates and can fire some extra events beyond those of the Repeater. Data binding and the overall behavior are nearly identical for the Repeater and DataList controls.

Programming Interface of the DataList Control

The DataList is smarter than the Repeater and works by making some assumptions about the expected results. This is good news and bad news for you as a programmer. It means that in some cases much less code is needed to accomplish the same effect; on the other hand, it also indicates that you should know the behavior of the control very well to govern it. For example, the DataList assumes that no HTML tag is split across templates. This fact isn't a problem per se, but it can result in badly formed or totally unexpected HTML output. In addition, by default the DataList renders its entire output as an HTML table, meaning that if this is exactly what you want, there's no need for you to comply with

or

elements.

Before we look in detail at the programming interface of the DataList control, let's see what we should do to make the previous example (the scrolling page) work with a DataList. Once you have replaced with , set the RepeatDirection property to Horizontal on the Picker control.

 

Both the Picker and Customers control also need to have the RepeatLayout property set to the value of Flow.

 

Finally, modify the item template of the Picker control by removing any text other than the declaration. Likewise, remove the header and the footer template—the DataList will automatically provide the formatting features that you have to code yourself when the Repeater is used. The final result is shown in Figure 9-4.

Figure 9-4: The previously discussed scrollable list implemented with DataList controls.

The DataList has no methods aside from those provided by parent controls. It features a lot of properties and fires quite a few events. The DataList control is declared as follows:

public class DataList : BaseDataList, INamingContainer, IRepeatInfoUser

Like the DataGrid control, it inherits from BaseDataList and implements the INamingContainer interface. In addition, the DataList class implements the IRepeatInfoUser interface. The IRepeatInfoUser interface defines the properties and methods that must be implemented by any list control that repeats a list of items. This interface is also supported by the CheckBoxList and RadioButtonList controls. However, all of its properties and methods are marked as private and, as such, are inaccessible from within the user code.

Properties of the DataList Control

Table 9-5 lists the properties of the DataList control. The table doesn't include properties inherited from Control and WebControl.

Table 9-5: Properties of the DataList

Property

Description

AlternatingItemStyle

Gets the style properties for alternating rows.

AlternatingItemTemplate

Gets or sets the template for alternating items.

CellPadding

Gets or sets the space (in pixels) between the cell's border and the embedded text.

CellSpacing

Gets or sets the space (in pixels) between two consecutive cells, both horizontally and vertically.

Controls

Gets the collection of all the child controls in the list.

DataKeyField

Gets or sets the key field in the bound data source.

DataKeys

Gets a collection that stores the key values of all the records displayed in the list. The column used as the key is the one stored in the DataKeyField property.

DataMember

Gets or sets a string that indicates the specific table in a multimember data source to bind. The property works in conjunction with DataSource. If DataSource is a DataSet object, then DataMember contains the name of a child table to bind.

DataSource

Gets or sets the data source object that contains the values to populate the control.

EditItemIndex

Gets or sets the 0-based index of the item to edit.

EditItemStyle

Gets the style properties for the item being edited.

EditItemTemplate

Gets or sets the template for the item selected for editing.

ExtractTemplateRows

Gets or sets a value that instructs the DataList to extract the constituent rows from any HTML table in the templates. The extracted rows are then combined to create a single, all-encompassing HTML table. Set to false by default.

FooterStyle

Gets the style properties for the footer section.

FooterTemplate

Gets or sets the template for the footer section.

GridLines

Gets or sets a value that indicates whether all cells must have the border drawn. The property is ignored if the RepeatLayout property is set to RepeatLayout.Flow.

HeaderStyle

Gets the style properties for the heading section.

HeaderTemplate

Gets or sets the template for the heading section.

HorizontalAlign

Gets or sets the horizontal alignment of the text in the list.

Items

Gets the collection of the currently displayed items.

ItemStyle

Gets the style properties for the items in the list.

ItemTemplate

Gets or sets the template for the items in the DataList.

RepeatColumns

Gets or sets the number of columns to display.

RepeatDirection

Gets or sets a value that indicates whether the DataList control displays its items vertically or horizontally.

RepeatLayout

Gets or sets a value that indicates whether the output of the control is displayed within an automatically created table or by using a flow layout. In a flow layout, text is output in free form.

SelectedIndex

Gets or sets the index of the currently selected item.

SelectedItem

Gets the DataListItem object that represents the currently selected item.

SelectedItemStyle

Gets the style properties for the selected item.

SelectedItemTemplate

Gets or sets the template for the currently selected item.

SeparatorStyle

Gets the style properties of the separator used to break text between consecutive items.

SeparatorTemplate

Gets or sets the template for the separator.

ShowFooter

Gets or sets a value that indicates whether the footer section is to be displayed.

ShowHeader

Gets or sets a value that indicates whether the heading section is to be displayed.

The ShowFooter and ShowHeader properties are both set to true by default, but they're considered only if the FooterTemplate and HeaderTemplate properties, respectively, are non-null.

The data binding process of the DataList control is nearly identical to that of the Repeater, and the generation of the output also takes place according to the same rules. However, the DataList control has a richer programming interface that lets you handle two additional states—selected and editing. The currently selected item is tracked through the SelectedIndex and SelectedItem properties. The same occurs for the item being edited. In this case, the property is EditItemIndex. The in-place editing mechanism of a DataList is identical to that of a DataGrid control. (See Chapter 6.)

Events of the DataList Control

Table 9-6 lists the events that, under various circumstances, the DataList control can fire. The list doesn't include the base events (for example, Load and DataBinding) the class inherits from Control and WebControl.

Table 9-6: Events Fired by the DataList

Event

Description

CancelCommand

Fires when a button with a command name of Cancel is clicked for a displayed item.

DeleteCommand

Fires when a button with a command name of Delete is clicked for a displayed item.

EditCommand

Fires when a button with a command name of Edit is clicked for a displayed item.

ItemCommand

Fires when a button is clicked within the DataList control.

ItemCreated

Fires when a DataList item is being created.

ItemDataBound

Fires after an item in the DataList has been bound to its underlying data. The event occurs before the data item is added to the Items collection. (Note that only items and alternating items are bindable.)

SelectedIndexChanged

Fires when a new item is selected in the DataList.

UpdateCommand

Fires when a button with a command name of Update is clicked for a displayed item.

With the exception of ItemCreated and ItemDataBound, all the events relate to the execution of some sort of command. ItemCommand is the base command event and always fires before the more specific event, such as EditCommand or DeleteCommand. Regardless of the name, SelectedIndexChanged also is a command event that fires when a button with the command name of Select is clicked.

  Note

Command buttons don't necessarily have to be item specific. Any button within the user interface of the DataList control can be given a special command name, such as Edit or Delete. The effect is always that of firing the related event. Of course, if the button is not item specific, the logic of the application must determine the affected item and decide the proper action.

The Update and Cancel command buttons are normally generated by the DataList itself when a particular item enters into edit mode. Finally, note that the DataList class provides constants to identify special command names in a parametric way. The constants are CancelCommandName, DeleteCommandName, EditCommandName, SelectCommandName, and UpdateCommandName. They are implemented as static properties (shared properties according to Microsoft Visual Basic .NET jargon).

Formatting Capabilities

As mentioned earlier, the Repeater control is a general-purpose iterator and is limited to generating templated text without applying any extra formatting rules. The DataList control, on the other hand, provides additional features specifically for controlling the layout of the list. In particular, the DataList can render surrounding table rows and cells around its template-defined elements. This capability allows you to build more advanced and user-friendly layouts with less code. Key elements for taking advantage of this enhanced set of capabilities are the RepeatColumns, RepeatDirection, and RepeatLayout properties. They let you specify the number of columns the output will span and the direction (vertical or horizontal) in which to render the data items.

Table and Flow Layout

The RepeatLayout property takes its values from the RepeatLayout enumeration, which consists of two possibilities—Table and Flow. The default is Table. If the layout is set to Flow, the DataList wraps any template element within a tag. Next, it automatically adds a

element if the rendering direction is vertical. You should note that vertical is the default direction. If the direction is horizontal, the output text simply flows in a concatenated HTML string. The following DataList output is for a vertical Flow layout:

header template item template separator template footer template

The Table mode saves you from a lot of HTML coding if you're just going to build an HTML table. The following DataList output is for a vertical Table layout:

header template
item template
separator template
footer template

If the direction is horizontal, the layout of the table is slightly different and is composed of only three table rows—the header, one row for all the items, and the footer.

header template
item template separator template
footer template

When the ExtractTemplateRows property is set to true, the DataList control extracts table rows out of all declared templates and merges them into an all-encompassing table. The ExtractTemplateRows property allows you to create a single table from other smaller tables defined for each DataList template. The use of the property is subject to a few restrictions. In particular, you must provide a well-formed Table control (the tag) for each template you want to include in the output. An exception would be thrown if you specify the table using the HTML

element or the ASP.NET HtmlTable control—the
element. Note also that all templates must be expressed using the Table control and that only the rows of the tables will be displayed. All other content in the templates will be ignored. The following code shows how to define a table in an item template:

<%# DataBinder.Eval(Container.DataItem, "Name") %>

The ExtractTemplateRows property is really helpful only when you're going to create quite a complex structure in which tables with different templates are to be merged together. You can use the ColumnSpan and RowSpan properties of the TableCell object to control the number of columns and rows the cell spans.

  Note

When the ExtractTemplateRows property is set to true, the RepeatColumns, RepeatDirection, and RepeatLayout properties are ignored and do not affect the appearance of the DataList control.

Extracting Template Rows

Let's drill down into a common scenario in which ExtractTemplateRows proves extremely useful. Suppose that you need to create a report in which multiple rows for each data item should be displayed. Assuming that you want it to work with a table of customers, let's say that you want to display the customer ID and name on the first row and the address on the second.

With a Repeater, you could accomplish this by defining a table in the header template and then defining a couple of rows for each item. If you don't mind using a Repeater, this is just fine. However, the DataList control has styling and formatting capabilities that one wouldn't easily sacrifice. The DataList, though, doesn't let you break HTML elements across templates. In other words, this means that you cannot place the opening tag of a table in the header and close it in the footer.

The DataList control requires that you express the layout of each template using stand-alone tables. You can use, for instance, a table for the header and a table for the item template. However, if you set the ExtractTemplateRows property to true, all the rows of the various tables will automatically converge into a unique, all-encompassing table.

Customer Contact <%# DataBinder.Eval(Container.DataItem, "CustomerID") %> <%# DataBinder.Eval(Container.DataItem, "ContactName") %> <%# DataBinder.Eval(Container.DataItem, "Address") + " - " + DataBinder.Eval(Container.DataItem, "City") %>

At display time, the DataList merges the two Table objects and removes any table element that is not an instance of the TableRow class. The final table contains two columns, and each record displays on two different rows, the second of which contains a single cell spanning two columns. The DataList control is smart enough to consistently manage visual settings (for example, the alternating item styles) for all table rows that actually participate in a data item row. The results are shown in Figure 9-5.

Figure 9-5: The DataList shows a data table in which all records display in two table rows.

The combined effect of these settings is that the link displays as normal text until the mouse moves over it. When that happens, the color switches to red and an underline appears to restore the typical look and feel of HTML clickable elements, as shown in Figure 9-7.

Figure 9-7: A selectable element in the output of a DataList control.

With DataGrid controls, the following tag would suffice to set the background color when a row is selected:

 

If you use that tag with a DataList control, though, the graphical aspect of the selected row is not what you would expect. As Figure 9-8 shows, with a Flow layout only the individual controls have the background color changed.

Figure 9-8: A selectable element in the output of a DataList control.

To extend the new background color to the whole row, use the following style declaration, which adds an explicit width. The result is shown in Figure 9-9.

Figure 9-9: A listbox-like selection in a DataList control.

At this point, we have a fully functional infrastructure for handling the item selection event. To take a particular action on the selected item, register a handler for the SelectedIndexChanged event and update the user interface.

Keeping the Selected Item in View

The second problem—ensuring that the portion of the DataList output that contains the selected item is included in the current view—requires a more subtle solution. In this example, we made the DataList output scrollable by surrounding the data items with a

element and setting the overflow style property to the value of auto.

However, when the page posts back, the scrollbar that lets you move back and forth over the DataList output is not automatically restored to the position occupied beforehand. Suppose that you scroll down and select one of the last customers in the list. When the page is re-created after the selection, the scrollbar is reset and the selected item is not in the default view. As a result, you have to scroll down again to see its updated content. Is there an automatic way to scroll a particular item into view?

The Dynamic HTML (DHTML) object model provides a large number of HTML elements with a method named scrollIntoView. The following JavaScript function illustrates the working of the scrollIntoView DHTML method:

 

The function takes the ID of an HTML element as its sole argument. Next, it attempts to locate the element in the page object model. If successful, it calls the scrollIntoView method to ensure the specified element is visible within its container. The container can be the page as well as a container tag such as

. The Boolean argument you pass to the method indicates whether the element must be shown at the top or the bottom of the view. Although this trick is not acceptable with all browsers, it represents the only possibility we have to scroll an HTML element into view. Let's see how to integrate this piece of code with the existing DataList.

To call the scrollIntoView method, we need to assign a unique HTML name to all the data items displayed by the DataList. Furthermore, the ID we assign to each item must be easily accessible from within the SelectedIndexChanged event handler. Whenever a new item is selected, we execute some ASP.NET code that configures the HTML page's onload method. In particular, we link the page's onload method to the JavaScript function just shown. The function is passed as an argument the name of the DHTML object that represents the currently selected DataList item.

In the following code snippet, we surround the contents of an item with a tag whose ID matches the value of the customerid column for the item being rendered:

In addition, we set the DataKeyField property of the DataList with the name of the customerid column and add a handler for the SelectedIndexChanged event.

When the user selects a new item, we retrieve the value of key field—the customerid column—and prepare the JavaScript call to link with the onload event on the HTML page.

public void SelectedIndexChanged(object sender, EventArgs e) { // Get the customerid of the currently selected item string custID = (string) Customers.DataKeys[Customers.SelectedIndex]; // Prepare the Javascript call. // For example, if the current item has an ID of ALFKI, the // Javascript call will be ScrollToElem('ALFKI') string js = "ScrollToElem('{0}')"; js = String.Format(js, custID); // Register the Javascript call with the onload event TheBody.Attributes["onload"] = js; }

The final step entails that we bind the JavaScript call to the onload event of the HTML page. There are two ways of accomplishing this from within ASP.NET code. You can either use the Page RegisterStartupScript method or set the onload attribute on the

The net effect of these changes is that whenever the user selects a new item, the page posts back and executes the SelectedIndexChanged method. The method updates the JavaScript function call associated with the page's onload event to reflect the current item. Next, the page is rendered and sent back to the browser. On the client, the onload event scrolls into view the piece of HTML code that corresponds to the currently selected item of the server-side DataList control.

Adding Support for Paging

The DataList control has a more free-form user interface than the DataGrid control. This consideration alone makes the DataList control particularly compelling to many developers who have the need to create interactive reports outside the relatively standard visualization pattern employed by grids. For example, no matter how powerful and customizable a grid can be, you can hardly force it to display a data-source column in more columns or rows of data. By contrast, you can easily obtain that flexibility using DataList controls. However, the DataList control doesn't support pagination.

Pagination is the control's ability to display equally sized blocks of data according to an internal index the user can modify through links. The DataGrid user interface incorporates a pager bar element, which is nothing more than a table row with links to internal pieces of code that handle the page movements. The DataList user interface doesn't provide any predefined link bar for pagination, meaning that the host page can define anywhere in the body a couple of links to move the data source back and forth.

Rendering Pages of Data

The following code snippets illustrate the page-level code that controls the current index and the contents of the current page:

private void OnPreviousPage(object sender, EventArgs e) { CurrentPageIndex -= 1; RefreshPage(); } private void OnNextPage(object sender, EventArgs e) { CurrentPageIndex += 1; RefreshPage(); }

The code adjusts the page index—a global variable named CurrentPageIndex—and binds the data to the DataList control. RefreshPage is a page-level helper routine that extracts the data to bind from the database or, more logically, from a server-side cache.

private void RefreshPage() { DataTable _data = GetData(); AdjustPageIndex(_data.Rows.Count); CurrentPage.Text = (CurrentPageIndex +1).ToString(); list.DataSource = GetPage(_data, CurrentPageIndex); list.DataBind(); } private DataTable GetData() { // Try to get the data from the session cache DataTable _data = (DataTable) Session["MyData"]; // If no data is available, read from the database if (_data == null) _data = LoadData(); return _data; }

The GetPage method is responsible for extracting the right subset of rows that fit into the current page. The page of data is returned as a DataTable object and is bound to the DataList.

private DataTable GetPage(DataTable dt, int pageIndex) { if (dt==null) dt = LoadData(); int firstIndexInPage = (CurrentPageIndex*PageSize); DataRowCollection rows = dt.Rows; DataTable target = dt.Clone(); for (int i=0; i

The function clones the original table of data and creates a new, smaller table with only the rows that fit into the specified page index. The Clone method creates a new empty object with the same structure as the sample. The ImportRow method duplicates and copies a new row from one DataTable object to another. Note that a deep copy of the row is necessary because ADO.NET objects can't be contained in more than one parent object.

 
    Note

As we saw in Chapter 6 and Chapter 7, the DataGrid control supports two flavors of pagination—automatic and custom. In automatic paging, you bind the control to a data source for the lifetime of the control and, while you page, the control itself takes care of extracting all the records that need to be displayed. Custom paging, on the other hand, loads in memory all the records to display. Implementing a form of custom paging for a DataList control is straightforward and, in a certain way, this is exactly what we did when we created a list of buttons with the initials of the available customers. The preceding code completes the demonstration showing how to extract records from a cache of rows. However, note that the DataGrid control relies on an ad hoc class to perform paging related tasks—the PagedDataSource class. You could rewrite this code using an instance of the PagedDataSource class for the actual pagination.

 
 

Tracking the Page Index

For effective pagination, at least a couple of properties are needed: CurrentPageIndex and PageSize. The sample page in Figure 9-10 defines them as global properties on the page. If you plan to incorporate the code discussed here in a self-contained control, CurrentPageIndex and PageSize have to be public properties on the new class.

Figure 9-10: All the customers are displayed one page at a time in three columns of data. The link buttons provide for page movements and cause the DataList to refresh its contents.

The CurrentPageIndex property contains the 0-based index relative to the current page. The PageSize property defines the maximum number of rows permitted per page.

public int PageSize = 12; public int CurrentPageIndex { get { if (ViewState["CurrentPageIndex"] == null) return 0; int _tmp = (int) ViewState["CurrentPageIndex"]; return _tmp; } set {ViewState["CurrentPageIndex"] = value;} }

It's interesting to note that unlike PageSize, CurrentPageIndex must necessarily be persisted across multiple page requests. This can easily be done using the ViewState collection. As the preceding code clearly shows, the value of the property is not stored in a class member but in the page ViewState bag. This ensures that its value will always be up to date.

Conclusion

In this chapter, we examined the characteristics of two iterative controls available with the ASP.NET programming platform. The two controls are the Repeater and DataList controls. A third iterative control—the DataGrid—has been widely covered in previous chapters. Iterative controls differ from list controls because of their native support for templates and their extremely flexible rendering engine. Iterative and list controls have some points in common too. Both can be bound to a data source and display a list of items. The key point is in the algorithm and the templates that each control, and category of controls, employs.

In an ideal scale, the Repeater is the simplest of all controls, but also the most flexible; the DataGrid has the richest set of features but is also the one that ties the developer to a multicolumn, tabular view. The DataList control falls somewhere in the middle, but it's logically closer to the DataGrid control. Just like the Repeater, the DataList displays the contents of a data-bound list through ASP.NET templates. But just like the DataGrid, it supports selecting and in-place editing and can have its look and feel customized using style properties.

Compared to the Repeater control, the DataList also shows off predefined layouts and more advanced formatting capabilities. Compared to the DataGrid, on the other hand, it lacks a key functionality: the ability to page through bound data. In this chapter, we've exploited the features of iterative controls, along with some HTML and JavaScript tricks, to build more effective and powerful components, and we discussed how to work around the lack of paging support in the DataList control.

In particular, we've seen how to build a sort of custom list box in which each item is free and based on a template. We wrote a lot of code in each page, both C# (or Visual Basic .NET) and ASP.NET layout code. It's about time we start practicing with more compact software elements that provide some level of code reusability. In Chapter 10, in fact, we'll cover Web user controls.

Resources

  • Paging in DataList (http://www.aspalliance.com/das/datalistpaging.aspx)
  • Creating Master-Detail Listings using ASP.NET controls (http://www.dotnetbips.com/displayarticle.aspx?id=184)
  • Paging Results with ASP.NET's PagedDataSource (http://www.sitepoint.com/article/921)

Web Forms User Controls

  Note

The need to merge multiple tables explains the strict requirement set on the use of the tag, which corresponds to the Table object. The DataList builds the resulting table object in memory. If you use a

, whether it's with or without the runat attribute, other objects are involved and the trick won't work.

Multicolumn Rendering

The output of a DataList control can be rendered over multiple columns whose number you specify by using the RepeatColumns property. By default, the text displays in one column. You can create multicolumn output by using either a flow or a table layout. However, the ideal way of repeating items over multiple columns is within a fixed table layout. Figure 9-6 shows the effect of the RepeatColumns property along with some possible shortcomings.

Figure 9-6: A DataList control that renders its output over three columns.

As you can see, the vertical alignment of the cells is the default and no properties are allowed to modify it programmatically.

Templates of the DataList Control

In addition to all the templates supported by the Repeater control, the DataList control supports two new templates—SelectedItemTemplate and EditItemTemplate. The SelectedItemTemplate template lets you control the contents and functionality of the selected item. The EditItemTemplate template takes care of in-place editing in much the same way as with DataGrid controls.

The SelectedItemTemplate Template

The appearance of the selected item is controlled by the SelectedItemStyle property. The following code changes the background color of the selected item to the specified color:

 

As for the DataGrid control, you must trigger the selection process using a command button labeled with the Select command name. If you want to do more, such as adding information or controls to the currently selected item, you can even replace the default template with a new one. You specify a template for the selected item through the tag. A similar mechanism greatly simplifies the building of master/detail views. Once the row has been selected, in fact, you display an ad hoc template that includes any child tables that are needed.

If you need to programmatically access the currently selected item, you use the SelectedItem property. It returns the instance of the DataListItem object that corresponds to the specified index. For each displayed row, the DataKeys collection contains the source values stored in the column that the DataKeyField column specifies.

// Get the key of the currently selected record and // drills down into the available data int key = (int) list.DataKeys[list.SelectedIndex]; GetChildData(key);

Items can be selected programmatically by setting the SelectedIndex property to a 0-based value. A value of –1 means that no item is currently selected. Note that the value stored in the SelectedIndex property is persisted in the view state and survives across consecutive page requests. For this reason, you should reset it whenever a new page of data is displayed.

The EditItemTemplate Template

You use the EditItemTemplate property to control the contents of the item selected for editing in a DataList control. The look and feel of the item selected for editing is controlled by the EditItemStyle property. The EditItemIndex property contains the 0-based index of the item being edited. Also, this value is cached in the view state, so you should reset it whenever your data set is bound to the control changes.

The in-place editing feature of the DataList control is nearly identical to DataGrid in-place editing when all columns are based on templates. The DataList fires the EditCommand event when the user clicks a button associated with the Edit command. However, the control enters into edit mode only if the event handler sets the EditItemIndex to a 0-based value. When the user clicks to commit the changes, the UpdateCommand event fires. You retrieve the current values from input fields using the FindControl method and then proceed to persist those changes. The overall procedure is identical to what we saw in Chapter 6 regarding DataGrid controls.

Working with the DataList Control

Let's add item selection to the page shown in Figure 9-4. The idea is to transform the scrollable area of the DataList into something that closely resembles a custom list box. The DataList renders its contents vertically using the Flow layout. To start, let's remove the


element from the separator template so that we save a few pixels of space.

Note that an empty SeparatorTemplate is not ignored but is rendered with an extra

element. To set up a functional and effective selection mechanism, we need to handle the SelectedIndexChanged event and define the SelectedItemStyle. In this case, we don't need to change the template of the item. That feature would be needed to make the component support drill-down functionality, however.

If you've played a little bit with the page shown in Figure 9-4, you should have noticed that whenever you refresh the view (for example, you checked a few items), the scrollbar is reset to its topmost position. This is boring, especially if you have a relatively long list of items; you scroll the view to locate the item to expand and then when the page is refreshed, the item of interest is out of view. This is another problem we'll address in a moment.

Adding Support for Selection

The first point to consider is, how can we make a displayed item selectable? We need to add a command button labeled with the special keyword Select. If, for some reason, you need more control over the whole process of selection, you could give the clickable element any command name. Next, when ItemCommand fires, you recognize the command and activate the selection yourself by setting the SelectedIndex property to a correct index. Note that the default process—applying a particular style and template—begins at this point. The trigger for the process is buried in the code that sets the SelectedIndex property.

Making an Item Selectable

The header of each item—that is, the customer's ID and company name—is a good candidate to become the trigger for the selection. Instead of rendering it as plain text, we surround it with a link button.

 

The text of the link is defined by the following function that simply concatenates a couple of source columns:

private string SetCustomerName(object dataItem) { DataRowView _row = (DataRowView) dataItem; string _output = "{0} - {1}"; _output = String.Format(_output, _row["customerid"], _row["companyname"]); return _output; }

The appearance of the link button is controlled by a couple of page-wide styles that we also used in previous chapters.

tag. I have chosen the second option because it's clearer and more intuitive. As we saw in Chapter 1, "The Web Forms Model," to programmatically assign a property on the body of the page, you must first give it an ID and mark it as runat="server".

 

Категории