Cutting Edge

Data Repeater Controls in ASP.NET

Dino Esposito

Code download available at:CuttingEdge0506.exe(144 KB)

Contents

The Repeater Control
Template Properties
User Controls
Implementing the ITemplate Interface
List Controls
ItemCreated Event
When to Use Which

The ASP.NET Repeater is a basic container control that allows you to create custom lists from any data available to the page. It's a handy control, especially since most ASP.NET pages that display data need to repeat the same kinds of data over and over. In this month's installment, I'll take a look at the Repeater control and discuss when it's a good idea to use it and when other tools may be a better option. Let's get started.

The Repeater Control

Imagine you're told to create a page that pulls a list of order records from a database and displays highlights. I define highlights as the most pertinent information for a record, a handful of a table's columns formatted any way that suits you, your client, and your boss. For example, it could be the order ID in bold on a row, then a horizontal ruler followed by order date, followed by a word that indicates if the order has been shipped or not. How would you create a page like this? And what kind of repeater tool would you need? Let's consider the most obvious solution first—the ASP.NET Repeater control.

The Repeater control does exactly what its name suggests—it repeats the specified data-bound templates without applying any sort of additional formatting. For example, the Repeater doesn't create a table or a vertical or horizontal list of data by default. You determine the final layout of the generated markup by creating ASP.NET templates and explicitly adding markup that separates one row from the next.

The canonical example of a separator element in the context of a free-form repeater control is the <BR> tag. However, the markup for the separator is strictly page-specific and is up to you. The most clear and elegant way to specify a separator is through its own template, named SeparatorTemplate.

When the page runs, the control loops through the records in the data source and renders the specified template for each record. In addition, before and after processing the data items, the Repeater emits some markup for the header and the footer of the resulting structure. Because the Repeater control has no default appearance, it is useful for creating virtually any kind of list with any layout and style.

The following is some typical Repeater-based code to produce the aforementioned orders list:

<asp:Repeater runat="server"> <ItemTemplate> <%# DataBinder.Eval(Container.DataItem, "orderid") %> <hr> <%# DataBinder.Eval(Container.DataItem, "orderdate") %> </ItemTemplate> <SeparatorTemplate> <br> </SeparatorTemplate> </asp:Repeater>

Technically speaking, the Repeater is an iterative control because it iterates over a data source and produces its output. Most data-bound controls follow this pattern, especially list controls. ASP.NET list controls derive from the ListControl base class and include controls such as CheckBoxList, ListBox, DropDownList, RadioButtonList (and BulletedList in ASP.NET 2.0). All of these create a particular type of list based on the records in the data source. Another control with "list" in its name, the DataList, is another story. Despite its name, the DataList is an iterative control that's a big brother to the Repeater. The two are roughly the same, except that the DataList has a richer set of built-in capabilities as far as templates and layout are concerned.

What do list controls like CheckBoxList and DropDownList have in common? And how do they differ from the Repeater? Both list and iterative controls output markup for every row of data bound to the control. Whereas list controls have a built-in rendering, iterative controls such as Repeater and DataList apply a custom and user-defined template that can be changed dynamically. This information will come in handy later.

So what kind of code would you use to create the page that takes a bunch of orders and renders them in a list? Figure 1 shows the simplest, and perhaps most natural, way of doing this.

Figure 1 Using a Repeater Control to Create a List

<%@ Page Language="C#" %> <%@ Import Namespace="System.Data" %> <%@ Import Namespace="System.Data.SqlClient" %> <SCRIPT runat="server"> private void Page_Load(object sender, System.EventArgs e) { if (!IsPostBack) LoadData(); } public void LoadData() { SqlDataAdapter adapter = new SqlDataAdapter("SELECT * FROM orders", "server=(local);database=northwind;Integrated Security=SSPI"); DataTable table = new DataTable(); adapter.Fill(table); Repeater1.DataSource = table; Repeater1.DataBind(); } </SCRIPT> <HTML> <HEAD> <title>ListControls #1</title> </HEAD> <body> <form id="Form1" method="post" runat="server"> <asp:Repeater runat="server" id="Repeater1"> <itemtemplate> <b><%# DataBinder.Eval(Container.DataItem, "orderid") %></b> <br>Due by: <%# DataBinder.Eval(Container.DataItem, "orderdate", "{0:d}") %>     Shipped: <%# DataBinder.Eval(Container.DataItem, "shippeddate", "{0:d}") %> </itemtemplate> <separatortemplate> <hr> </separatortemplate> </asp:Repeater> </form> </body> </HTML>

The page from Figure 1 works nicely but has at least two inherent drawbacks. One is that all the required template markup is incorporated in the ASPX source and is tightly coupled to the page. This is not an issue per se, but it will be when you want to reuse the code, and the solution based on it, elsewhere. In other words, using a handcrafted Repeater is hardly a repeatable operation without cut-and-paste techniques; therefore any contents based on the Repeater are not very reusable or extensible.

When you use the Repeater control to fill a page with templates, you actually build a solution based on two logical levels: the data repeater and the templates. The logic that repeats the templates and binds them to data is incorporated in the Repeater control; the template content is usually inline with the ASPX source. To reuse this solution on another page or in another application, the only viable approach is cutting and pasting the template content from page to page.

Before we look into practical solutions, an overview of the template properties in ASP.NET might be helpful.

Template Properties

Consider the ItemTemplate property of the Repeater control. According to the MSDN® documentation, the property gets or sets the template that defines how items in the control are displayed. The property is declared as follows:

public virtual ITemplate ItemTemplate {get; set;}

It is a read/write overridable property of type ITemplate. When processing a page, if the ASP.NET page parser finds an inline template, it creates an instance of a template builder and makes it return an ITemplate object. Figure 2 shows a sample of the code that the page code generator creates for the header of a Repeater.

Figure 2 Header of a Repeater Control

// 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; ... // Later in the dynamically generated class file control void __BuildControl__control3(Control __ctrl) { IParserAccessor __parser = (IParserAccessor) __ctrl; LiteralControl _header = new LiteralControl( "text of <headertemplate>"); __parser.AddParsedSubObject(_header); }

The CompiledTemplateBuilder class is listed in the ASP.NET documentation, but is described as supporting the Microsoft® .NET Framework and should not be used from your code directly. It inherits from the supported class TemplateBuilder, though. 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 code of Figure 2? It's simply the literal control that contains all the source code of the <HeaderTemplate> tag.

Okay, so there should be a way to create templates programmatically, but what's the fastest and easiest way? The simplest way is resorting to user controls.

User Controls

A user control is primarily a form made of any combination of server and client controls sewn together with server and client script code. A user control has a rich user interface and can expose an object model built on top of contained controls. In ASP.NET user controls derive from the UserControl class and do not implement ITemplate. So how can user controls provide a template?

The Page class inherits from its base TemplateControl class a method named LoadTemplate, which just obtains an instance of the ITemplate interface from an external ASCX user control file. You specify the virtual path to the ASCX file and the method does the rest. In particular, LoadTemplate first gets an absolute path to the ASCX control and then compiles the user control to a dynamic class. The obtained class is then wrapped by a built-in simple template class that doesn't do much more than implement ITemplate. Modify the code in Figure 1 to move the template definition to a separate ASCX file. Next, try the following code:

Repeater1.ItemTemplate = LoadTemplate("ucitemtemplate.ascx"); Repeater1.DataSource = table; Repeater1.DataBind();

You shouldn't notice anything different in the page's markup, but this time the item template is being generated from an external compiled resource. A little change is required to make the item template of Figure 1 work unchanged as a distinct ASCX component. The object of the change is the DataBinder expression:

DataBinder.Eval(Container.DataItem, "OrderId");

Container.DataItem returns the data item object bound to the Repeater's item being rendered. From within a Repeater instance, Container evaluates to a RepeaterItem object where DataItem is a valid property. When used from within a user control, though, Container is just a Control and the DataItem property doesn't get defined. There are two ways to solve this. One is casting Container to RepeaterItem; the other is moving the DataItem name up into the navigation path of the data binding expression:

DataBinder.Eval(Container, "DataItem.OrderId");

In this way, the same object is retrieved as expected, but DataItem is not treated like a string and is accessed through reflection. Both approaches lead you to the same result; the second provides a more reusable solution since you don't need to know what kind of iterative control is calling the user control.

Now add the following instruction immediately after assigning the ItemTemplate property:

Response.Write(Repeater1.ItemTemplate.ToString());

What do you expect the real type of ItemTemplate to be? UserControl? Or is it more likely to be a dynamic class name generated after the URL of the ASCX? The name of the template class, actually, is the following:

System.Web.UI.TemplateControl+SimpleTemplate

SimpleTemplate is an internal nested type:

public class TemplateControl { ... internal class SimpleTemplate : ITemplate { ... } }

SimpleTemplate is supplied with the Type of the user control. When instantiated within a host control, it simply creates an instance of the user control and adds it to the host site.

The solution based on user controls is sadly not problem-free. You need a new user control for each different template you want to generate dynamically. In many cases, you have a proliferation of relatively small and simple user controls that must be deployed individually and duplicated to reuse them in other projects.

Implementing the ITemplate Interface

As mentioned, the ITemplate interface is the only requirement for a template property. Both the classic inline template and the user control trick are mere workarounds that create an underlying object that implements ITemplate. If reusability is not an issue, line templates are a pretty good solution. If you want a bit of reusability and flexibility but don't see deployment as an issue, you can opt for user controls. In all other cases, you should read on and consider writing your own class that implements ITemplate.

The main benefit to you when implementing ITemplate is simplified deployment and full reusability of the Repeater-based solution. Let's see how to rewrite the previous example with ITemplate classes. But first, a brief overview of the interface is in order:

public interface ITemplate { void InstantiateIn(Control container); }

The interface has only one method—InstantiateIn—which returns void and takes a Control parameter. The control passed to InstantiateIn defines the Control object to which child controls in the template will be parented. These child controls can be defined within an inline template or a user control, or created programmatically within a new class.

The goal of InstantiateIn is quite simple. It has to create and add any needed control to the specified container control. If data binding is required on any of these controls, the DataBinding event must be hooked up too. The following class creates a header template that simply displays the word "Orders" in H1 style:

public class OrderHeaderTemplate : ITemplate { public void InstantiateIn(Control container) { string txt = "<h1>Orders</h1>"; LiteralControl h1 = new LiteralControl(txt); container.Controls.Add(h1); } }

It does so by using a literal control. Note that the header template is not data-bound, meaning that no data from the data source is visible at this point for binding. The following line shows you how to connect the previous type with the HeaderTemplate property of a Repeater:

Repeater1.HeaderTemplate = new OrderHeaderTemplate();

Defining the item template is a little bit more complicated because it is data-bound. Let's take a look at the example in Figure 3. The template to reproduce determines the order ID on a row, followed on the next row by the order and the shipment dates. A few static literals complete the template. The order ID is added as a Label. There's no reason to use a Label in lieu of a literal as far as pure text is concerned. Here I opted for a Label simply because it provides a Font property which makes it easy to display the order ID in bold type.

Figure 3 Sample Repeater Item Template Using a Custom Class

public class OrderItemTemplate : ITemplate { public void InstantiateIn(Control container) { Label header = new Label(); header.Font.Bold = true; header.DataBinding += new EventHandler(OrderHeader_DataBinding); container.Controls.Add(header); container.Controls.Add(new LiteralControl("<br>Due by:  ")); LiteralControl orderDate = new LiteralControl(); orderDate.DataBinding += new EventHandler(OrderDate_DataBinding); container.Controls.Add(orderDate); container.Controls.Add( new LiteralControl("   Shipped: ")); LiteralControl orderShipped = new LiteralControl(); orderShipped.DataBinding += new EventHandler(OrderShipped_DataBinding); container.Controls.Add(orderShipped); } private void OrderHeader_DataBinding(object sender, EventArgs e) { Label header = (Label) sender; RepeaterItem container = (RepeaterItem) header.NamingContainer; header.Text = DataBinder.GetPropertyValue( container.DataItem, "orderid").ToString(); } private void OrderDate_DataBinding(object sender, EventArgs e) { LiteralControl orderDate = (LiteralControl) sender; RepeaterItem container = (RepeaterItem) orderDate.NamingContainer; orderDate.Text = (string) DataBinder.GetPropertyValue( container.DataItem, "orderdate", "{0:d}"); } private void OrderShipped_DataBinding(object sender, EventArgs e) { LiteralControl orderShipped = (LiteralControl) sender; RepeaterItem container = (RepeaterItem) orderShipped.NamingContainer; orderShipped.Text = (string) DataBinder.GetPropertyValue( container.DataItem, "shippeddate", "{0:d}"); } }

In addition to the insertion of the control in the existing container's context, the key thing that is going on here is the DataBinding event handler. A DataBinding handler is needed for each control that would require DataBinder.Eval in the inline template. First, you save a reference to the sender through the sender parameter of the event handler. The sender is the control on behalf of which you're handling the DataBinding event. The NamingContainer property of the sender indicates the nearest control that implements INamingContainer. The nearest container for a child control generated within a Repeater's row is just the RepeaterItem object of the corresponding row. Getting this reference is essential, as it is the only way to access the bound data.

The RepeaterItem type has a property named DataItem. This property is DataRowView in the case of a bound DataView; it is String in the case of a bound string collection. To get the bound value for a particular field, use either DataBinder.Eval or DataBinder.GetPropertyValue. In general, Eval is more flexible as it can retrieve both individual and indexed properties. The Eval method has two public overloads, as shown here:

public static string Eval(object container, string expression) public static string Eval( object container, string expression, string format)

Figure 4 ASP.NET Repeater

Figure 4** ASP.NET Repeater **

While building a custom class for each template, you can define as many constructors as you need, add to them as many parameters as required, and in general customize the class as you like with extra methods, properties, and events. Figure 4 shows a sample page in action powered by custom classes that implement the ITemplate interface.

List Controls

The final option is list controls. List controls inherit from the abstract class ListControl and provide a data-bindable, fixed rendering mechanism. Using a list control is considerably easier than setting up an iterative control. Defining templates requires quite a bit of declarative code or, if accomplished programmatically, requires that you write a class that implements ITemplate.

A list control takes its data from the DataSource property; other properties are used to select particular columns of data for personalized rendering. For example, consider the DropDownList control. It renders out to HTML as follows:

<select> <option value="...">...</option> ... </select>

The value attribute is expected to be a unique value in the collection of items; the inner text of the <option> tag contains the displayed text for the item. These two elements can be bound to data columns through DataValueField and DataTextField, respectively. The list controls CheckBoxList and RadioButtonList provide more formatting options, such as the maximum number of columns or rows to show and the repeat direction. To write a custom list control, you simply need to extend the base ListControl class (to take advantage of the RepeatInfo class's default rendering, you can also implement IRepeatInfoUser). Let's take a closer look.

A list control is based on the idea of a repeated control. The repeatable control doesn't have to be a single, individual control; it can also be a tree of controls with a common root. Typically, you implement this control through a private property, ControlToRepeat. Figure 5 shows an example for a button list control.

Figure 5 Button List Control

private Button ControlToRepeat { get { if (_controlToRepeat == null) { _controlToRepeat = new Button(); _controlToRepeat.EnableViewState = false; Controls.Add(_controlToRepeat); } return _controlToRepeat; } }

In addition, you expose a few public properties to let users control the layout of the control: RepeatDirection, RepeatColumns, and RepeatLayout. These properties have the same meaning as they do in DataList or CheckBoxList controls.

The Render method of the control takes into account the control's repeating capabilities, as shown in Figure 6. The Render method builds an instance of the RepeatInfo class and passes it a reference to the control itself. The method RenderRepeater of the RepeatInfo structure ends up calling the members of the IRepeatInfoUser interface of the control and finally provides the expected behavior. Figure 7 shows the sample ButtonList control in action in the Visual Studio® .NET designer.

Figure 7 ButtonList in the Visual Studio .NET Designer

Figure 6 IRepeatInfoUser

public bool HasHeader { get { return false; } } public bool HasSeparators { get { return false; } } public bool HasFooter { get { return false; } } public Style GetItemStyle(System.Web.UI.WebControls.ListItemType itemType, int repeatIndex) { return null; } public void RenderItem(System.Web.UI.WebControls.ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer) { ButtonItem item = Items[repeatIndex]; Button btn = ControlToRepeat; btn.Text = item.Text; btn.CommandName = item.CommandName; btn.RenderControl(writer); } public int RepeatedItemCount { get { return this.Items.Count; } } public virtual RepeatDirection RepeatDirection { get { object o = ViewState["RepeatDirection"]; if (o != null) return (RepeatDirection)o; return RepeatDirection.Vertical; } set { ViewState["RepeatDirection"] = value; } } public virtual RepeatLayout RepeatLayout { get { object o = ViewState["RepeatLayout"]; if (o != null) return (RepeatLayout)o; return RepeatLayout.Table; } set { ViewState["RepeatLayout"] = value; } } public virtual int RepeatColumns { get { object o = ViewState["RepeatColumns"]; if (o != null) return (int)o; return 0; } set { ViewState["RepeatColumns"] = value; } } protected override void Render(HtmlTextWriter writer) { if (Items.Count >0) { RepeatInfo ri = new RepeatInfo(); Style controlStyle = (base.ControlStyleCreated ? base.ControlStyle : null); ri.RepeatColumns = RepeatColumns; ri.RepeatDirection = RepeatDirection; ri.RepeatLayout = RepeatLayout; ri.RenderRepeater(writer, this, controlStyle, this); } } protected override Style CreateControlStyle() { return new TableStyle(this.ViewState); }

ItemCreated Event

When a RepeatItem is created in the Repeater control, the ItemCreated event fires. The event simply signals the creation of the element; it says nothing about the data associated with the element. Data binding the item, in fact, happens through the ItemDataBound event. ItemCreated and ItemDataBound are extremely useful for modifying the control style dynamically to, say, alert users that values are at a critical threshold. Default list controls, though, have no such events. If you ever tried to make one particular checkbox in a CheckBoxList collection bold or italic, you probably know what I mean.

To finish with a flourish, let's see how to add a ButtonCreated event to the ButtonList control. First, declare a custom delegate:

public delegate void ButtonCreatedHandler( object sender, ButtonCreatedEventArgs e);

Next, create the data structure to hold event data:

public class ButtonCreatedEventArgs : EventArgs { public Button ControlToRepeat; public int ButtonIndex; }

You pass the event handler the index of the button being created and a reference to the button itself. This way, users can make last-minute changes quite easily. The event is declared as follows:

public event ButtonCreatedHandler ButtonCreated;

Where do you fire the event? If you look at the behavior of the Repeater control, the ideal launch time is when each button is created. Unlike Repeater and other iterative controls, though, which create a hierarchy of child controls before rendering, list controls create their own children directly at rendering time. In other words, list controls don't really have distinct moments for the creation, data binding, and rendering of the child controls. Suppose you catch the ButtonCreated event in an ASP.NET page and then write the following code:

void ButtonList1_ButtonCreated( object sender, Samples.ButtonCreatedEventArgs e) { if (e.ControlToRepeat.CommandName == "Test") e.ControlToRepeat.BackColor = Color.Yellow; }

All buttons have a yellow background, not just the one with the given command name. What's up? The trick is the ControlToRepeat private property that I talked about a bit earlier. That property represents the main piece of the list control's user interface. For better performance, a single instance of the control is used—the one stored in the ControlToRepeat property. When it comes to implementing the ButtonCreated event, though, that apparently positive feature turns into a negative. Because all buttons are rendered through the same Button object, any changes made at a certain point in the flow propagate to all subsequent buttons in the list. To avoid that, you need to handle buttons separately (see Figure 8). To see the difference, compare the implementations of the RenderItem method in Figure 5 and Figure 8.

Figure 8 Firing the ItemCreated Event from a List Control

public void RenderItem( ListItemType itemType, int repeatIndex, RepeatInfo repeatInfo, HtmlTextWriter writer) { // Prepare the button to render—new button ButtonItem item = Items[repeatIndex]; Button btn = new Button(); btn.Text = item.Text; btn.CommandName = item.CommandName; // Fire the ItemCreated event if (ItemCreated != null) { ButtonCreatedEventArgs bcea = new ButtonCreatedEventArgs(); bcea.ItemIndex = repeatIndex; bcea.Item = Items[repeatIndex]; bcea.ControlToRepeat = btn; ItemCreated(this, bcea); } // Render the control btn.RenderControl(writer); }

When to Use Which

There are many ways to repeat controls and templates in ASP.NET pages. If you want to be able to switch templates at run time or deploy using different views of the same content, then user controls are just perfect. For a quick and page-specific solution, nothing is better than inline code. You should use ITemplate classes or list controls when you have a custom but fixed pattern to repeat for each bound record and want this to be reusable in the full sense of the word—as if it were a true custom control. Whether an ITemplate class or a custom list control will work better for you depends on other factors. For example, how important are a control's layout capabilities? Would you need to let users choose a horizontal or vertical view? Or a maximum number of rows or columns? If you answered yes to any of these quick questions, a custom list control should be your first choice.

The RepeatInfo class provides layout capabilities to IRepeatInfoUser implementations, but the Repeater doesn't use it. On the other hand, implementing the interface is much easier in a list control where you control the whole binding mechanism. (This could also be done in a control that inherits from the Repeater class, but is a task for brave hearts only.) List controls are powerful, but often overlooked. In ASP.NET 2.0, building list controls is even easier than in ASP.NET 1.x.

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

Dino Esposito is a Wintellect instructor and consultant based in Italy. Author of Programming ASP.NET and the new book Introducing ASP.NET 2.0 (both from Microsoft Press), he spends most of his time teaching classes on ASP.NET and ADO.NET and speaking at conferences. Get in touch with Dino at cutting@microsoft.com or join the blog at weblogs.asp.net/despos.