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