Using the UpdatePanel Control

In this section, we'll walk through three increasingly complex examples of UpdatePanel. Each example begins from an ASP.NET 2.0 page that runs without ASP.NET Ajax. By adding UpdatePanel controls in strategic places, you can considerably improve the user's experience with minimal changes.

The first example is a wizard, the second is a master/details scenario that you might find on an e-commerce site, and the third is a simple search engine that could be part of the same site.

Making the ASP.NET 2.0 Wizard Control Behave in a More Fluid Manner

ASP.NET 2.0 introduced the Wizard control, a feature that enables developers to break tasks too complex for a single page into a number of smaller steps. Before moving from one step to the next, however, the control must post back to the server, which forces the user to wait while the browser completes its work. Example 2 shows markup and C# code for a very simple, three-step wizard page. In the first step, you enter your name; in the second step, you enter your address; and in the third step, a summary of the data you entered in the first two steps is displayed.

Example 2. A simple page with a Wizard control

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>A simple wizard</title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:Wizard runat="server" ID="Wizard1" >
                <WizardSteps>
                    <asp:TemplatedWizardStep runat="server" ID="Step1"
                        AllowReturn="true" Title="Name">
                        <ContentTemplate>
                            Please enter your name:<br />
                            <asp:TextBox runat="server" ID="Name" />
                        </ContentTemplate>
                    </asp:TemplatedWizardStep>
                    <asp:TemplatedWizardStep runat="server" ID="Step2"
                        AllowReturn="true" Title="Address">
                        <ContentTemplate>
                            Please enter your address:<br />
                            <asp:TextBox runat="server" ID="Address"
                              TextMode="MultiLine" />
                        </ContentTemplate>
                    </asp:TemplatedWizardStep>
                    <asp:TemplatedWizardStep runat="server" ID="Summary"
                        Title="Summary" StepType="Finish">
                        <ContentTemplate>
                            <%=
((TextBox)(Step1.ContentTemplateContainer.FindControl("Name"))).Text %><br />
                            <%=
((TextBox)(Step2.ContentTemplateContainer.FindControl("Address"))).Text %><br />
                        </ContentTemplate>
                    </asp:TemplatedWizardStep>
                </WizardSteps>
            </asp:Wizard>
        </div>
    </form>
</body>
</html>

This example uses C#, but only the content template of the last step uses C#-specific code. Here's the same template in VB.NET:

<ContentTemplate>
  <%= CType(Step1.ContentTemplateContainer.FindControl("Name"), TextBox).Text %>
  <br />
  <%= CType(Step2.ContentTemplateContainer.FindControl("Address"), TextBox).Text %>
  <br />
</ContentTemplate>

Figure 8 shows how the browser displays the Wizard.

A simple Wizard

Figure 8. A simple Wizard

To make the wizard experience more fluid, you can place an UpdatePanel control around the Wizard control. This is enough to transparently transform the classical postbacks from any control inside the Wizard control into asynchronous postbacks. Example 3 shows the markup you need to add to make this happen.

Example 3. Adding asynchronous postbacks to a simple wizard

<%@ Page Language="C#" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title>A simple wizard</title>
</head>
<body>
    <form id="form1" runat="server">
        <asp:ScriptManager ID="ScriptManager1" runat="server"
          />
        <div>
            <asp:UpdatePanel runat="server" ID="WizardPanel">
                <ContentTemplate>
                    <asp:Wizard runat="server" ID="Wizard1" >
                        <WizardSteps>
                            <asp:TemplatedWizardStep runat="server" ID="Step1"
                                AllowReturn="true" Title="Name">
                                <ContentTemplate>
                                    Please enter your name:<br />
                                    <asp:TextBox runat="server" ID="Name" />
                                </ContentTemplate>
                            </asp:TemplatedWizardStep>
                            <asp:TemplatedWizardStep runat="server" ID="Step2"
                                AllowReturn="true" Title="Address">
                                <ContentTemplate>
                                    Please enter your address:<br />
                                    <asp:TextBox runat="server" ID="Address"
                                      TextMode="MultiLine" />
                                </ContentTemplate>
                            </asp:TemplatedWizardStep>
                            <asp:TemplatedWizardStep runat="server" ID="Summary"
                                Title="Summary" StepType="Finish">
                                <ContentTemplate>
                                    <%=
((TextBox)(Step1.ContentTemplateContainer.FindControl("Name"))).Text %><br />
                                    <%=
((TextBox)(Step2.ContentTemplateContainer.FindControl("Address"))).Text %><br />
                                </ContentTemplate>
                            </asp:TemplatedWizardStep>
                        </WizardSteps>
                    </asp:Wizard>
                </ContentTemplate>
            </asp:UpdatePanel>
        </div>
    </form>
</body>
</html>

All that had to be done to AJAX-enable this page was to add the ScriptManager control to the page and surround the Wizard control with the UpdatePanel control. Otherwise, the page remains untouched.

Creating a More Interactive Master/Details Page

In this section, we'll show you how to use an UpdatePanel control to refresh the data display from the AdventureWorks SQL Server sample database (see the section "What You Need to Get the Most from this Short Cut").

Figure 9 shows the page you'll be enhancing.

The data access layer for the master/details sample application

Figure 9. The data access layer for the master/details sample application

To work with this example, the AdventureWorks_Data.mdf and AdventureWorks_Log.ldf files must be placed in the application's App_Data directory. The Web.config file must also be modified to include the connection string to the database. You can do this by adding the following markup to the <configuration> section of the Web.config file (add to the existing connectionStrings section if you already have one instead of creating a new one):

<connectionStrings>
  <add name="AdventureWorks_DataConnectionString"
    connectionString="Data Source=.SQLEXPRESS;
      AttachDbFilename=|DataDirectory|AdventureWorks_Data.mdf;
      Integrated Security=True;User Instance=True"
   providerName="System.Data.SqlClient" />
</connectionStrings>

First, you need to create a simple data access layer using the Visual Web Developer DataSet wizard (See "Appendix: Creating the AdventureProducts.xsd DataSet"). This .xsd file must be placed in the application's App_Code directory so that the .xsd build provider can build and compile the code from the XML.

The master/details page in Figure 9 gets its data from the AdventureWorks.xsd data access layer using ObjectDataSource controls. It should be clear that the architecture used here is not exactly what you would use in a real application; in this sample application, the UI layer talks directly to the data access layer. A real application would usually have an intermediary business layer in between, which we omitted here for simplicity and to keep the focus of the discussion on UpdatePanel.

The page displays the master list of products in a GridView where sorting, pagination, and selection are enabled. The data displayed in the master view can be filtered by product category by choosing a category name from the ProductCategoryList DropDownList control.

When the user selects a product in the master view, the product's details are displayed in the ProductDetails FormView control.

Both the master and details views show images of the products that are obtained by pointing the ImageUrl property of an Image control to a small handler that gets the binary data for the image from the database and outputs it to the Response stream. Example 4 shows C# code that implements an image handler for this application. The code from example 4 should be saved as ProductImage.ashx in the root directory of the application.

Example 4. Image handler for the master/details page

<%@ WebHandler Language="C#" Class="ProductImage" %>

using System;
using System.Web;
using AdventureProductsTableAdapters;

public class ProductImage : IHttpHandler {

    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "image/jpeg";
        string idString = context.Request.QueryString["ID"];
        if (!String.IsNullOrEmpty(idString)) {
            QueriesTableAdapter adapter = new QueriesTableAdapter();
            int photoID = int.Parse(idString);
            byte[] img = (String.IsNullOrEmpty(context.Request.QueryString["full"]) ?
                adapter.GetProductThumbnail(photoID) :
                adapter.GetProductPhoto(photoID)
                ) as byte[];
            if (img != null) {
                context.Response.BinaryWrite(img);
            }
        }
    }

    public bool IsReusable {
        get {
            return true;
        }
    }

}

The category DropDownList control, the master view, and the details view communicate with and filter each other when they are used as parameters of the ObjectDataSource controls they get their data from.

Example 5 shows the markup for this page without UpdatePanel. This page works fine in ASP.NET 2.0 without ASP.NET Ajax. It posts back whenever you select a category in the drop-down, sort, go to a different page, or select a product.

Example 5. Commerce page with master and details views

<%@ Page Language="C#" %>

<!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 runat="server">
    <title>Master/Details with UpdatePanel</title>
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <asp:ObjectDataSource ID="ProductDataSource" runat="server"
            SelectMethod="GetProductsByCategory"
            TypeName="AdventureProductsTableAdapters.ProductTableAdapter">
            <SelectParameters>
                <asp:ControlParameter ControlID="ProductCategoryList"
                  DefaultValue="−1" Name="ProductCategoryID"
                  PropertyName="SelectedValue" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>
        <asp:ObjectDataSource ID="ProductCategoryDataSource" runat="server"
          TypeName="AdventureProductsTableAdapters.ProductCategoryTableAdapter"
          SelectMethod="GetData">
        </asp:ObjectDataSource>
        <asp:ObjectDataSource ID="ProductDetailsDataSource" runat="server"
            SelectMethod="GetProductDetails"
            TypeName="AdventureProductsTableAdapters.ProductDetailsTableAdapter">
            <SelectParameters>
                <asp:Parameter Name="CultureID" Type="String" DefaultValue="en" />
                <asp:ControlParameter ControlID="ProductList"
                  PropertyName="SelectedValue" Name="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:DropDownList ID="ProductCategoryList" runat="server"
          DataSourceID="ProductCategoryDataSource"
          DataTextField="Name" DataValueField="ProductCategoryID" AutoPostBack="true"
          AppendDataBoundItems="true">
            <asp:ListItem Value="−1" Text="Any category" />
        </asp:DropDownList>

        <asp:GridView ID="ProductList" runat="server" AllowPaging="True"
          PageSize="5" AllowSorting="True"
          AutoGenerateColumns="False" DataSourceID="ProductDataSource"
          DataKeyNames="ProductID">
            <Columns>
                <asp:CommandField ShowSelectButton="True" />
                <asp:ImageField DataImageUrlField="ProductPhotoID"
                  DataImageUrlFormatString="ProductImage.ashx?ID={0}"
                  DataAlternateTextField="Name" />
                <asp:BoundField DataField="Name" HeaderText="Name"
                  SortExpression="Name" />
                <asp:BoundField DataField="Color" HeaderText="Color"
                  SortExpression="Color" />
                <asp:BoundField DataField="CategoryName"
                  HeaderText="CategoryName" SortExpression="CategoryName" />
                <asp:BoundField DataField="SubCategoryName"
                  HeaderText="SubCategoryName"
                  SortExpression="SubCategoryName" />
            </Columns>
         </asp:GridView>

         <asp:FormView ID="ProductDetails" runat="server"
           EmptyDataText="Please select a product."
           DataSourceID="ProductDetailsDataSource">
             <ItemTemplate>
                <h1>
                    <asp:Literal ID="NameLiteral" runat="server"
                      Text='<%# Eval("Name") %>' />:
                    <asp:Literal ID="PriceLiteral" runat="server"
                      Text='<%# Eval("ListPrice", "{0:c}") %>' />
                </h1>
                <h2>
                    <asp:Literal ID="CategoryLiteral" runat="server"
                      Text='<%# Eval("CategoryName") %>' /> /
                    <asp:Literal ID="SubCategoryLiteral" runat="server"
                      Text='<%# Eval("SubCategoryName") %>' />
                </h2>
                <p>
                    <asp:Image ID="ProductImage" runat="server"
                      ImageUrl='<%# Eval("ProductPhotoID",
                        "ProductImage.ashx?ID={0}&full=true") %>'
                      AlternateText='<% Eval("Name") %>' ImageAlign="Left" />
                    <asp:Literal ID="DescriptionLiteral" runat="server"
                      Text='<%# Eval("Description") %>' />
                    <br />

                    Color:
                    <asp:Literal ID="ColorLiteral" runat="server"
                      Text='<%# Eval("Color") %>' />
                    <br />

                    Size:
                    <asp:Literal ID="SizeLiteral" runat="server"
                      Text='<%# Eval("Size") %>' />
                    <asp:Literal ID="SizeUnitLiteral" runat="server"
                      Text='<%# Eval("SizeUnitMeasureCode") %>'/>
                    <br />

                    Weight:
                    <asp:Literal ID="WeightLiteral" runat="server"
                      Text='<%# Eval("Weight") %>' />
                    <asp:Literal ID="WeightUnitLiteral" runat="server"
                      Text='<%# Eval("WeightUnitMeasureCode") %>'/>
                </p>
            </ItemTemplate>
        </asp:FormView>
    </div>
    </form>
</body>
</html>

Example 6 shows the same page with UpdatePanel controls, one around the master view and another around the details view.

Example 6. Commerce page that uses UpdatePanel to implement asynchronous postbacks

<%@ Page Language="C#" %>

<!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 runat="server">
    <title>Master/Details with UpdatePanel</title>
</head>
<body>
    <asp:ScriptManager ID="ScriptManager1" runat="server"
 />
    <form id="form1" runat="server">
    <div>
        <asp:ObjectDataSource ID="ProductDataSource" runat="server"
            SelectMethod="GetProductsByCategory"
            TypeName="AdventureProductsTableAdapters.ProductTableAdapter">
            <SelectParameters>
                <asp:ControlParameter ControlID="ProductCategoryList"
                  DefaultValue="−1" Name="ProductCategoryID"
                  PropertyName="SelectedValue" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>
        <asp:ObjectDataSource ID="ProductCategoryDataSource" runat="server"
          TypeName="AdventureProductsTableAdapters.ProductCategoryTableAdapter"
          SelectMethod="GetData">
        </asp:ObjectDataSource>
        <asp:ObjectDataSource ID="ProductDetailsDataSource" runat="server"
            SelectMethod="GetProductDetails"
            TypeName="AdventureProductsTableAdapters.ProductDetailsTableAdapter">
            <SelectParameters>
                <asp:Parameter Name="CultureID" Type="String" DefaultValue="en" />
                <asp:ControlParameter ControlID="ProductList"
                  PropertyName="SelectedValue" Name="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:DropDownList ID="ProductCategoryList" runat="server"
          DataSourceID="ProductCategoryDataSource"
          DataTextField="Name" DataValueField="ProductCategoryID" AutoPostBack="true"
          AppendDataBoundItems="true">
            <asp:ListItem Value="−1" Text="Any category" />
        </asp:DropDownList>

        <asp:UpdatePanel ID="ProductListPanel" runat="server"
UpdateMode="Conditional">
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="ProductCategoryList"
                  EventName="SelectedIndexChanged" />
            </Triggers>
            <ContentTemplate>
                <asp:GridView ID="ProductList" runat="server" AllowPaging="True"
                  PageSize="5" AllowSorting="True"
                  AutoGenerateColumns="False" DataSourceID="ProductDataSource"
                  DataKeyNames="ProductID">
                    <Columns>
                        <asp:CommandField ShowSelectButton="True" />
                        <asp:ImageField DataImageUrlField="ProductPhotoID"
                          DataImageUrlFormatString="ProductImage.ashx?ID={0}"
                          DataAlternateTextField="Name" />
                        <asp:BoundField DataField="Name" HeaderText="Name"
                          SortExpression="Name" />
                        <asp:BoundField DataField="Color" HeaderText="Color"
                          SortExpression="Color" />
                        <asp:BoundField DataField="CategoryName"
                          HeaderText="CategoryName" SortExpression="CategoryName" />
                        <asp:BoundField DataField="SubCategoryName"
                          HeaderText="SubCategoryName"
                          SortExpression="SubCategoryName" />
                    </Columns>
                </asp:GridView>
            </ContentTemplate>
        </asp:UpdatePanel>

        <asp:UpdatePanel ID="ProductDetailsPanel" runat="server"
UpdateMode="Conditional">
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="ProductList"
                  EventName="SelectedIndexChanged" />
            </Triggers>
            <ContentTemplate>
                <asp:FormView ID="ProductDetails" runat="server"
                  EmptyDataText="Please select a product."
                  DataSourceID="ProductDetailsDataSource">
                    <ItemTemplate>
                        <h1>
                            <asp:Literal ID="NameLiteral" runat="server"
                              Text='<%# Eval("Name") %>' />:
                            <asp:Literal ID="PriceLiteral" runat="server"
                              Text='<%# Eval("ListPrice", "{0:c}") %>' />
                        </h1>
                        <h2>
                            <asp:Literal ID="CategoryLiteral" runat="server"
                              Text='<%# Eval("CategoryName") %>' /> /
                            <asp:Literal ID="SubCategoryLiteral" runat="server"
                              Text='<%# Eval("SubCategoryName") %>' />
                        </h2>
                        <p>
                            <asp:Image ID="ProductImage" runat="server"
                              ImageUrl='<%# Eval("ProductPhotoID",
                                "ProductImage.ashx?ID={0}&full=true") %>'
                              AlternateText='<% Eval("Name") %>' ImageAlign="Left" />
                            <asp:Literal ID="DescriptionLiteral" runat="server"
                              Text='<%# Eval("Description") %>' />
                            <br />

                            Color:
                            <asp:Literal ID="ColorLiteral" runat="server"
                              Text='<%# Eval("Color") %>' />
                            <br />

                            Size:
                            <asp:Literal ID="SizeLiteral" runat="server"
                              Text='<%# Eval("Size") %>' />
                            <asp:Literal ID="SizeUnitLiteral" runat="server"
                              Text='<%# Eval("SizeUnitMeasureCode") %>'/>
                            <br />

                            Weight:
                            <asp:Literal ID="WeightLiteral" runat="server"
                              Text='<%# Eval("Weight") %>' />
                            <asp:Literal ID="WeightUnitLiteral" runat="server"
                              Text='<%# Eval("WeightUnitMeasureCode") %>'/>
                        </p>
                    </ItemTemplate>
                </asp:FormView>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

In Example 5, we once again did not change anything in the code or in the features of the page; we made the page more responsive simply by surrounding the right controls with UpdatePanel controls. The results are shown in Figure 10.

The master/details view

Figure 10. The master/details view

The main thing to notice when comparing Example 5 and Example 6 is the addition of triggers to Example 6. The trigger for the master view is the category list, which has auto-postback enabled, which means the page posts back automatically whenever the user changes the selected item. Notice that the list is outside the panel, which is why it must be declared as a trigger. It is outside because it won't ever change, so it doesn't need to be updated; however, a new choice must trigger a fresh rendering of the master view. The GridView itself has a lot of internal postback triggers. Column headers trigger sorting, page numbers navigate to grid pages, and select buttons change the currently selected product. Because these postbacks occur inside the UpdatePanel control, you don't need to declare them as triggers: any postbacks of a control inside an UpdatePanel control are automatically treated as implicit triggers for the panel's updates.

The trigger for the details view is the SelectedIndexChanged event of the master view's GridView control, so the selection of a product triggers the re-rendering of the details view. When this happens, because the SelectedValue of the ProductList GridView is a parameter of the details view's ObjectDataSource, the details view always shows the product that's currently selected in the master view.

Any interaction with the page now triggers asynchronous postbacks. We've made the user's experience much more fluid by just adding UpdatePanel controls around the right controls.

Creating a Search Page with a Pop-up Details Preview

Our final example is a variation of Example 6. This time, we create a rudimentary search engine for a product catalog called AdventureWorks. The search results are paginated using GridView and UpdatePanel controls. There is also a details view, but this time it's displayed and updated when the mouse hovers over the master view. The search engine can be viewed as a super tool tip that loads on demand. The user sees her details displayed almost instantaneously despite the fact that these details were not sent with the original search results. The sample also features scalable pagination that enables the application to display very large numbers of records without any perceivable slowdown.

To implement the search page, add a stored procedure to the database and a table adapter to your DataSet.

To create the stored procedure, open the database in the server explorer, and in the Stored Procedures node's context menu, choose Add New Stored Procedure (see Figure 11). Paste the following code into the window that opens:

CREATE PROCEDURE dbo.PaginatedSearchProduct
       (
       @SearchCriteria NVarChar(255),
       @startRowIndex INT,
       @maximumRows INT
       )
AS

BEGIN

WITH SearchResults AS (
SELECT ROW_NUMBER() OVER (ORDER BY ProductID) AS Row, ProductID, Name, ListPrice,
Color
FROM Production.Product
WHERE (LOWER(Name) LIKE '%' + RTRIM(LTRIM(LOWER(@SearchCriteria))) + '%')
)

SELECT ProductID, Name, ListPrice, Color
FROM SearchResults
WHERE Row BETWEEN @startRowIndex AND @startRowIndex + @maximumRows

END
Adding a stored procedure to the database

Figure 11. Adding a stored procedure to the database

This stored procedure first creates a temporary view of the data that has all the columns you need to display in the search results plus one pseudocolumn that contains the row number. This view enables the procedure's second SQL query to filter out the records that are not in the current page's range. Using this procedure, you can send only the records you actually need from the database to ASP.NET, which ensures that the application can easily scale to search queries that return millions of records without any significant slowdown.

The searching logic here, on the other hand, is not what you would use in a real application. The "LIKE" keyword is, by far, not the fastest way to search in a database. A real application would use SQL Server 2005's full-text search engine (which is now available in the Express version of the product). To perform a search, you would have to create a full-text index and use it in the query. This is beyond of the scope of this Short Cut, but more details can be found on the MSDN web site at http://msdn2.microsoft.com/en-us/library/ms166353.aspx.

With this procedure in place, you can create the table adapter that will consume the procedures output. To do this, open AdventureProducts.xsd, right-click on the design interface, and choose Add→Table adapter... from the context menu.

On the first page of the wizard, you're prompted for the data connection you want to use. Choose the default, which should be the AdventureWorks_DataConnectionString from the previous example.

Click Next. The wizard now asks how it should query the database. Choose "Use existing stored procedures" and click Next.

The next screen asks which procedures to use for the Select, Insert, Update, and Delete commands. Since you only want to select from the database, enter PaginatedSearchProduct as the name of the Select command to use, as shown in Figure 12.

Choosing the Select procedure for the search engine

Figure 12. Choosing the Select procedure for the search engine

Click Finish. You should now have an additional adapter on the design surface. We'll add one more command that will get the total number of results the ObjectDataSource control needs to manage the pagination. To do this, right-click the newly created adapter's title bar and chose Add→Query... from the context menu. In the wizard that appears, leave the default choice selected ("Use SQL statements") and click Next. On the next screen, choose "SELECT which returns a single value" because you want to return the number of rows from the search. Click Next. On the next screen, paste this code for the query:

SELECT COUNT(*) AS EXPR1
FROM Production.Product
WHERE (LOWER(Name) LIKE '%' + RTRIM(LTRIM(LOWER(@SearchCriteria))) + '%')

Click Next and enter GetTotalRowCount as the name of the function that will be used to execute this query. Once you've clicked Finish, your adapter should look like Figure 13.

The search adapter on the design surface

Figure 13. The search adapter on the design surface

The @SearchCriteria parameter needs to allow for null values. To do this, right-click on the GetTotalRowCount command and select "Properties" from the context menu to bring the property sheet. Set the AllowDbNull property to true for the SearchCriteria parameter.

Now that the search data access layer is in place, you can create the search page itself. Once again, we'll start with an ordinary ASP.NET 2.0 page, whose markup is shown in Example 7.

Example 7. Markup for a simple search page

<%@ Page Language="C#" %>

<!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 runat="server">
    <title>Search</title>
</head>
<body>
    <form id="form1" runat="server"
        defaultbutton="SearchButton" defaultfocus="SearchBox">
    <div>
        <asp:ObjectDataSource ID="SearchDataSource" runat="server" EnablePaging=
"True"
        SelectCountMethod="GetTotalRowCount" SelectMethod="GetData"
        TypeName="AdventureProductsTableAdapters.PaginatedSearchProductTableAdapter">
            <SelectParameters>
                <asp:ControlParameter ControlID="SearchBox" Name="SearchCriteria"
                    PropertyName="Text" Type="String" DefaultValue="" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:ObjectDataSource ID="ProductDetailsDataSource" runat="server"
            SelectMethod="GetProductDetails"
            TypeName="AdventureProductsTableAdapters.ProductDetailsTableAdapter">
            <SelectParameters>
                <asp:Parameter Name="CultureID" Type="String" DefaultValue="en" />
                <asp:ControlParameter ControlID="SearchResults"
                    PropertyName="SelectedValue"
                    Name="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:TextBox ID="SearchBox" runat="server"></asp:TextBox>
        <asp:LinkButton ID="SearchButton" runat="server" Text="Search" />

        <asp:GridView ID="SearchResults" runat="server" AllowPaging="True"
            PageSize="5" AutoGenerateColumns="False" DataKeyNames="ProductID"
            DataSourceID="SearchDataSource" ShowHeader="False"
            AutoGenerateSelectButton="true">
            <Columns>
                <asp:BoundField DataField="Name" />
                <asp:BoundField DataField="Color" />
                <asp:BoundField DataField="ListPrice" DataFormatString="{0:c}" />
            </Columns>
        </asp:GridView>

        <asp:FormView runat="server" ID="DetailsView"
            DataSourceID="ProductDetailsDataSource">
            <ItemTemplate>
            <h1>
                <asp:Literal ID="NameLiteral" runat="server"
                    Text='<%# Eval("Name") %>' />:
                <asp:Literal ID="PriceLiteral" runat="server"
                    Text='<%# Eval("ListPrice", "{0:c}") %>' />
            </h1>
            <h2><asp:Literal ID="CategoryLiteral" runat="server"
                    Text='<%# Eval("CategoryName") %>' /> /
            <asp:Literal ID="SubCategoryLiteral" runat="server"
                    Text='<%# Eval("SubCategoryName") %>' /></h2>
            <p>
                <asp:Image ID="ProductImage" runat="server"
       ImageUrl='<%# Eval("ProductPhotoID", "ProductImage.ashx?ID={0}&full=true") %>'
       AlternateText='<% Eval("Name") %>' ImageAlign="Left" />
                <asp:Literal ID="DescriptionLiteral" runat="server"
                    Text='<%# Eval("Description") %>' />
                <br />
                <br />
                Color:
                <asp:Literal ID="ColorLiteral" runat="server"
                    Text='<%# Eval("Color") %>' />
                <br />
                Size:
                <asp:Literal ID="SizeLiteral" runat="server"
                    Text='<%# Eval("Size") %>' />
                <asp:Literal ID="SizeUnitLiteral" runat="server"
                    Text='<%# Eval("SizeUnitMeasureCode") %>'/>
                <br />
                Weight:
                <asp:Literal ID="WeightLiteral" runat="server"
                    Text='<%# Eval("Weight") %>' />
                <asp:Literal ID="WeightUnitLiteral" runat="server"
                    Text='<%# Eval("WeightUnitMeasureCode") %>'/>
            </p>
            </ItemTemplate>
        </asp:FormView>
    </div>
    </form>
</body>
</html>

VB.NET

This sample runs equally well in VB.NET if that is your language of choice; just replace <%@ Page Language="C#" %> with <%@ Page Language="VB " %>.

Example 7 uses a TextBox control, "SearchBox", as the parameter for the "SearchDataSource" control that gets its data from the adapter you created earlier. Another thing to note about this data source is that it has a "SelectCountMethod" that points to the "GetTotalRowCount" method, and "EnablePaging" is set to true, which enables the custom pagination you set up earlier in the stored procedure.

The Search button is currently posting back, but it will have a more directly active role when you add UpdatePanel controls to the page.

The search results are displayed in the "SearchResults" DataGrid control, which has a fairly minimal configuration. You allowed pagination, disabled the header, and enabled the select buttons. The grid is directly bound to the "SearchDataSource" control.

Finally, a details view under the form of a FormView control isthat will be showned when an item is selected in the results grid. The FormView control is bound to a DataSource that's using the table adapter you built in the previous example and whose "ProductID" parameter is bound to the SelectedValue property of the results grid. Figure 14 shows the result.

A search page using regular ASP.NET postbacks

Figure 14. A search page using regular ASP.NET postbacks

This page is pretty nice, especially considering that it's entirely declarative; but again, its flow is not as fluid as it could be. Furthermore, wouldn't it be nice if the details were fetched and displayed on demand as the user hovers over a product in the search results?

To achieve this result, you need to put the results pane into an UpdatePanel control. You also need to put the FormView into its own UpdatePanel control so it can be refreshed independently of the search results, and add progress notification so the user knows that something is happening when he selects an item. Example 8 shows the markup.

Example 8. Markup for a search page with UpdatePanel controls

<%@ Page Language="C#" %>

<!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 runat="server">
    <title>Search</title>
</head>
<body>
    <form id="form1" runat="server"
        defaultbutton="SearchButton" defaultfocus="SearchBox">
    <div>
        <asp:ScriptManager runat="server" ID="ScriptManager1"
 />

        <asp:ObjectDataSource ID="SearchDataSource" runat="server" EnablePaging="True"
        SelectCountMethod="GetTotalRowCount" SelectMethod="GetData"
        TypeName="AdventureProductsTableAdapters.PaginatedSearchProductTableAdapter">
            <SelectParameters>
                <asp:ControlParameter ControlID="SearchBox" Name="SearchCriteria"
                    PropertyName="Text" Type="String" DefaultValue="" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:ObjectDataSource ID="ProductDetailsDataSource" runat="server"
            SelectMethod="GetProductDetails"
            TypeName="AdventureProductsTableAdapters.ProductDetailsTableAdapter">
            <SelectParameters>
                <asp:Parameter Name="CultureID" Type="String" DefaultValue="en" />
                <asp:ControlParameter ControlID="SearchResults"
                    PropertyName="SelectedValue"
                    Name="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:TextBox ID="SearchBox" runat="server"></asp:TextBox>
        <asp:LinkButton ID="SearchButton" runat="server" Text="Search" />

        <asp:UpdatePanel runat="server" ID="ResultsPanel" UpdateMode="Conditional">
            <Triggers>
                <asp:AsyncPostBackTrigger ControlID="SearchButton"
                    EventName="Click" />
            </Triggers>
            <ContentTemplate>
                <asp:GridView ID="SearchResults" runat="server" AllowPaging="True"
                    PageSize="5" AutoGenerateColumns="False" DataKeyNames="ProductID"
                    DataSourceID="SearchDataSource" ShowHeader="False"
                    AutoGenerateSelectButton="true">
                    <Columns>
                        <asp:BoundField DataField="Name" />
                        <asp:BoundField DataField="Color" />
                        <asp:BoundField DataField="ListPrice"
                            DataFormatString="{0:c}" />
                    </Columns>
                </asp:GridView>
            </ContentTemplate>
        </asp:UpdatePanel>

        <asp:UpdateProgress runat="server" ID="Progress">
            <ProgressTemplate>
                Please wait...
            </ProgressTemplate>
        </asp:UpdateProgress>
        <asp:UpdatePanel runat="server" ID="DetailsUpdatePanel" UpdateMode="Always">
            <ContentTemplate>
              <asp:FormView runat="server" ID="DetailsView"
                    DataSourceID="ProductDetailsDataSource">
                    <ItemTemplate>
                    <h1>
                        <asp:Literal ID="NameLiteral" runat="server"
                            Text='<%# Eval("Name") %>' />:
                        <asp:Literal ID="PriceLiteral" runat="server"
                            Text='<%# Eval("ListPrice", "{0:c}") %>' />
                    </h1>
                    <h2><asp:Literal ID="CategoryLiteral" runat="server"
                            Text='<%# Eval("CategoryName") %>' /> /
                    <asp:Literal ID="SubCategoryLiteral" runat="server"
                            Text='<%# Eval("SubCategoryName") %>' /></h2>
                    <p>
                        <asp:Image ID="ProductImage" runat="server"
     ImageUrl='<%# Eval("ProductPhotoID", "ProductImage.ashx?ID={0}&full=true") %>'
     AlternateText='<% Eval("Name") %>' ImageAlign="Left" />
                        <asp:Literal ID="DescriptionLiteral" runat="server"
                            Text='<%# Eval("Description") %>' />
                        <br />
                        <br />
                        Color:
                        <asp:Literal ID="ColorLiteral" runat="server"
                            Text='<%# Eval("Color") %>' />
                        <br />
                        Size:
                        <asp:Literal ID="SizeLiteral" runat="server"
                            Text='<%# Eval("Size") %>' />
                        <asp:Literal ID="SizeUnitLiteral" runat="server"
                            Text='<%# Eval("SizeUnitMeasureCode") %>'/>
                        <br />
                        Weight:
                        <asp:Literal ID="WeightLiteral" runat="server"
                            Text='<%# Eval("Weight") %>' />
                        <asp:Literal ID="WeightUnitLiteral" runat="server"
                            Text='<%# Eval("WeightUnitMeasureCode") %>'/>
                    </p>
                    </ItemTemplate>
                </asp:FormView>
            </ContentTemplate>
        </asp:UpdatePanel>
    </div>
    </form>
</body>
</html>

Example 8 is more efficient because searches, page changes, and selections now happen without any flickering.

Navigation vs. application operations

Let's take a closer look at what we did here. The first UpdatePanel control, which contains a GridView control to display search results, uses the Click event of SearchButton as its trigger. Using a trigger here is optional. If you get rid of the trigger, pagination operations and selections will continue to be handled in fluid asynchronous postbacks, but searches, in contrast, will cause page postbacks because the search button lies outside the UpdatePanel. Still, without the trigger, the application will still work pretty much the same. There is an interesting side effect, however: search operations now will come under the control of the browser's navigation buttons.

This is important because in what we've seen until now, all UpdatePanel operations pretty much made the Back button useless. This may be the intended behavior in some cases, but there are cases, such as a search, in which you want to keep the navigation behavior users are used to.

This leads to a crucial point: the distinction between application operations, which should be handled by regular or asynchronous postbacks, and navigation operations, which should be handled by links or regular navigation. Application operations typically are operations on the current page that have side effects and should never enter the browser's history. Navigation, on the other hand, is a transition to a different page with a well-defined state that the user would expect to be able to bookmark and come back to without any other manipulation than choosing this bookmark or favorite. As such, navigation operations should definitely enter the browser's history. Bookmarks, as well as the back, forward and refresh buttons, should behave as expected.

Regular synchronous postbacks fall somewhere between these two situations: they enter the browser history, but if a user goes back to one of these states, the browser pops up an alert asking him if he wants to repost the form, which can be confusing to users, most of whom have no clue what an HTTP POST operation is in the first place.

The goal here is to allow the application developer to decide what should be treated as navigation and what shouldn't. If you want navigation, there are a few things you can do to ensure it is chosen.

The ASP.NET Ajax team is working on features that will distinguish between navigation and application operations and make navigation easier to create and manage. In the meantime, you can still do interesting things with your page to achieve this distinction.

Once you've removed the Search button as a trigger, you should take additional steps to make the navigation more natural. First, enable a switch on the form to use GET operations instead of POST:

<form id="form1" runat="server" defaultbutton="SearchButton"
    defaultfocus="SearchBox" method="get">

This allows the user to go back to previous search results by using the Back button without any nasty alerts. The nice thing is that the navigation really takes the user from one search query to the other, skipping all the selections and page changes he may have made in between (which may or may not be the results you're trying to achieve; simply modify where you're using UpdatePanel controls at your discretion).

It is now also possible to bookmark a search.

This looks nice, but switching the whole form to use the GET verb is a little too radical. After all, the rest of the form may contain a lot more than this simple page, and the rest of the page is likely to contain mostly controls whose semantics are closer to POST operations.

So ideally, you would apply this GET behavior only to navigation operations such as the Search button and leave the rest of the page to use POST.

To do this, revert your change and switch the form back to POST by removing the method attribute.

Then, modify the button so that it is responsible for navigating to the page instead of posting back. It should set the current query as a URL parameter. To do this, add the following JavaScript block to the head of the page:

<script type="text/javascript">
function doSearch() {
    window.location.href = '?search=' + encodeURIComponent($get('SearchBox').value);
    return false;
}
</script>

The doSearch function navigates to the current URL, replacing the query string with "?search=UserQuery", where UserQuery is the contents of the SearchBox control ($get('SearchBox').value). You use encodeURIComponent on the TextBox value so that any special characters are properly encoded and don't end up breaking the search.

The $get function is an alias that ASP.NET Ajax defines as document.getElementById.

You need to connect the Search button to this function. This is easily done by adding an OnClientClick property to the button:

<asp:LinkButton ID="SearchButton" runat="server" Text="Search"
    OnClientClick="return doSearch();" />

It's important that you return false from doSearch and OnClientClick to suppress the default behavior of the button, which is to post back.

Finally, you need to replace the ControlParameter of SearchDataSource with a QueryStringParameter that takes its value from the "search" query string field:

<asp:QueryStringParameter Name="SearchCriteria" QueryStringField="search"
    Type="String" DefaultValue="" />

You now have a fluidly operating application that makes an excellent distinction between navigation and operations. But you still have to implement the dynamic tool tip feature that we promised at the start of this section.

Implementing dynamic tooltips

The tool tip logic uses JavaScript to manage the visibility and position of the details view. (ASP.NET Ajax provides better ways to achieve such results, but these are beyond the scope of this Short Cut.) Furthermore, the script is fairly simple.

The tool tip consists of a DIV tag that surrounds your UpdateProgress and DetailsUpdatePanel. Copy the highlighted DIV tag just above the UpdateProgress control:

        </asp:GridView>
    </ContentTemplate>
</asp:UpdatePanel>

<div id="detailsFloatingPanel" onclick="hideDetails()">
    <asp:UpdateProgress runat="server" ID="Progress">

You can already guess that the panel will disappear when the user clicks on its contents (we'll describe the hideDetails function in a moment).

Close the DIV tag just after the closing tag for the DetailsUpdatePanel control:

                    </asp:FormView>
                </ContentTemplate>
            </asp:UpdatePanel>
        </div>
    </div>
    </form>
</body>
</html>

Next, we want to give some style to our new DIV tag so that it has fixed width, is absolutely positioned, has a black border, and is initially hidden. To do this, add the following stylesheet to the head of your page. (In this example, you place the stylesheet inline in the page to keep the example simple, but in a real application, you would place it in a separate CSS file.)

<style type="text/css">
    #detailsFloatingPanel {
        display: none;
        position: absolute;
        width: 400px;
        height: auto;
        background-color: White;
        border: solid 1px black;
    }
</style>

The style is associated to the element by ID (#detailsFloatingPanel).

Now you can add the display and hide logic to the script tag that you already added to the head:

function displayDetails(e, index) {
    // Display the tooltip panel.
    var detailsFloatingPanelStyle = $get("detailsFloatingPanel").style;
    detailsFloatingPanelStyle.display = "block";
    // If the panel to display is the current one, stop here.
    if (index == window.currentIndex) return;
    // Store the current index
    window.currentIndex = index;
    // Empty the details view panel.
    $get("<%= DetailsUpdatePanel.ClientID %>").innerHTML = "";
    // Position the tooltip panel at the mouse position.
    detailsFloatingPanelStyle.left = e.clientX + "px";
    detailsFloatingPanelStyle.top = e.clientY + "px";
    // Simulate a selection in the master view to update the details.
    __doPostBack("<%= SearchResults.ClientID %>", "Select$" + index);
}

function hideDetails() {
    $get("detailsFloatingPanel").style.display = "none";
}

The hideDetails function is fairly simple. It just sets the display style of the tool tip panel to "none", which makes it disappear.

The displayDetails function is a little more complex. It displays the tool tip panel by setting its display style to "block". It then tries to determine if the index of the product to display is the same as that of the product that's currently displayed. It does this because you don't want to requery the server and reposition the panel each time the user moves the mouse.

Then it empties the current details view panel so that the user sees only the progress UI and not the previous product when he hovers over a new product.

Once you've set the position of the panel to the position of the mouse cursor, you can simulate a selection operation on the search results' GridView control. This is arguably a little dirty as it relies on your knowledge of the implementation details of the GridView control's selection mechanisms. There are ways to achieve the same results without relying on this knowledge— for example, you can use a hidden form field to store the selected index, add some server-side logic to handle it, and translate the view index into a ProductID. This would also allow you to keep the EnableEventValidation page's flag set to true (which is not strictly necessary in this type of application but could be important in an application that manipulates more sensitive information). To keep the example relatively simple, we decided to implement this simpler solution and leave it as an exercise for the reader to implement a more complete one.

In several places, notice the inclusion of server code to determine the ClientID of an element. This is not strictly necessary in our example but would be if you wanted to use the same code in a content page using a Master Page because the client and server IDs would then be different. If you needed to encapsulate your client script into a separate file, you would need to get rid of these server blocks. This can be done by initializing a data structure with the variable information (the client IDs) and passing this structure to the script function. This is not very difficult but is beyond the scope of this Short Cut.

The final change you need to make to the page is to modify the search result grid so that the name of the product displays details when it is hovered over. First, remove the "select" links by setting AutoGenerateSelectButton to false on the GridView control. Then replace the "Name" BoundField with the templated field shown in the following snippet:

<asp:TemplateField>
    <ItemTemplate>
        <asp:HyperLink ID="Label1" runat="server"
            Text='<%# Eval("Name") %>' NavigateUrl="javascript:;"
            onmouseover='<%# String.Format("displayDetails(event, {0})",
                               ((GridViewRow)Container).RowIndex) %>'/>
    </ItemTemplate>
</asp:TemplateField>

Example 9 shows the full source code for the finished page.

Example 9. Complete markup for the searchable catalog with dynamic tooltips

<%@ Page Language="C#" EnableEventValidation="false" %>

<!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 runat="server">
    <title>Search</title>
    <style type="text/css">
        #detailsFloatingPanel {
            display: none;
            position: absolute;
            width: 400px;
            height: auto;
            background-color: White;
            border: solid 1px black;
        }

        .hidden {
            display:none;
        }
    </style>
    <script type="text/javascript">
    function doSearch() {
        window.location.href = '?search=' +
           encodeURIComponent($get('SearchBox').value);
        return false;
    }

    function displayDetails(e, index) {
        // Display the tooltip panel.
        var detailsFloatingPanelStyle = $get("detailsFloatingPanel").style;
        detailsFloatingPanelStyle.display = "block";
        // If the panel to display is the current one, stop here.
        if (index == window.currentIndex) return;
        // Store the current index
        window.currentIndex = index;
        // Empty the details view panel.
        $get("<%= DetailsUpdatePanel.ClientID %>").innerHTML = "";
        // Position the tooltip panel at the mouse position.
        detailsFloatingPanelStyle.left = e.clientX + "px";
        detailsFloatingPanelStyle.top = e.clientY + "px";
        // Simulate a selection in the master view to update the details.
        __doPostBack("<%= SearchResults.ClientID %>", "Select$" + index);
    }

    function hideDetails() {
        $get("detailsFloatingPanel").style.display = "none";
    }
    </script>
</head>
<body>
    <form id="form1" runat="server"
        defaultbutton="SearchButton" defaultfocus="SearchBox">
    <div>
        <asp:ScriptManager runat="server" ID="ScriptManager1"
 />

        <asp:ObjectDataSource ID="SearchDataSource" runat="server" EnablePaging="True"
        SelectCountMethod="GetTotalRowCount" SelectMethod="GetData"
        TypeName="AdventureProductsTableAdapters.PaginatedSearchProductTableAdapter">
            <SelectParameters>
                <asp:QueryStringParameter Name="SearchCriteria"
                    QueryStringField="search" Type="String" DefaultValue="" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:ObjectDataSource ID="ProductDetailsDataSource" runat="server"
        SelectMethod="GetProductDetails"
        TypeName="AdventureProductsTableAdapters.ProductDetailsTableAdapter">
            <SelectParameters>
                <asp:Parameter Name="CultureID" Type="String" DefaultValue="en" />
                <asp:ControlParameter ControlID="SearchResults"
                PropertyName="SelectedValue" Name="ProductID" Type="Int32" />
            </SelectParameters>
        </asp:ObjectDataSource>

        <asp:TextBox ID="SearchBox" runat="server"></asp:TextBox>
        <asp:LinkButton ID="SearchButton" runat="server" Text="Search"
            OnClientClick="return doSearch();" />

        <asp:UpdatePanel runat="server" ID="ResultsPanel" UpdateMode="Conditional">
            <ContentTemplate>
                <asp:GridView ID="SearchResults" runat="server" AllowPaging="True"
                    PageSize="5" AutoGenerateColumns="False"
                    DataKeyNames="ProductID" DataSourceID="SearchDataSource"
                    ShowHeader="False" AutoGenerateSelectButton="false">
                    <Columns>
                        <asp:TemplateField>
                            <ItemTemplate>
                                <asp:HyperLink ID="Label1" runat="server"
                                    Text='<%# Eval("Name") %>'
                                    NavigateUrl="javascript:;"
                                    onmouseover='<%#
                                      String.Format("displayDetails(event, {0})",
                                       ((GridViewRow)Container).RowIndex) %>'/>
                            </ItemTemplate>
                        </asp:TemplateField>
                        <asp:BoundField DataField="Color" />
                        <asp:BoundField DataField="ListPrice"
                            DataFormatString="{0:c}" />
                    </Columns>
                </asp:GridView>
            </ContentTemplate>
        </asp:UpdatePanel>

        <div id="detailsFloatingPanel" onclick="hideDetails()">
            <asp:UpdateProgress runat="server" ID="Progress">
                <ProgressTemplate>
                    Please wait...
                </ProgressTemplate>
            </asp:UpdateProgress>
            <asp:UpdatePanel runat="server" ID="DetailsUpdatePanel"
UpdateMode="Always">
                <ContentTemplate>
                    <asp:FormView runat="server" ID="DetailsView"
                        DataSourceID="ProductDetailsDataSource">
                        <ItemTemplate>
                        <h1>
                            <asp:Literal ID="NameLiteral" runat="server"
                                Text='<%# Eval("Name") %>' />:
                            <asp:Literal ID="PriceLiteral" runat="server"
                                Text='<%# Eval("ListPrice", "{0:c}") %>' />
                        </h1>
                        <h2><asp:Literal ID="CategoryLiteral" runat="server"
                                Text='<%# Eval("CategoryName") %>' /> /
                        <asp:Literal ID="SubCategoryLiteral" runat="server"
                            Text='<%# Eval("SubCategoryName") %>' /></h2>
                        <p>
                            <asp:Image ID="ProductImage" runat="server"
                                ImageUrl='<%# Eval("ProductPhotoID",
                                  "ProductImage.ashx?ID={0}&full=true") %>'
                                AlternateText='<% Eval("Name") %>'
                                ImageAlign="Left" />
                            <asp:Literal ID="DescriptionLiteral" runat="server"
                                Text='<%# Eval("Description") %>' />
                            <br />
                            <br />
                            Color:
                            <asp:Literal ID="ColorLiteral" runat="server"
                                Text='<%# Eval("Color") %>' />
                            <br />
                            Size:
                            <asp:Literal ID="SizeLiteral" runat="server"
                                Text='<%# Eval("Size") %>' />
                            <asp:Literal ID="SizeUnitLiteral" runat="server"
                                Text='<%# Eval("SizeUnitMeasureCode") %>'/>
                            <br />
                            Weight:
                            <asp:Literal ID="WeightLiteral" runat="server"
                                Text='<%# Eval("Weight") %>' />
                            <asp:Literal ID="WeightUnitLiteral" runat="server"
                                Text='<%# Eval("WeightUnitMeasureCode") %>'/>
                        </p>
                        </ItemTemplate>
                    </asp:FormView>
                </ContentTemplate>
            </asp:UpdatePanel>
        </div>
    </div>
    </form>
</body>
</html>

Figure 15 shows how the search page displays in a browser.

The product search engine displays on-demand tool tips that provide details about each product.

Figure 15. The product search engine displays on-demand tool tips that provide details about each product.

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

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