Creating Templated Controls
A template
enables you to customize the layout of a control. Furthermore, a
template can contain expressions that are not evaluated until runtime.
The ASP.NET Framework supports two types of templates. First, you can
create a one-way databinding template. You use a one-way databinding
template to display data items. In a one-way databinding template, you
use the Eval() expression to display the value of a data item.
Second, you
have the option of creating a two-way databinding template. A two-way
databinding template can be used not only to display data items, but
also to update data items. You can use the Bind() expression in a
two-way databinding template to both display a data item and extract the
value of a data item. Typically, you use templates with a databound
control. For example, the ListView, GridView, Repeater, DataList,
FormView, and DetailsView controls all support an ItemTemplate that
enables you to format the data items that these controls display.
Implementing the ITemplate Interface
You
create a one-way databinding template by adding a property to a control
that returns an object that implements the ITemplate interface. The
ITemplate interface includes one method:
InstantiateIn—Instantiates the contents of a template in a particular control.
You are not
required to implement the InstantiateIn() method yourself. The ASP.NET
Framework creates the method for you automatically. You call the
InstantiateIn method in your control to add the contents of a template
to your control. For example, the control in Listing represents an
article. The Article control includes a template named ItemTemplate. The
ItemTemplate is used to lay out the elements of the article: the title,
author, and contents.
LISTING Article.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{
public class Article : CompositeControl
{
private string _title;
private string _author;
private string _contents;
private ITemplate _itemTemplate;
public string Title
{
get { return _title; }
set { _title = value; }
}
public string Author
{
get { return _author; }
set { _author = value; }
}
public string Contents
{
get { return _contents; }
set { _contents = value; }
}
[TemplateContainer(typeof(Article))]
[PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate ItemTemplate
{
get { return _itemTemplate; }
set { _itemTemplate = value; }
}
protected override void CreateChildControls()
{
_itemTemplate.InstantiateIn(this);
}
}
}
Notice that
the Article control contains a property named ItemTemplate that returns
an object that implements the ITemplate interface. Notice that this
property is decorated with two attributes: a TemplateContainer and a
PersistenceMode attribute. The TemplateContainer attribute is used to
specify the type of control that will contain the template. In the case
of the Article control, the template will be contained in the Article
control itself. Therefore, the Article control’s type is passed to the
TemplateContainer attribute.
The
PersistenceMode attribute indicates how a property is persisted in an
ASP.NET page. The possible values are Attribute,
EncodedInnerDefaultProperty, InnerDefaultProperty, and InnerProperty. We
want to declare the ItemTemplate like this:
<custom:Article
runat=”server”>
<ItemTemplate>
... template contents ...
</ItemTemplate>
</custom:Article>
Because we
want to declare the ItemTemplate inside the Article control, the
PersistenceMode attribute needs to be set to the value InnerProperty.
The Article control overrides the base WebControl class’s
CreateChildControls() method. The ItemTemplate is added as a child
control to the Article control. Any
controls
contained in the template become child controls of the current control.
The page in Listing illustrates how you can use the Article control and
its ItemTemplate.
LISTING ShowArticle.aspx
<%@ Page Language=”C#” %>
<%@ Register TagPrefix=”custom” Namespace=”myControls” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<script runat=”server”>
void Page_Load()
{
Article1.Title = “Creating Templated Databound Controls”;
Article1.Author = “Stephen Walther”;
Article1.Contents = “Blah, blah, blah, blah...”;
Article1.DataBind();
}
</script>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Show Article</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<custom:Article id=”Article1” Runat=”server”>
<ItemTemplate>
<h1><%# Container.Title %></h1>
<em>By <%# Container.Author %></em>
<br /><br />
<%# Container.Contents %>
</ItemTemplate>
</custom:Article>
</div>
</form>
</body>
</html>
In the
Page_Load() method, the Title, Author, and Contents properties of the
article are set. Notice that these properties are used within
databinding expressions within the Article control’s ItemTemplate. For
example, the value of the Title property is displayed with the following
databinding expression:
<%# Container.Title %>
The
Container keyword refers to the current binding container. In this case,
the binding container is the Article control itself. Therefore, you can
refer to any property of the Article control by using the Container
keyword. Notice that the Article control’s DataBind() method is called
at the end of the Page_Load() method. Don’t forget to call this method
when you include databinding expressions in a template. If you don’t
call this method, then the databinding expressions are never evaluated
and displayed.
Creating a Default Template
Normally,
you don’t implement the InstantiateIn() method; you let the ASP.NET
Framework do it for you. However, if you want to supply a control with a
default template, then you need to implement this method. The modified
Article control in Listing includes a default template for the
ItemTemplate. The default template is used when an ItemTemplate is not
supplied.
LISTING ArticleWithDefault.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace myControls
{
public class ArticleWithDefault : CompositeControl
{
private string _title;
private string _author;
private string _contents;
private ITemplate _itemTemplate;
public string Title
{
Creating Templated Controls 1575
30
get { return _title; }
set { _title = value; }
}
public string Author
{
get { return _author; }
set { _author = value; }
}
public string Contents
{
get { return _contents; }
set { _contents = value; }
}
[TemplateContainer(typeof(ArticleWithDefault))]
[PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate ItemTemplate
{
get { return _itemTemplate; }
set { _itemTemplate = value; }
}
protected override void CreateChildControls()
{
if (_itemTemplate == null)
_itemTemplate = new ArticleDefaultTemplate();
_itemTemplate.InstantiateIn(this);
}
}
public class ArticleDefaultTemplate : ITemplate
{
public void InstantiateIn(Control container)
{
Label lblTitle = new Label();
lblTitle.DataBinding += new EventHandler(lblTitle_DataBinding);
Label lblAuthor = new Label();
lblAuthor.DataBinding += new EventHandler(lblAuthor_DataBinding);
Label lblContents = new Label();
lblContents.DataBinding += new EventHandler(lblContents_DataBinding);
container.Controls.Add(lblTitle);
container.Controls.Add(new LiteralControl(“<br />”));
container.Controls.Add(lblAuthor);
container.Controls.Add(new LiteralControl(“<br />”));
container.Controls.Add(lblContents);
}
void lblTitle_DataBinding(object sender, EventArgs e)
{
Label lblTitle = (Label)sender;
ArticleWithDefault container = (ArticleWithDefault)lblTitle.
➥NamingContainer;
lblTitle.Text = container.Title;
}
void lblAuthor_DataBinding(object sender, EventArgs e)
{
Label lblAuthor = (Label)sender;
ArticleWithDefault container = (ArticleWithDefault)lblAuthor.
➥NamingContainer;
lblAuthor.Text = container.Author;
}
void lblContents_DataBinding(object sender, EventArgs e)
{
Label lblContents = (Label)sender;
ArticleWithDefault container = (ArticleWithDefault)lblContents.
➥NamingContainer;
lblContents.Text = container.Contents;
}
}
}
Supporting Simplified Databinding
The
databinding expressions used in the previous two sections might seem a
little odd. For example, we used the following databinding expression to
refer to the Title property:
<%# Container.Title %>
When you
use a databinding expression with one of the standard ASP.NET controls,
such as the GridView control, you typically use a databinding expression
that looks like this:
<%# Eval(“Title”) %>
Why the
difference? The standard ASP.NET controls support a simplified
databinding syntax. If you want to support this simplified syntax in
your custom controls, then you must implement the IDataItemContainer
interface. The IDataItemContainer includes the following three
properties, which you are required to implement:
DataItem—Returns the value of the data item.
DataItemIndex—Returns the index of the data item from its data source.
DisplayIndex—Returns the index of the data item as it is displayed in a control.
Typically,
you implement the IDataItemContainer when creating a databound control.
For example, you wrap up each record retrieved from a database table in
an object that implements the IDataItemContainer interface. That way,
you can use a simplified databinding expression to refer to the value of
a particular database record column.
Supporting Two-Way Databinding
Two-way
databinding is a feature that was introduced with the ASP.NET 2.0
Framework. Two-way databinding enables you to extract values from a
template. You can use a twoway databinding expression not only to
display the value of a data item, but also to update the value of a data
item. You create a template that supports two-way databinding
expressions by creating a property that returns an object that
implements the IBindableTemplate interface. This interface inherits from
the ITemplate interface. It has the following two methods:
InstantiateIn—Instantiates the contents of a template in a particular control.
ExtractValues—Returns a collection of databinding expression values from a template.
For
example, the ProductForm control in Listing represents a form for
editing an existing product. The control includes a property named
EditItemTemplate that represents a two-way databinding template.
LISTING ProductForm.cs
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Collections.Specialized;
namespace myControls
{
public class ProductForm : CompositeControl
{
public event EventHandler ProductUpdated;
private IBindableTemplate _editItemTemplate;
private ProductFormItem _item;
private IOrderedDictionary _results;
public IOrderedDictionary Results
{
get { return _results; }
}
public string Name
{
get
{
EnsureChildControls();
return _item.Name;
}
set
{
EnsureChildControls();
_item.Name = value;
}
}
public decimal Price
{
get
{
EnsureChildControls();
return _item.Price;
}
set
{
EnsureChildControls();
_item.Price = value;
}
}
[TemplateContainer(typeof(ProductFormItem), BindingDirection.TwoWay)]
[PersistenceMode(PersistenceMode.InnerProperty)]
public IBindableTemplate EditItemTemplate
{
get { return _editItemTemplate; }
set { _editItemTemplate = value; }
}
protected override void CreateChildControls()
{
_item = new ProductFormItem();
_editItemTemplate.InstantiateIn(_item);
Controls.Add(_item);
}
protected override bool OnBubbleEvent(object source, EventArgs args)
{
_results = _editItemTemplate.ExtractValues(_item);
if (ProductUpdated != null)
ProductUpdated(this, EventArgs.Empty);
return true;
}
}
public class ProductFormItem : WebControl, IDataItemContainer
{
private string _name;
private decimal _price;
public string Name
{
get { return _name; }
set { _name = value; }
}
public decimal Price
{
get { return _price; }
set { _price = value; }
}
public object DataItem
{
get { return this; }
}
public int DataItemIndex
{
get { return 0; }
}
public int DisplayIndex
{
get { return 0; }
}
}
}
You should
notice two special things about the EditItemTemplate property. First,
notice that the property returns an object that implements the
IBindableTemplate interface. Second, notice that the TemplateContainer
attribute that decorates the property includes a BindingDirection
parameter. You can assign one of two possible values to
BindingDirection: OneWay and TwoWay. The ProductForm includes an
OnBubbleEvent() method. This method is called when a child control of
the ProductForm control raises an event. For example, if someone clicks a
Button control contained in the EditItemTemplate, the OnBubbleEvent()
method is called. In Listing, the OnBubbleEvent() method calls the
EditItemTemplate’s ExtractValues() method. This method is supplied by
the ASP.NET Framework because the EditItemTemplate is marked as a
two-way databinding template. The ExtractValues() method returns an
OrderedDictionary collection that contains name/value pairs that
correspond to each of the databinding expressions contained in the
EditItemTemplate. The ProductForm control exposes this collection of
values with its Results property. After the values are extracted, the
control raises a ProductUpdated event.
The page in Listing illustrates how you can use the ProductForm control to update the properties of a product.
LISTING ShowProductForm.aspx
<%@ Page Language=”C#” %>
<%@ Register TagPrefix=”custom” Namespace=”myControls” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<script runat=”server”>
void Page_Load()
{
if (!Page.IsPostBack)
{
ProductForm1.Name = “Laptop”;
ProductForm1.Price = 433.12m;
ProductForm1.DataBind();
}
}
protected void ProductForm1_ProductUpdated(object sender, EventArgs e)
{
lblName.Text = ProductForm1.Results[“Name”].ToString();
lblPrice.Text = ProductForm1.Results[“Price”].ToString();
}
</script>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<title>Show ProductForm</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<custom:ProductForm id=”ProductForm1”
Runat=”server” OnProductUpdated=”ProductForm1_ProductUpdated”>
<EditItemTemplate>
<asp:Label id=”lblName” Text=”Product Name:” AssociatedControlID=”txtName” Runat=”server” />
<asp:TextBox id=”txtName” Text=’<%# Bind(“Name”) %>’ Runat=”server” />
<br /><br />
<asp:Label id=”lblPrice” Text=”Product Price:” AssociatedControlID=”txtPrice” Runat=”server” />
<asp:TextBox id=”txtPrice” Text=’<%# Bind(“Price”) %>’ Runat=”server” />
<br /><br />
<asp:Button id=”btnUpdate” Text=”Update” Runat=”server” />
</EditItemTemplate>
</custom:ProductForm>
<hr />
New Product Name:
<asp:Label id=”lblName” Runat=”server” />
<br /><br />
New Product Price:
<asp:Label id=”lblPrice” Runat=”server” />
</div>
</form>
</body>
</html>
In the
Page_Load() method in Listing, the ProductForm Name and Price properties
are set. Next, the DataBind() is called in order to cause the
ProductForm control to evaluate its databinding expressions. Notice that
the ProductForm control’s EditItemTemplate includes Bind() expressions
instead of Eval() expressions. You use Bind() expressions in a two-way
databinding template.
The
EditItemTemplate includes a Button control. When you click the Button
control, the ProductForm control’s OnBubbleEvent() method executes, the
values are retrieved from the EditItemTemplate, and the ProductUpdated
event is raised. The page in Listing handles the ProductUpdated event
and displays the new values with two Label controls
Creating Templated Databound Controls
A
databound control can be bound to a DataSource control such as the
SqlDataSource or ObjectDataSource controls. The ASP.NET Framework
provides you with a number of base classes that you can use when
creating a custom databound control. So, let’s look at some tables and
figures. Typically, you inherit from one of the leaf nodes. You create a
control that derives from the base CompositeDataBoundControl,
HierarchicalDataBoundControl, or ListControl class. This is the easiest
base class to use when you want to display one or more database records
and use templates.
Creating a DivView Control
Let’s
start simple. In this section, we create a custom databound control
named the DivView control. The DivView control displays a set of data
items (database records) in HTML <div> tags. The DivView control
inherits from the base CompositeDataBoundControl class and overrides a
single method of the base class. The DivView control overrides the base
class’s CreateChildControls() method. The DivView control is contained
in Listing.
LISTING DivView.cs
using System;
using System.Collections;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace AspNetUnleashed
{
public class DivView : CompositeDataBoundControl
{
private ITemplate _itemTemplate;
[TemplateContainer(typeof(DivViewItem))]
[PersistenceMode(PersistenceMode.InnerProperty)]
public ITemplate ItemTemplate
{
get { return _itemTemplate; }
set { _itemTemplate = value; }
}
protected override int CreateChildControls(IEnumerable dataSource, bool
➥dataBinding)
{
int counter = 0;
foreach (object dataItem in dataSource)
{
DivViewItem contentItem = new DivViewItem(dataItem, counter);
_itemTemplate.InstantiateIn(contentItem);
Controls.Add(contentItem);
counter++;
}
DataBind(false);
return counter;
}
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
}
public class DivViewItem : WebControl, IDataItemContainer
{
private object _dataItem;
private int _index;
public object DataItem
{
get { return _dataItem; }
}
public int DataItemIndex
{
get { return _index; }
}
public int DisplayIndex
{
get { return _index; }
}
protected override HtmlTextWriterTag TagKey
{
get
{
return HtmlTextWriterTag.Div;
}
}
public DivViewItem(object dataItem, int index)
{
_dataItem = dataItem;
_index = index;
}
}
}
The DivView
control supports an ItemTemplate that is used to format each of its
data items. You are required to supply an ItemTemplate when you use the
DivView control. All the work happens in the CreateChildControls()
method. Notice that this is not the same CreateChildControls() method
that is included in the base System.Web.UI.Control class. The DivView
control overrides the CompositeDataBounControl’s CreateChildControls()
method. The CreateChildControls() method accepts the following two
parameters:
dataSource—Represents all the data items from the data source.
dataBinding—Represents
whether the CreateChildControls() method is called when the data items
are being retrieved from the data source.
The
CreateChildControls() method is called every time that the DivView
control renders its data items. When the control is first bound to a
DataSource control, the dataSource parameter represents the data items
retrieved from the DataSource control. After a postback, the dataSource
parameter contains a collection of null values, but the correct number
of null values.
After a
postback, the contents of the data items can be retrieved from View
State. As long as the correct number of child controls is created, the
Framework can rebuild the contents of the databound control. You can use
the dataBinding parameter to determine whether the data items from the
data source actually represent anything. Typically, the dataBinding
parameter has the value True when the page first loads and the value
False after each postback. Notice that the DataBind() method is called
after the child controls are created. You must call the DataBind()
method when a template includes databinding expressions. Otherwise, the
databinding expressions are never evaluated. The page in Listing
illustrates how you can bind the DivView control to a SqlDataSource
control.
LISTING ShowDivView.aspx
<%@ Page Language=”C#” %>
<%@ Register TagPrefix=”custom” Namespace=”AspNetUnleashed” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
“http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” >
<head id=”Head1” runat=”server”>
<style type=”text/css”>
.movies
{
width:500px;
}
.movies div
{
border:solid 1px black;
padding:10px;
margin:10px;
}
</style>
<title>Show DivView</title>
</head>
<body>
<form id=”form1” runat=”server”>
<div>
<custom:DivView id=”lstMovies” DataSourceID=”srcMovies” CssClass=”movies” Runat=”Server”>
<ItemTemplate>
<h1><%# Eval(“Title”) %></h1>
Director: <%# Eval(“Director”) %>
</ItemTemplate>
</custom:DivView>
<asp:SqlDataSource id=”srcMovies” ConnectionString=”<%$ ConnectionStrings:Movies %>”
SelectCommand=”SELECT Title, Director FROM Movies” Runat=”server” />
<br />
<asp:LinkButton id=”lnkReload” Text=”Reload” Runat=”server” />
</div>
</form>
</body>
</html>
In Listing
the SqlDataSource control represents the Movies database table. The
DivView control includes an ItemTemplate that formats each of the
columns from this database table.