From the July 2001 issue of MSDN Magazine.

MSDN Magazine

Custom Web Data Reporting
Dino Esposito
Download the code for this article:Cutting0107.exe (40KB)
Browse the code for this article at Code Center: CustDataRep

T

he ASP.NET programming interface supplies five controls for rendering collections of data and creating effective reports. Four controls—Table, Repeater, DataList, and DataGrid—belong to the ASP.NET System.Web.UI.WebControls namespace asp, whereas the HtmlTable control is an ASP.NET server control that merely mimics the behavior of the HTML <table> tag. (For a more detailed review of the differences between the two main ASP.NET namespaces, please refer to Cutting Edge in the February 2001 issue of MSDN® Magazine.) In the last few installments of this column, I've been analyzing and testing the features of the DataGrid control. This month, I'll be taking an in-depth look at the DataList and Repeater controls.
      I won't spend much time on HtmlTable, whose programming interface you should be quite familiar with. I'll just remind you that HtmlTable is a general-purpose programmable table that can display any combination of HTML text and controls. It differs from the HTML <table> tag only in that it is an ASP.NET server-side control that you can instantiate on the server. Once you've set the element's runat attribute to "server", HtmlTable elements are live components that you can code against, and programmatically set properties and methods. Like HtmlTable, <asp:Table> renders an HTML <table> tag. However, the main difference is that <asp:Table> offers a strongly typed object model like other controls in the System.Web.UI.WebControls namespace. Although largely supported by all browsers, such tables have rather limited functionality. For example, they neither let you use templates for display nor provide repeating capability. All the content must be specified cell by cell and row by row. In other words, HtmlTable controls aren't data-bound. This doesn't mean that they cannot show content coming from a data source, only that none of the rows can be inferred by reading an in-memory or disk-based data source. In other words, they can't be automatically populated.
      In contrast, the ASP.NET counterpart of the HTML <table> tag supplies a consistent programming model to dynamically add cells and rows. This difference is due to the fact that ASP.NET controls live on the server, so they don't need any special support from the Web browser.

The Repeater Control

      The <asp:Repeater> control is the base ASP.NET control for data binding. In some aspects, it is both more advanced than the HtmlTable and even simpler in that it requires more coding. The Repeater control does just what its name suggests. It loops through a collection of data (arrays, XML files, datasets, and so on) and produces a list of displayable items—one item for each row in the data source. It is inherently data-bound so you don't have to programmatically add cells one at a time.
      The Repeater control has no predefined layout. It iterates through your data source, but you're responsible for the layout. Thus, a displayable item is anything created by your item's templates. You can specify templates for normal and alternating items, separators, footer, and header. The granularity of items is the data source row as a whole; there's no explicit support for data columns and fields. However, because the control is inherently data-bound, you can access any of the columns your data collection contains when defining the templates.
      The item-based nature of the Repeater control makes it similar to a DataGrid control. However, the range of items they each support sets them apart. For example, the Repeater control doesn't provide any native facility for selection, paging, sorting, and editing.
      Figure 1 shows the code necessary to create an ordinary table with the Repeater control, which is shown in Figure 2. The table contains the names of the subfolders found below the My Pictures system folder on the server. It utilizes Microsoft® .NET classes to list directories and files in a given system path. You don't need to fill each row and cell yourself. Instead, you specify how the header and the footer must look and how you want each item to display.

Figure 2 Repeater in Action
Figure 2 Repeater in Action

      Repeater mimics the behavior of the tabular data ActiveX® control, which implements the client-side data binding available since Microsoft Internet Explorer 4.0. That control provides no built-in support for paging, but can specify a template for all the child items that are repeatedly called within a container control.
      Changing the layout of the final page using the Repeater control is as easy as changing the Repeater's templates. The code in Figure 3 produces the screen shown in Figure 4.

Figure 4 Creating Folder Buttons with Repeater
Figure 4 Creating Folder Buttons with Repeater

      The subdirectories of the My Pictures folder are now rendered as buttons. When you click on one of them, its content will be displayed below in the Explorer view. This is accomplished by setting the src attribute of an <iframe> tag to the folder path. This is what's executed when the user clicks on a folder button.
      You can normally enrich the programming interface of the <asp:button> server control by adding an OnClick handler and even an ID.

  <asp:button runat="server" width="100%"
  
cssclass="MyButton"
id="btnFolder" OnClick="OnFolderSelected"
Text='<%# DataBinder.Eval(Container.DataItem, "FolderName") %>'
/>

 

      When ASP.NET button controls are used within DataLists or Repeaters, you generally respond to their events differently than when they were standing alone in a form. A click on a button in a container control generates an event that gets bubbled up to the container control, where a container-specific event is fired. For DataList and Repeater controls, this event is the ItemCommand event.

  void ItemCommand(
  
Object Sender,
RepeaterCommandEventArgs e)

 

      The list control can contain several different buttons. To identify a specific button, you set a button's CommandName property to a certain value and check it later. In addition, you can also rely on the CommandArgument attribute, which plays the role of one argument for the command name.
      The following code shows how to access the button from the ItemCommand event handler:

  String strDir = strMyPicturesFolder;
  
Button b = (Button) e.CommandSource;
fViewPictures.Attributes["src"] =
strMyPicturesFolder + "\\" + b.Text;

 

      As a side note, you probably noticed that I used an indirect syntax to set the src property of the <iframe> element. While you may have expected to see this:

  fViewPictures.src = ...
  

 

I actually used this:

  fViewPictures.Attributes["src"] = ...
  

 

If you try to set src directly, the ASP.NET runtime complains about the lack of a src property in the programming interface of the HtmlGenericControl server component. This is a limitation of all of the ASP.NET controls in the System.Web.UI.HtmlControls namespace: no strongly typed object model. For all of the HtmlControls, however, you can add attributes to the rendered tag by adding items to the control's Attributes collection.
      In the long list of ASP.NET server controls—it numbers 45 different controls as of Beta 1—there isn't one that's a perfect reproduction of the HTML <iframe> tag. ASP.NET componentized most of the HTML tags. The <iframe> tag is one of the few exceptions as it is rendered through a generic control. HtmlGenericControl features a few common properties and methods, but none of them execute typical functions of the <iframe>. However, the Attributes collection has been introduced to let you access properties. So functionally speaking you don't lose anything, and you can continue using custom expando properties on HTML tags.

The DataList Control

      The demo shown earlier renders a collection of pictures that reside on the server. It's relatively simple code that lets you publish the contents of a server-based folder to the Web in an Explorer view. But it could be more elegant. To improve it, get rid of the <iframe> and manually populate the page with images from a given folder.
      However, you are completely responsible for the graphical form that a displayable item can take (button, cell, anchor, and so on) with a Repeater control. If you want preset choices, you'll have to use the DataList control.
      DataList supports directional rendering—items can flow horizontally or vertically. It can behave like a table, letting you dispense with much of the code in Figure 1. DataList has built-in support for selection and editing as well as better graphical capabilities.
      To provide for selection and in-place editing, the DataList control has an internal mechanism to recognize the various items, another significant departure from the Repeater control. Figure 5 shows the much longer list of the DataList properties. If you compare this list with the DataGrid control (see Figure 8 of the March 2001 installment of Cutting Edge), you'll see that they have much in common. (The DataGrid, though, is still the richest of all the list-bound controls.)
      The structure of the code looks much the same, except for a number of additional properties (see Figure 6). The final result can be seen in Figure 7. The various items are displayed horizontally, 10 items per row, using a table-based template. The <img> tags are added into the cells of a <table> that is automatically generated when the repeatlayout property is missing or explicitly set to "Table". If you don't want the Table layout, the only alternative is the Flow style. In this case, the items will be displayed as consecutive <span> tags with a <br> tag to break the line when the given number of items per row has been reached.

Figure 7 DataList in Action
Figure 7 DataList in Action

Table Item Styles

      The DataList's Items collection contains the members of the DataList control that have been associated through the DataSource property. The output of the DataList—whether it's a <table> or a sequence of <span> tags—is generated when the DataBind method is called on the control or, more generally, on all the controls within the page.
      In light of this, the OnFolderSelected handler I introduced earlier must be rewritten like this:

  void OnFolderSelected(Object sender, EventArgs e)
  
{
String strDir = strMyPicturesFolder;
Button b = (Button) sender;

lblFolder.Text = b.Text;
DataList1.DataSource = CreateImageDataSource(
strDir + "\\" + b.Text);
DataList1.DataBind();
}

 

      If any header is present in the control's body template, then it is added first. In the page displayed in Figure 7, there's no header, but an <h2> tag with the name of the folder could have been used.
      Below the header (if there is one) you find all the items in the data source. Items flow horizontally or vertically based on the RepeatDirection and the RepeatColumns settings. If the RepeatLayout is set to Table, they flow in cells and rows, respectively. Should the RepeatLayout have a non-default value of Flow, then items would form a single row or a single line break-separated column.
      If you want, you can also define a special separator that will be drawn between items, both horizontally or vertically. To do so, you simply define a SeparatorTemplate. Separators, like header and footer templates, are not data-bindable and are not added to the Items collection.
      The footer (if any) is then added at the end of the output. As shown in Figure 7, the footer is merely a horizontal rule produced by an <hr> tag.
      When dealing with any of the templates, the control raises an ItemCreated event where the event argument—an object of type DataListItemEventArgs—contains information about the item itself and the specific type. The following code—once again borrowed from the corresponding DataGrid code—represents the skeleton of the typical handler:

  void PageItemCreated(Object sender,
  
DataListItemEventArgs e)
{
ListItemType itemType = e.Item.ItemType;
if (itemType == ListItemType.Header)
•••
}

 

      The ItemCreated event also fires for separators. You cannot programmatically add to or remove elements from the Items collection. However, by catching the ItemCreated event, you can do whatever you want with the item, among other things changing the text and the layout.
      The DataList control lets you define up to seven different styles for particular item types. In addition to the item, the alternating item (by the way, there's no creation event for such items), footer, header, and separators, you can also define a CSS style for the item being selected and edited.

  <asp:datalist ...>
  
<property name="SelectedItemStyle">
<asp:TableItemStyle
BorderColor="black"
BorderWidth="3"
BackColor="#9BDBFF" />
</property>
</asp:datalist>

 

      Selection works in much the same way as it does with DataGrids. First you define the style for the selected item, then you put in the item's template a button control with the CommandName property set to "select". This is only slightly different from DataGrids where you had to insert a <asp:ButtonColumn> column, which is nothing more than a column rendered through a button with a CommandName of Select. In Figure 8 you can see how selection works. Figure 9 shows the source code.

Figure 8 Adding Selection
Figure 8 Adding Selection

Practical Uses of DataLists

      Needless to say, the DataList control is certainly much more usable than the Repeater control. Yet DataList is less specialized than the DataGrid control that I examined in great depth in the last four installments of this column.
      DataList can be used to implement a form-based front-end for data sources. In fact, it's quite good for presenting data in a grid fashion, where each record is rendered on a different row and many different rows are shown at once. What if you want a form-based representation of the same dataset? Here's where the DataList control is especially handy.
      By default, the DataList control doesn't support pagination. This means that all the records are displayed as a whole, one after the next. Without pagination, you can't set up a form-based view of the data. A form view makes sense only if the user can select a single record to look at.
      Supplying pagination, though, is not particularly difficult especially if you know how to implement it with DataGrids. (Once again, look back at the last four installments of this column for more details about ASP.NET data-bound grids.)
      There are two things to keep in mind. First, be aware that when you rebind the data control—you do that by calling the DataBind method—all the currently accessible rows in the associated data source are copied into the control's Items collection and are displayed. Second, if you limit the number of visible rows to fit the page, make sure you refresh this content when the user moves to another page. Implementing a form-based view means dealing with pages of just one row. Pageable DataGrids manage this for you under the hood of the PageIndexChanged event. But you're responsible for this if you disable the automatic pagination feature in DataGrids or if you're working with DataList controls.

Implementing a Form-based View

      The most efficient way to implement a form-based view involves two steps. First, read all of your data at once and store it in Session state. Second, use a row filter to hide all the undesired records.
      It goes without saying that if you have one million records to manage, either the form-based approach won't work at all or you'll have to implement several levels of reading and caching. Generally speaking, if you have a reasonable amount of data this is an ideal approach because it provides you with an excellent mix of easy maintenance, programming comfort, and performance results.
      You can start by writing a procedure that reads the dataset from the database server. You normally invoke such code only once in the session lifecycle. You might want to run it more frequently if your application needs to work on fresh data.

  void Page_Load(Object sender, EventArgs e)
  
{
if (!Page.IsPostBack)
{
CreateDataSource();
DataList1.DataSource = GetView();
DataList1.DataBind();
}
}

 

      The CreateDataSource method will create the dataset and store it, in whole or in part, with the Session manager. The GetView function will be responsible for taking the right subset of information out of the dataset.

    public ICollection GetView()
  

 

The function returns an ICollection-based object that is actually a DataView. This view of the data will be obtained by filtering the rows of the main table that CreateDataSource prepared upon page loading. The index of the next page to show is also kept in memory in a proper Session variable.

  public ICollection GetView()
  
{
DataTable data = (DataTable) Session["MyTable"];
int nCurrentIndex = (int) Session["CurrentIndex"];

DataView dv = new DataView();
dv.Table = data;

dv.RowFilter = "EmployeeID=" +
data.Rows[nCurrentIndex-1]["employeeid"];
return dv;
}

 

      The view is initialized from the original table of data and then a filter is applied on its rows. In this example, I just want to move from the first to the last record. Unfortunately, there's no way with DataView to move to a row in any particular position. The string you store in the RowFilter property must be an expression that involves the fields of the table. For this reason, I decided to filter on the EmployeeID field but used the current record index not as a value to match, but as a selector for the value in the corresponding record in the table. The following snippet basically says "show me the record whose EmployeeID field matches the value on the same field in the nth record."

  dv.RowFilter = "EmployeeID=" +
  
data.Rows[nCurrentIndex-1]["employeeid"];

 

      By just adding a couple of buttons for moving back and forth, the DataList form-based view is ready to serve. You can see the final output in Figure 10. The content of the form is determined by the item template in Figure 11. The full source code for this page is shown in Figure 12.

Figure 10 DataList Form-based View
Figure 10 DataList Form-based View

      It's interesting to note that, at least in this relatively simple code, you can replace the <asp:datalist> control with the <asp:repeater> control and obtain exactly the same result as for the user interface. The Repeater control is simple and more lightweight than the DataList, which are qualities you might prefer.

More on Filters

      A row filter is a logical mask you apply to your data tables in order to create a new view of your data. The default view for an ADO.NET DataTable is the view in which no filter has been applied and the RowFilter property is equal to the empty string.
      The RowFilter property looks much the same as the Filter property of ADO Recordset objects. It has to be a string composed in the same format as the WHERE clause of a SQL expression. There are a few differences with recordset's filters, though, that warrant further explanation. First, an ADO.NET filter applies to a freshly created object—the DataView—rather than to an existing instance of the data object—the Recordset. This makes a significant difference because it saves you from the housekeeping code of ADO Recordsets and makes it much faster for you to move from record to record. The ADO.NET DataView object maintains a reference to the parent Table—yet it is a brand new object that contains only a subset of the records. A DataView object represents a customized view of a DataTable that you can use for sorting, filtering, searching, editing, and navigating the original data.
      You create DataView objects through two flavors of constructors, as shown here:

  DataView dv1 = new DataView();
  
DataView dv2 = new DataView(myDataTable);

 

In any case, you always need a reference to a DataTable for the view to be effective. As soon as you set the RowFilter property, the filtering takes effect.
      When defining the format that a row filter expression must have, consider that you can use operators such as LIKE and aggregates such as Sum, Min, Max, and Avg. Any literal must be single-quoted and dates must be wrapped by # characters.
      Special operators are available to allow a great deal of flexibility. For example, you can rely on syntax constructs like IIF, IsNull, Len, and Trim.
      Finally, through views you can enrich you data-bound controls with interesting filter capabilities. In particular, let's suppose that you're displaying data that a user can edit. All the changes are kept in memory waiting for the user to click the Submit or Update button. By setting the RowStateFilter of the DataGrid's view to special constants such as DataViewRowState.New, DataViewRowState.Modified, or DataViewRowState.Deleted you can show only the records that have been modified, deleted, or added. In this way, the user can review all the changes entered before proceeding with the definitive update. Of course, for even greater flexibility you could combine RowFilter and RowStateFilter to show your users only, say, the modified records that match certain criteria.

Coming Next

      The next natural step in the study of ASP.NET data-bound controls is customizing the controls themselves by writing new components that can inherit from a base control. Starting next month, I'll be reviewing the steps necessary to create even richer ASP.NET server controls.

Send questions and comments for Dino to cutting@microsoft.com.

Dino Esposito is a trainer and consultant based in Rome, Italy. Author of several books for Wrox Press, he now spends most of his time teaching classes on ASP.NET and ADO.NET for Wintellect (https://www.wintellect.com). Get in touch with Dino at dinoe@wintellect.com.