Chapter 33. Building Templated Databound Controls

<feature><title>In this Chapter</title> <objective>

Creating Templated Controls

</objective>
<objective>

Creating Templated Databound Controls

</objective>
<objective>

Summary

</objective>
</feature>

The ASP.NET Framework is a framework. If you don’t like anything about the framework, you always have the option of extending it. In particular, if you discover that the standard databound controls in the framework don’t do everything you need, you can create a custom databound control.

In this chapter, you learn how to create custom controls that work like the ASP.NET 2.0 GridView, DetailsView, and FormView controls. In the first part of this chapter, you learn how to create controls that support templates. You learn how to implement controls that support both standard templates and two-way databinding templates. You also learn how to supply a control with a default template.

The last part of this chapter is devoted to the topic of databound controls. You learn about the new base control classes included in the framework that were supplied to make it easier to create custom databound controls. We also create several custom databound controls. For example, at the end of this chapter, we create a custom FormView control that uses AJAX to insert and update database records from the client.

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 2.0 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 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 GridView, Repeater, DataList, FormView, and DetailsView controls all support an ItemTemplate that enables you to format the data items that these controls display. However, you can use a template even when you are not displaying a set of data items. For example, the Login control supports a LayoutTemplate that enables you to customize the appearance of the Login form.

This part of this chapter concentrates on creating non-databound controls that support templates. In the next part of this chapter, you learn how to use templates with databound controls.

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 33.1 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.

Example 33.1. Article.vb

Imports System
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace myControls

    Public Class Article
        Inherits CompositeControl

        Private _title As String
        Private _author As String
        Private _contents As String

        Private _itemTemplate As ITemplate
        Public Property Title() As String
            Get
                Return _title
            End Get
            Set(ByVal Value As String)
                _title = value
            End Set
        End Property

        Public Property Author() As String
            Get
                Return _author
            End Get
            Set(ByVal Value As String)
                _author = value
            End Set
        End Property

        Public Property Contents() As String
            Get
                Return _contents
            End Get
            Set(ByVal Value As String)
                _contents = value
            End Set
        End Property

        <TemplateContainer(GetType(Article))> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property ItemTemplate() As ITemplate
            Get
                Return _itemTemplate
            End Get
            Set(ByVal Value As ITemplate)
                _itemTemplate = Value
            End Set
        End Property

        Protected Overrides Sub CreateChildControls()
            _itemTemplate.InstantiateIn(Me)
        End Sub
    End Class

End Namespace

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 33.2 illustrates how you can use the Article control and its ItemTemplate.

Example 33.2. ShowArticle.aspx

<%@ Page Language="VB" %>
<%@ 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">

    Sub Page_Load()
        Article1.Title = "Creating Templated Databound Controls"
        Article1.Author = "Stephen Walther"
        Article1.Contents = "Blah, blah, blah, blah..."
        Article1.DataBind()
    End Sub

</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>

When you open the page in Listing 33.2, the contents of the ItemTemplate are displayed (see Figure 33.1).

Using a template to display an article.

Figure 33.1. Using a template to display an article.

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

The previous section discussed the ITemplate interface’s InstantiateIn() method. 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 33.3 includes a default template for the ItemTemplate. The default template is used when an ItemTemplate is not supplied.

Example 33.3. ArticleWithDefault.vb

Imports System
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace myControls

    Public Class ArticleWithDefault
        Inherits CompositeControl

        Private _title As String
        Private _author As String
        Private _contents As String

        Private _itemTemplate As ITemplate

        Public Property Title() As String
            Get
                Return _title
            End Get
            Set(ByVal Value As String)
                _title = value
            End Set
        End Property

        Public Property Author() As String
            Get
                Return _author
            End Get
            Set(ByVal Value As String)
                _author = value
            End Set
        End Property

        Public Property Contents() As String
            Get
                Return _contents
            End Get
            Set(ByVal Value As String)
                _contents = value
            End Set
        End Property

        <TemplateContainer(GetType(ArticleWithDefault))> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property ItemTemplate() As ITemplate
            Get
                Return _itemTemplate
            End Get
            Set(ByVal Value As ITemplate)
                _itemTemplate = Value
            End Set
        End Property

        Protected Overrides Sub CreateChildControls()
            If _itemTemplate Is Nothing Then
                _itemTemplate = New ArticleDefaultTemplate()
            End If
            _itemTemplate.InstantiateIn(Me)
        End Sub
    End Class

    Public Class ArticleDefaultTemplate
        Implements ITemplate
        Public Sub InstantiateIn(ByVal container As Control) Implements ITemplate.InstantiateIn
            Dim lblTitle As New Label()
            AddHandler lblTitle.DataBinding, AddressOf lblTitle_DataBinding

            Dim lblAuthor As New Label()
            AddHandler lblAuthor.DataBinding, AddressOf lblAuthor_DataBinding

            Dim lblContents As New Label()
            AddHandler lblContents.DataBinding, AddressOf 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)
        End Sub

        Private Sub lblTitle_DataBinding(ByVal sender As Object, ByVal e As EventArgs)
            Dim lblTitle As Label = CType(sender, Label)
            Dim container As ArticleWithDefault = CType(lblTitle.NamingContainer, ArticleWithDefault)
            lblTitle.Text = container.Title
        End Sub

        Private Sub lblAuthor_DataBinding(ByVal sender As Object, ByVal e As EventArgs)
            Dim lblAuthor As Label = CType(sender, Label)
            Dim container As ArticleWithDefault = CType(lblAuthor.NamingContainer, ArticleWithDefault)
            lblAuthor.Text = container.Author
        End Sub

        Private Sub lblContents_DataBinding(ByVal sender As Object, ByVal e As EventArgs)
            Dim lblContents As Label = CType(sender, Label)
            Dim container As ArticleWithDefault = CType(lblContents.NamingContainer, ArticleWithDefault)
            lblContents.Text = container.Contents
        End Sub

    End Class

End Namespace

The control in Listing 33.3 is very similar to the control created in the previous section. However, notice that the CreateChildControls() method has been modified. The new version of the CreateChildControls() method tests whether there is an ItemTemplate. If there is no ItemTemplate, an instance of the ArticleDefaultTemplate class is created.

The ArticleDefaultTemplate class, which is also included in Listing 33.3, implements the ITemplate interface. In particular, the class implements the InstantiateIn() method. The instantiateIn() method creates all the controls that will appear in the template.

In Listing 33.3, three Label controls are created that correspond to the Title, Author, and Contents properties. Notice that the DataBinding event is handled for all three of these Label controls. When the DataBind() method is called, the DataBinding event is raised for each child control in the Article control. At that time, the values of the Title, Author, and Contents properties are assigned to the Text properties of the Label controls.

The page in Listing 33.4 illustrates how you can use the modified Article control.

Example 33.4. ShowArticleWithDefault.aspx

<%@ Page Language="VB" %>
<%@ 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">

    Sub Page_Load()
        ArticleWithDefault1.Title = "Creating Templated Databound Controls"
        ArticleWithDefault1.Author = "Stephen Walther"
        ArticleWithDefault1.Contents = "Blah, blah, blah, blah..."
        ArticleWithDefault1.DataBind()
    End Sub

</script>
<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Show Article with Default Template</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    <custom:ArticleWithDefault
        id="ArticleWithDefault1"
        Runat="server" />

    </div>
    </form>
</body>
 </html>

The ArticleWithDefault control in Listing 33.4 does not include an ItemTemplate. When the page is displayed in a browser, the contents of the ItemTemplate are supplied by the ArticleDefaultTemplate class (see Figure 33.2).

Displaying an article with a default template.

Figure 33.2. Displaying an article with a default template.

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.

In this section, we create a non-databound control that supports the simplified databinding syntax. The control is named the Product control, and it is included in Listing 33.5.

Example 33.5. Product.vb

Imports System
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace myControls

    Public Class Product
        Inherits CompositeControl

        Private _itemTemplate As ITemplate
        Private _item As ProductItem

        Public Property Name() As String
            Get
                EnsureChildControls()
                Return _item.Name
            End Get
            Set(ByVal Value As String)
                EnsureChildControls()
                _item.Name = Value
            End Set
        End Property

        Public Property Price() As Decimal
            Get
                EnsureChildControls()
                Return _item.Price
            End Get
            Set(ByVal Value As Decimal)
                EnsureChildControls()
                _item.Price = Value
            End Set
        End Property

        <TemplateContainer(GetType(ProductItem))> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property ItemTemplate() As ITemplate
            Get
                Return _itemTemplate
            End Get
            Set(ByVal Value As ITemplate)
                _itemTemplate = Value
            End Set
        End Property

        Protected Overrides Sub CreateChildControls()
            _item = New ProductItem()
            _itemTemplate.InstantiateIn(_item)
            Controls.Add(_item)
        End Sub
    End Class

    Public Class ProductItem
        Inherits WebControl
        Implements IDataItemContainer

        Private _name As String
        Private _price As Decimal

        Public Property Name() As String
            Get
                Return _name
            End Get
            Set(ByVal Value As String)
                _name = Value
            End Set
        End Property

        Public Property Price() As Decimal
            Get
                Return _price
            End Get
            Set(ByVal Value As Decimal)
                _price = Value
            End Set
        End Property
        Public ReadOnly Property DataItem() As Object Implements IDataItemContainer.DataItem
            Get
                Return Me
            End Get
        End Property

        Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
            Get
                Return 0
            End Get
        End Property

        Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex
            Get
                Return 0
            End Get
        End Property
    End Class

End Namespace

The file in Listing 33.5 actually contains two classes: the Product and the ProductItem class. The Product control includes an ItemTemplate property. Notice that the TemplateContainer attribute that decorates this property associates the ProductItem class with the ItemTemplate.

In the CreateChildControls() method, the ItemTemplate is instantiated into the ProductItem class. The ProductItem class, in turn, is added to the controls collection of the Product class.

The ProductItem class implements the IDataItemContainer interface. Implementing the DataItemIndex and DisplayIndex properties is a little silly because there is only one data item. However, you are required to implement all the properties of an interface.

The page in Listing 33.6 illustrates how you can use the Product control with the simplified databinding syntax.

Example 33.6. ShowProduct.aspx

<%@ Page Language="VB" %>
<%@ 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">

    Sub Page_Load()
        Product1.Name = "Laptop Computer"
        Product1.Price = 1254.12
        Product1.DataBind()
    End Sub

</script>

<html xmlns="http://www.w3.org/1999/xhtml" >
<head id="Head1" runat="server">
    <title>Show Product</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>

    <custom:Product
        id="Product1"
        Runat="Server">
        <ItemTemplate>
        Name: <%# Eval("Name") %>
        <br />
        Price: <%# Eval("Price", "{0:c}") %>
        </ItemTemplate>
    </custom:Product>

    </div>
    </form>
</body>
 </html>

Notice that the Eval() method is used in the Product control’s ItemTemplate. For example, the expression Eval("Name") is used to display the product name. If you prefer, you can still use the Container.Name syntax. However, the Eval() syntax is more familiar to ASP.NET developers.

Supporting Two-Way Databinding

Two-way databinding is a new feature of the ASP.NET 2.0 Framework. Two-way databinding enables you to extract values from a template. You can use a two-way databinding expression not only to display the value of a data item, but 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 33.7 represents a form for editing an existing product. The control includes a property named EditItemTemplate that represents a two-way databinding template.

Example 33.7. ProductForm.vb

Imports System
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls
Imports System.ComponentModel
Imports System.Collections.Specialized

Namespace myControls

    Public Class ProductForm
        Inherits CompositeControl

        Public Event ProductUpdated As EventHandler

        Private _editItemTemplate As IBindableTemplate
        Private _item As ProductFormItem
        Private _results As IOrderedDictionary

        Public ReadOnly Property Results() As IOrderedDictionary
            Get
                Return _results
            End Get
        End Property

        Public Property Name() As String
            Get
                EnsureChildControls()
                Return _item.Name
            End Get
            Set(ByVal Value As String)
                EnsureChildControls()
                _item.Name = Value
            End Set
        End Property

        Public Property Price() As Decimal
            Get
                EnsureChildControls()
                Return _item.Price
            End Get
            Set(ByVal Value As Decimal)
                EnsureChildControls()
                _item.Price = Value
            End Set
        End Property

        <TemplateContainer(GetType(ProductFormItem), BindingDirection.TwoWay)> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property EditItemTemplate() As IBindableTemplate
            Get
                Return _editItemTemplate
            End Get
            Set(ByVal Value As IBindableTemplate)
                _editItemTemplate = Value
            End Set
        End Property

        Protected Overrides Sub CreateChildControls()
            _item = New ProductFormItem()
            _editItemTemplate.InstantiateIn(_item)
            Controls.Add(_item)
        End Sub

        Protected Overrides Function OnBubbleEvent(ByVal source As Object, ByVal args As EventArgs) As Boolean
            _results = _editItemTemplate.ExtractValues(_item)
            RaiseEvent ProductUpdated(Me, EventArgs.Empty)
            Return True
        End Function
    End Class

    Public Class ProductFormItem
        Inherits WebControl
        Implements IDataItemContainer

        Private _name As String
        Private _price As Decimal

        Public Property Name() As String
            Get
                Return _name
            End Get
            Set(ByVal Value As String)
                _name = Value
            End Set
        End Property

        Public Property Price() As Decimal
            Get
                Return _price
            End Get
            Set(ByVal Value As Decimal)
                _price = Value
            End Set
        End Property

        Public ReadOnly Property DataItem() As Object Implements IDataItemContainer.DataItem
            Get
                Return Me
            End Get
        End Property

        Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
            Get
                Return 0
            End Get
        End Property

        Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex
            Get
                Return 0
            End Get
        End Property

    End Class
End Namespace

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 33.7, 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 33.8 illustrates how you can use the ProductForm control to update the properties of a product.

Example 33.8. ShowProductForm.aspx

<%@ Page Language="VB" %>
<%@ 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">

    sub Page_Load()
        If Not Page.IsPostBack Then
            ProductForm1.Name = "Laptop"
            ProductForm1.Price = 433.12
            ProductForm1.DataBind()
        End If
    End Sub

    Sub ProductForm1_ProductUpdated(ByVal sender As Object, ByVal e As EventArgs)
        lblName.Text = ProductForm1.Results("Name").ToString()
        lblPrice.Text = ProductForm1.Results("Price").ToString()
    End Sub
</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"
        OnProductUpdated="ProductForm1_ProductUpdated"
        Runat="server">
        <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 33.8, 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 33.8 handles the ProductUpdated event and displays the new values with two Label controls (see Figure 33.3).

Using a two-way databinding template.

Figure 33.3. Using a two-way databinding template.

Creating Templated Databound Controls

In this section, you learn how to build templated databound controls. A databound control can be bound to a DataSource control such as the SqlDataSource or ObjectDataSource controls.

The ASP.NET 2.0 Framework provides you with a number of new base classes that you can use when creating a custom databound control. Creating a databound control using the previous version of the Framework was not a trivial matter (it took longer than a single afternoon). The ASP.NET 2.0 Framework, on the other hand, makes creating databound controls easy. (You can implement a custom databound control in less than 15 minutes.)

So, let’s look at some tables and figures. Table 33.1 lists the base control classes for all the standard ASP.NET databound controls. Figure 33.4 displays the inheritance hierarchy of all the new databound controls in the ASP.NET 2.0 Framework. Typically, you’ll inherit from one of the leaf nodes. You’ll create a control that derives from the base CompositeDataBoundControl, HierarchicalDataBoundControl, or ListControl class.

Table 33.1. Base Databound Control Classes

Control

Base Control

GridView, DetailsView, FormView

CompositeDataBoundControl

Menu, TreeView

HierarchicalDataBoundControl

DropDownList, ListBox RadioButtonList, CheckBoxList, BulletedList

ListControl

DataList, DataGrid

BaseDataList

Repeater

Control

Databound control inheritance hierarchy.

Figure 33.4. Databound control inheritance hierarchy.

This chapter concentrates on inheriting new controls from the base CompositeDataBoundControl class. This is the appropriate base class to use when you want to display one or more database records and use templates.

Note

You learned how to create controls that inherit from the base ListControl class in Chapter 10, “Using List Controls.”

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 33.9.

Example 33.9. DivView.vb

Imports System
Imports System.Collections
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace AspNetUnleashed
    Public Class DivView
        Inherits CompositeDataBoundControl

        Private _itemTemplate As ITemplate

        <TemplateContainer(GetType(DivViewItem))> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property ItemTemplate() As ITemplate
            Get
                Return _itemTemplate
            End Get
            Set(ByVal Value As ITemplate)
                _itemTemplate = Value
            End Set
        End Property
        Protected Overrides Function CreateChildControls(ByVal dataSource As IEnumerable, ByVal dataBinding As Boolean) As Integer
            Dim counter As Integer = 0
            For Each dataItem As Object In dataSource
                Dim contentItem As New DivViewItem(dataItem, counter)
                _itemTemplate.InstantiateIn(contentItem)
                Controls.Add(contentItem)
                counter = counter + 1
            Next
            DataBind(False)
            Return counter
        End Function

        Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
            Get
                Return HtmlTextWriterTag.Div
            End Get
        End Property
    End Class


    Public Class DivViewItem
        Inherits WebControl
        Implements IDataItemContainer

        Private _dataItem As Object
        Private _index As Integer


        Public ReadOnly Property DataItem() As Object Implements IDataItemContainer.DataItem
            Get
                Return _dataItem
            End Get
        End Property

        Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
            Get
                Return _index
            End Get
        End Property

        Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex
            Get
                Return _index
            End Get
        End Property

        Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
            Get
                Return HtmlTextWriterTag.Div
            End Get
        End Property

        Public Sub New(ByVal dataItem As Object, ByVal index As Integer)
            _dataItem = dataItem
            _index = index
        End Sub

    End Class
End Namespace

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 or not 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 33.10 illustrates how you can bind the DivView control to a SqlDataSource control.

Example 33.10. ShowDivView.aspx

<%@ Page Language="VB" %>
<%@ 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 33.10, the SqlDataSource control represents the Movies database table. The DivView control includes an EditItemTemplate that formats each of the columns from this database table (see Figure 33.5).

Displaying database records with the DivView control.

Figure 33.5. Displaying database records with the DivView control.

Creating an AjaxDivView Control

Two of the three ASP.NET controls that derive from the CompositeDataBoundControl class take advantage of AJAX. The GridView control supports AJAX when sorting and paging through database records. The DetailsView control supports AJAX when paging through database records.

There is a good reason for supporting AJAX when building databound controls. Using AJAX can significantly improve your application’s performance. By taking advantage of AJAX, you can avoid re-creating an entire page each and every time you perform a database operation. AJAX enables you to transfer only the information you need back and forth between the web server and browser.

Note

Microsoft insists on using the term Client Callbacks to refer to AJAX. Because AJAX has become the standard name for this technology, I use AJAX instead of Client Callbacks in this book.

In this section, we add AJAX functionality to the DivView control. The modified DivView control, the AjaxDivView control, supports having its contents refreshed through an AJAX call.

The AjaxDivView control is contained in Listing 33.11.

Example 33.11. AjaxDivView.vb

Imports System
Imports System.Text
Imports System.IO
Imports System.Collections
Imports System.Web
Imports System.Web.UI
Imports System.Web.UI.WebControls

Namespace AspNetUnleashed
    Public Class AjaxDivView
        Inherits CompositeDataBoundControl
        Implements ICallbackEventHandler

        Private _itemTemplate As ITemplate

        ''' <summary>
        ''' The ItemTemplate is used to format each item
        ''' from the data source
        ''' </summary>
        <TemplateContainer(GetType(DivViewItem))> _
        <PersistenceMode(PersistenceMode.InnerProperty)> _
        Public Property ItemTemplate() As ITemplate
            Get
                Return _itemTemplate
            End Get
            Set(ByVal Value As ITemplate)
                _itemTemplate = Value
            End Set
        End Property

        ''' <summary>
        ''' Register JavaScripts
        ''' </summary>
        Protected Overrides Sub OnPreRender(ByVal e As EventArgs)
            ' Register JavaScript library
            Page.ClientScript.RegisterClientScriptInclude("AjaxDivView",Page.ResolveUrl("~/ClientScripts/AjaxDivView.js"))

            ' Register Refresh function
            Dim eRef As String = Page.ClientScript.GetCallbackEventReference(Me, Nothing, "AjaxDivView_Result", "'" & Me.ClientID & "'", "AjaxDivView_Error", False)
            Dim refreshFunc As String = "function AjaxDivView_Refresh() {" & eRef & "}"
            Page.ClientScript.RegisterClientScriptBlock(Me.GetType(), Me.UniqueID, refreshFunc, True)

            MyBase.OnPreRender(e)
        End Sub


        ''' <summary>
        ''' Iterate through the data items and instantiate each data
        ''' item in a template
        ''' </summary>
        Protected Overrides Function CreateChildControls(ByVal dataSource As IEnumerable, ByVal dataBinding As Boolean) As Integer
            Dim counter As Integer = 0
            For Each dataItem As Object In dataSource
                Dim contentItem As New DivViewItem(dataItem, counter)
                _itemTemplate.InstantiateIn(contentItem)
                Controls.Add(contentItem)
                counter = counter + 1
            Next
            DataBind(False)
            Return counter
        End Function
        ''' <summary>
        ''' Render this control's contents in a DIV tag
        ''' </summary>
        Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
            Get
                Return HtmlTextWriterTag.Div
            End Get
        End Property

        ''' <summary>
        ''' Whenever I get called through AJAX,
        ''' rebind my data
        ''' </summary>
        Public Sub RaiseCallbackEvent(ByVal eventArgument As String) Implements ICallbackEventHandler.RaiseCallbackEvent
            Me.DataBind()
        End Sub

        ''' <summary>
        ''' Render my contents to a string
        ''' and send the result back to the client
        ''' </summary>
        Public Function GetCallbackResult() As String Implements ICallbackEventHandler.GetCallbackResult
            Dim builder As New StringBuilder()
            Dim sWriter As New StringWriter(builder)
            Dim hWriter As New HtmlTextWriter(sWriter)
            Me.RenderContents(hWriter)
            Return builder.ToString()
        End Function

    End Class


    Public Class AjaxDivViewItem
        Inherits WebControl
        Implements IDataItemContainer

        Private _dataItem As Object
        Private _index As Integer


        Public ReadOnly Property DataItem() As Object Implements IDataItemContainer.DataItem
            Get
                Return _dataItem
            End Get
        End Property

        Public ReadOnly Property DataItemIndex() As Integer Implements IDataItemContainer.DataItemIndex
            Get
                Return _index
            End Get
        End Property

        Public ReadOnly Property DisplayIndex() As Integer Implements IDataItemContainer.DisplayIndex
            Get
                Return _index
            End Get
        End Property

        Protected Overrides ReadOnly Property TagKey() As HtmlTextWriterTag
            Get
                Return HtmlTextWriterTag.Div
            End Get
        End Property

        Public Sub New(ByVal dataItem As Object, ByVal index As Integer)
            _dataItem = dataItem
            _index = index
        End Sub

    End Class
End Namespace

The AjaxDivView control in Listing 33.11 is very similar to the DivView control created in the previous section, except for the fact that it implements the ICallbackEventHandler interface. This interface has two methods that you must implement: the RaiseCallbackEvent() and GetCallbackResult() methods.

The AjaxDivView control calls its DataBind() method in the RaiseCallbackEvent() method. When an AJAX call is made to this control, the control automatically refreshes its contents by rebinding to its data source.

The GetCallbackResult() returns a string that is sent to the browser as a result of an AJAX call. The AjaxDivView control renders its contents to a string and sends the string to the browser.

Notice that the AjaxDivView control registers two JavaScript scripts in its OnPreRender() method. First, the method registers an external JavaScript library named AjaxDivView.js. Second, it creates a JavaScipt function named AjaxDivView_Refresh() that refreshes the contents of the AjaxDivView.

The AjaxDivView.js file is contained in Listing 33.12.

Example 33.12. AjaxDivView.js

function AjaxDivView_Result(result, controlID)
{
    var control = document.getElementById(controlID);
    control.innerHTML = result;
}

function AjaxDivView_Error(error)
{
    alert( error );
}

The JavaScript file in Listing 33.12 includes two functions. The first function, named AjaxDivView_Result(), is called after AjaxDivView content is retrieved from the server. This function updates the innerHTML of the AjaxDivView control’s containing <div> tag with the updated content.

The second function, the AjaxDivView_Error() function, is called only when there is an error on the server during an AJAX call. This function simply displays the error in a JavaScript alert box.

The page in Listing 33.13 illustrates how you can use the AjaxDivView control.

Example 33.13. ShowAjaxDivView.aspx

<%@ Page Language="VB" %>
<%@ 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">
    <script type="text/javascript">

    window.onload = function()
    {
        window.setInterval("AjaxDivView_Refresh()", 5000);
    }
    </script>

    <style type="text/css">
        h1
        {
            font-size:16px;
        }
        .ajaxDivView div
        {
            border:solid 1px black;
            padding:5px;
            margin:10px;
        }

    </style>
    <title>Show AjaxDivView</title>
</head>
<body language="javascript">
    <form id="form1" runat="server">
    <div>

    Page Time: <%= DateTime.Now.ToString() %>

    <br /><br />
    <button onclick="AjaxDivView_Refresh();return false;">Refresh</button>

    <br /><br />
    <custom:AjaxDivView
        id="AjaxDivView1"
        DataSourceID="srcMovies"
        CssClass="ajaxDivView"
        Runat="server">
        <ItemTemplate>

        <h1><%# Eval("Title") %></h1>
        <em>Director: <%# Eval("Director") %></em>

        </ItemTemplate>
    </custom:AjaxDivView>


    <asp:SqlDataSource
        id="srcMovies"
        ConnectionString="<%$ ConnectionStrings:Movies %>"
        SelectCommand="SELECT Id,Title,Director FROM Movies"
        Runat="server" />
    </div>
    </form>
</body>
 </html>

The page in Listing 33.13 displays the contents of the Movies database table. The AjaxDivView control is bound to a SqlDataSource control.

The page includes a little bit of JavaScript. The JavaScript causes the contents of the AjaxDivView to refresh automatically every five seconds. If you change one of the records in the Movies database table, then the contents of the AjaxDivView control automatically updates to reflect the change within five seconds (see Figure 33.6).

Displaying data with the AjaxDivView control.

Figure 33.6. Displaying data with the AjaxDivView control.

The following JavaScript statement causes the AjaxDivView to update its contents every five seconds:

window.setInterval("AjaxDivView_Refresh()", 5000);

The AjaxDivView_Refresh() method initiates the AJAX call to refresh the AjaxDivView control’s contents.

As an alternative to waiting the five seconds after a database update, if you are really impatient, you can click the button contained on the page. The button is a client-side button that calls the AjaxDivView_Refresh() method in its onclick handler.

Notice that the current time is displayed at the top of the page. This time never changes even when the contents of the AjaxDivView is updated. Only the AjaxDivView is refreshed, and not the rest of the page.

Creating an AjaxFormView Control

In this section, we create an AjaxFormView control. The AjaxFormView control works like the standard ASP.NET FormView control. However, the AjaxFormView control enables you to update and delete database records from the client by making AJAX calls back to the server.

Unfortunately, the code for the AjaxFormView control is too long to include in the pages of this book. The entire source code for the control is included on the CD that accompanies this book in both VB.NET and C# versions.

The AjaxFormView control has the following properties:

  • InsertItemTemplate—. Enables you to supply a template that is used when the control is in Insert mode.

  • EditItemTemplae—. Enables you to supply a template that is used when the control is in Edit mode.

  • DataKeyNames—. Enables you to specify one or more primary keys to use when updating a record.

  • DataKey—. Enables you to retrieve the value of the data key associated with the current record being edited.

  • DefaultMode—. Enables you to place the control in either Edit or Insert mode.

  • OnClientItemInserted—. Enables you to specify an optional script that executes when a new record is inserted.

  • OnClientItemUpdated—. Enables you to specify an optional script that executes when an existing record is updated.

The AjaxFormView control can be used to either insert or update a database record. You can set the control to either Insert or Update mode by setting the DefaultMode property.

Warning

Make sure that you assign a value to the AjaxFormView control’s DataKeyNames property when the control is set to Edit mode.

When you set the AjaxFormView control to Insert mode, the control renders an Insert button with its RenderInsertButton() method. This method creates a client-side button that initiates an AJAX call. The code for the RenderInsertButton() method is contained in Listing 33.14.

Example 33.14. RenderInsertButton()

''' <summary>
''' Render the Insert button with the AJAX onclick handler
''' </summary>
Private Sub RenderInsertButton(ByVal writer As HtmlTextWriter)
  Dim eRef As String = Page.ClientScript.GetCallbackEventReference(Me, "'insert'", _onClientItemInserted, "'" & Me.ClientID & "'", "AjaxFormView_Error", False)
  eRef = "__theFormPostData='';WebForm_InitCallback();" & eRef & ";return false"
  writer.AddAttribute(HtmlTextWriterAttribute.Onclick, eRef)
  writer.RenderBeginTag(HtmlTextWriterTag.Button)
  writer.Write("Insert")
  writer.RenderEndTag()
End Sub

The RenderInsertButton() method creates a button onclick handler that performs three actions. First, it sets the __theFormPostData variable to an empty string. Next, it calls the WebForm_InitCallback() method. These first two steps are required to pass the updated form field values back to the server in the AJAX call. By default, when an AJAX call is made back to the server, only the initial values of all the form fields are sent with the AJAX request.

Finally, the onclick handler actually makes the AJAX call. The necessary function call to initiate the AJAX request is retrieved with the help of the Page.ClientScript.GetCallbackEventReference() method.

When a user clicks the Insert button, an AJAX call is made back to the server and the control’s RaiseCallbackEvent() method executes. When the Insert button is clicked, this method calls the HandleInsert() method. The code for the HandleInsert() method is contained in Listing 33.15.

Example 33.15. HandleInsert()

''' <summary>
''' Perform database insert by executing DataSource Insert method
''' </summary>
Private Sub HandleInsert()
  Dim values As IOrderedDictionary = _insertItemTemplate.ExtractValues(_item)
  Dim dataSource As DataSourceView = CType(Me.GetData(), DataSourceView)
  dataSource.Insert(values, AddressOf DataCallback)
End Sub

The HandleInsert() method gets the values of the databinding expressions from the InsertItemTemplate by calling the template’s ExtractValues() method. This method is available because the InsertItemTemplate is marked as a template that supports two-way databinding.

The DataSource control to which the AjaxFormView control is bound has a DataSourceView associated with it. This DataSourceView is retrieved by a call to the GetData() method. The DataSourceView class includes Insert(), Update(), and Delete() methods you can call to modify the database data associated with the FormView. The HandleInsert() method in Listing 33.15 calls the Insert() method to insert a new database record.

The page in Listing 33.16 illustrates how you can use the AjaxFormView control. This page contains three controls: an AjaxFormView, AjaxDivView, and SqlDataSource control. The page enables you to add new records to the Movies database table (see Figure 33.7).

Inserting new records with the AjaxFormView control.

Figure 33.7. Inserting new records with the AjaxFormView control.

Example 33.16. ShowAjaxFormView.aspx

<%@ Page Language="VB" %>
<%@ 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">
    <script type="text/javascript">

        function ItemInserted(error, controlID)
        {
            if (error == '')
            {
                var ajaxFormView = document.getElementById(controlID);
                var inputs = ajaxFormView.getElementsByTagName('input'),
                for (var i=0;i < inputs.length;i++)
                    inputs?.value = inputs?.defaultValue;

                window.setTimeout("AjaxDivView_Refresh()", 100);
            }
            else
                alert( 'Error: ' + error );
        }

    </script>

    <style type="text/css">
        html
        {
            background-color:silver;
        }

        .headerStrip
        {
            margin-bottom:10px;
            padding:10px;
            background-color:white;
            border:solid 1px black;
        }

        .frmMovie
        {
            background-color:white;
            width:400px;
            padding:10px;
            margin-right:20px;
            float:left;
            border:solid 1px black;
        }

        .listMovies
        {
            background-color:white;
            float:left;
            padding:10px;
            border:solid 1px black;

        }

        .listMovies div
        {
            margin:10px;
            border:solid 1px black;
            padding:5px;
        }
    </style>

    <title>Show AjaxFormView</title>
</head>
<body>
    <form id="form1" runat="server">

    <div class="headerStrip">
    Page Time: <%= DateTime.Now.ToString("T") %>
    </div>

    <custom:AjaxFormView
        id="frmMovie"
        DataSourceID="srcMovies"
        DefaultMode="insert"
        OnClientItemInserted="ItemInserted"
        CssClass="frmMovie"
        Runat="server">
        <InsertItemTemplate>

        <asp:Label
            id="lblTitle"
            Text="Movie Title:"
            AssociatedControlID="txtTitle"
            Runat="server" />
        <br />
        <asp:TextBox
            id="txtTitle"
            Text='<%# Bind("Title") %>'
            Runat="server" />
        <br /><br />
        <asp:Label
            id="Label1"
            Text="Movie Director:"
            AssociatedControlID="txtDirector"
            Runat="server" />
        <br />
        <asp:TextBox
            id="txtDirector"
            Text='<%# Bind("Director") %>'
            Runat="server" />

        </InsertItemTemplate>
    </custom:AjaxFormView>

    <custom:AjaxDivView
        id="listMovies"
        DataSourceID="srcMovies"
        CssClass="listMovies"
        Runat="server">
        <ItemTemplate>
        <h1><%# Eval("Title") %></h1>
        <em>Director:<%# Eval("Director") %></em>
        </ItemTemplate>
    </custom:AjaxDivView>

    <asp:SqlDataSource
        id="srcMovies"
        ConnectionString="<%$ ConnectionStrings:Movies %>"
        SelectCommand="SELECT Id,Title, Director
            FROM Movies ORDER BY Id DESC"
        InsertCommand="INSERT MOVIES (Title, Director)
            VALUES (@Title, @Director)"
        Runat="server" />

    </div>
    </form>
</body>
 </html>

When you add new records to the Movies database table by clicking the AjaxFormView control’s Insert button, the page is not posted back to the server. Instead, the new record is added to the database with an AJAX call.

The AjaxDivView displays the current contents of the Movies database table. The contents of the AjaxDivView control are refreshed automatically whenever a new record is added with the AjaxFormView control. The AjaxDivView control is refreshed by the AjaxFormView control’s client-side ItemInserted() method.

The beautiful thing about both the AjaxFormView and AjaxDivView control is that both controls enable you to interact with the web server without posting the page that contains the controls back to the web server. This creates a user experience much closer to working with a desktop application. It also greatly improves the performance of your application because the entire page does not need to be re-created each and every time that you need to perform a database operation.

Summary

This chapter was devoted to the topic of building templated databound controls. In the first part, you learned how to support templates in your custom controls. You learned how to create templates that support both one-way and two-way databinding expressions. You also learned how to create a default template for a control.

The second half of this chapter focused on the topic of building databound controls. You learned how to create a simple DivView control that displays the records from a database table in a series of HTML <div> tags. Finally, you learned how to build two databinding controls that take advantage of AJAX. We created an AjaxDivView control that automatically refreshes its contents every five seconds. We also created a AjaxFormView control that enables you to insert and update database records directly from the client without a postback to the server.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset