User Controls in IBuyAdventure .NET


User Controls in IBuyAdventure .NET

The user controls are defined at the top of each page using the @ Register directive. As discussed in Chapter 4, this allows you to associate a user control with an ASP.NET tag prefix (that is, an element namespace). When the ASP.NET runtime finds these special tags, it knows to create the appropriate user control and render the necessary output.

The @ Register directives common to each page are shown here:

  <%@ Page Language="C#" Inherits="IBuyAdventure.PageBase"   src="components/stdpage.cs" %>   <%@ Register TagPrefix="IBA" TagName="Header" Src="UserControl\Header.ascx" %>   <%@ Register TagPrefix="IBA" TagName="Categories"   src="UserControl\Categories.ascx" %>   <%@ Register TagPrefix="IBA" TagName="Special" src="UserControl\Special.ascx"   %>   <%@ Register TagPrefix="IBA" TagName="Footer" src="UserControl\Footer.ascx" %>  

The user controls that you have registered are then inserted into a page in the same way as seen in previous chapters:

  <IBA:Header id="Header" runat="server" />  

Most of the pages in the IBuyAdventure application have the same basic format, containing an HTML table. Therefore let's review the complete page code for default.aspx that shows all of the user controls being declared, the language, 'code behind' page directive, the default output cache directive, and the basic HTML page structure:

  <%@ Page Language="C#" Inherits="IBuyAdventure.PageBase"   src="components/stdpage.cs" %>   <%@ Register TagPrefix="IBA" TagName="Header" src="UserControl\Header.ascx" %>   <%@ Register TagPrefix="IBA" TagName="Categories"   src="UserControl\Categories.ascx" %>   <%@ Register TagPrefix="IBA" TagName="Special" src="UserControl\Special.ascx" %>   <%@ Register TagPrefix="IBA" TagName="Footer" src="UserControl\Footer.ascx" %>   <%@ OutputCache Duration="60" VaryByParam="*" %>     <script language="C#" runat="server" >     private String GetCustomerID() {   if (Context.User.Identity.Name != "")   return Context.User.Identity.Name;   else {   if (Session["AnonUID"] == null)   Session["AnonUID"] = Guid.NewGuid();   return Session["AnonUID"].ToString();   }   }     void Page_Load(Object sender, EventArgs e) {   if (Request.Params["Abandon"] == "1")   {   IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(   ConfigurationSettings.AppSettings["connectionString"]);   cart.ResetShoppingCart(GetCustomerID());   Session.Abandon();   FormsAuthentication.SignOut();   }   }   </script>     <html>   <head>   <title>IBuyAdventure Catalog</title>   </head>     <body background="images/back_sub.gif">   <form runat="server">   <font face="Verdana, Arial, Helvetica" size="2">     <table border="0" cellpadding="0" cellspacing="0">   <tr>   <td colspan="5">   <IBA:Header id="Header" runat="server"/>   </td>   </tr>   <tr>   <td colspan="3" align="left" valign="top">   <IBA:Categories id="Categories" runat="server"/>   </td>   <td>   &nbsp;&nbsp;   </td>   <td align="left" valign="top">   <h3>Welcome to IBuyAdventure!</h3>   <p>   <font face="Verdana, Arial, Helvetica" size="2">   You know the drill: Proper equipment for your climb leads to   a successful ascent. IBuyAdventure gear has been tested in   the most extreme environments on earth, from the 8,000-meter   peaks of the Himalayas to the sub-arctic giants of Alaska.   <p>   <IBA:Special runat="server"/>   <p>   IBuyAdventure has all the gear you need for any excursion,   from a day hike to a major expedition. Shop with us, set up   camp with us, and take our challenge. Join the IBuyAdventure   expedition!   <br>   <br>   <br>   <IBA:footer runat="server"/>   </font>   </td>   </tr>   </table>   </font>   </form>   </body>   </html>  

Although the appearance of the front page is fairly rich, the amount of code within the page is actually quite small because much of the HTML and code is encapsulated within the three user controls. default.aspx , like most pages, uses the @ OutputCache directive to specify that pages be cached for 60 seconds. This reduces database overhead, but you should consider the following issues:

  • Cached information is stored in memory so the amount of memory used by your application will be larger.

  • The same page will be cached multiple times if it has different query parameters, so you will have to allow for that increase in the working set.

  • If a page is cached, then all the output for that page is also cached. This might seem obvious, but it does mean that, for example, the AdRotator control for the Adventure Work application doesn't rotate as often as a normal site (the advert changes once every 60 seconds on the pages that use caching). If you wanted portions of the page to be cached, while the rest is rendered afresh every time, use fragment caching . Fragment caching works by caching the information in a user control. The .aspx page is rendered each time, but when the time comes to add the contents of the user control to a page, those contents are drawn from the cache.

Single Server-Side <form> Element

One important point to note about the default.aspx page is that it contains a single <form> element with the runat="server" attribute. This form contains the majority of the page's HTML. None of the user controls have a server side <form> element. This is important because <form> elements cannot be nested, so the single form must include all user control code. If you attempt to define a <form> element with the runat="server" attribute anywhere within the outer <form> element, this will generate an error.

Using C# for the User Controls and Code

The first line of all your pages in IBuyAdventure contains the @Page directive:

  <%@ Page Language="C#" Inherits="IBuyAdventure.PageBase"   src="components/stdpage.cs" %>  

This kind of directive was first seen in Chapter 4. The one used here informs the ASP.NET compiler of two key points about the pages:

  • All of the page code is written using C# (although you could just as easily have used other languages).

  • Each page uses 'code behind', and derives from the .NET class PageBase that provides common functionality.

The main motivation for using C# to write the IBuyAdventure application was to show that it really isn't so different from JScript and Visual Basic, and it is easy to read and understand. ASP.NET itself is written in C#, which indicates that it has a solid future ahead of it. Since all .NET languages compile down to MSIL before they are executed. It really doesn't matter which language the code is written in “use the one that you're most comfortable with.

The 'code behind' class specified using the Inherits and src attributes, causes the ASP.NET compiler to create a page that derives from the class PageBase rather than Page . The implementation of PageBase is very simple:

  using System;   using System.Collections;   using System.Web.UI;   using System.Web.Security;   using System.Configuration;     namespace IBuyAdventure   {   public class PageBase : Page   {   public string getConnStr() {   string dsn;   dsn = ConfigurationSettings.AppSettings["connectionString"];   return dsn;   }   }   }  

By deriving each page from this class the getConnStr function is made available within each of the ASP.NET pages. This function retrieves the database connection string from the web.config file, and is called in pages when constructing business objects that connect to the back-end data source. The web.config file is cached, so accessing it frequently in the pages should not have any detrimental effect on performance. Should you want to cache just the connection string you could use the data cache to hold it, only accessing the web.config file initially to retrieve the value when creating the cache entry:

  public String getConnStrCached() {   string connectionString;     // Check the Cache for the ConnectionString   connectionString = (string) Context.Cache["connectionString"];     // If the ConnectionString is not in the cache, fetch from Config.web   if (connectionString == null) {   connectionString =   ConfigurationSettings.AppSettings["connectionString"];     //store to cache   Cache["connectionString"] = connectionString;     }   return connectionString;   }  

One point to consider when using the data cache is that the values held within it will be updated if somebody changes the web.config file. ASP.NET automatically creates a new application domain and essentially restarts the web application to handle all new web requests when the web.config file is changed. As this results in a new data cache being created, the new connection string will be cached after the first call to getConnStrCached .

Important

As discussed in Chapter 13, applications settings should always be stored in the appsettings section of web.config . Values within that section are cached automatically.

However, should you decide to store application configuration in another location (maybe your own XML file on a central server) you can still invalidate the cache when your files change by creating a file dependency. This allows a cache item to be automatically flushed from the cache when a specific file changes:

 //store to cache  Cache.Insert("connectionString", connectionString,   new CacheDependency(Server.MapPath("\someserver\myconfig.xml")));  

File dependencies are just one of the cache dependency types ASP.NET supports. The other types supported include:

  • Scavenging :Flushing cache items based upon their usage, memory consumption, and rating

  • Expiration :Flushing cache items at a specific time or after a period of inactivity/access

  • File and key dependencies :Flushing cache items when either a file changes or another cache entry changes

    Note

    For more details about caching see Chapter 12.

The Specials User Control “ special.ascx

As you might have noticed, the default.aspx page seen earlier that implements the welcome page, actually uses an additional user control ( UserControl\Special.ascx ) to display today's special product, so the page structure is slightly more complex than it would otherwise . See Figure 24-8:

click to expand
Figure 24-8:

The product on offer is stored in the web.config file, making it easy for the site administrator to change the product displayed:

 <configuration>    <appSettings>       <add key="connectionString"          value="server=localhost;uid=sa;pwd=;database=IBuyAdventure" />  <add key="specialOffer" value="AW048-01" />  </appSettings>    ... </configuration> 

The Special user control reads this value in its Page_Load event handler, retrieving the product information using the ProductDB component, and then updates the page using server-side controls:

  ...   <%@ Control Inherits="IBuyAdventure.ControlBase"   src="../components/stdctrl.cs" %>   <%@ Import Namespace="System.Data" %>   <%@ Import Namespace="System.Configuration" %>     <script language="C#" runat="server">     void Page_Load(Object sender, EventArgs e) {   // Obtain today's special product.   IBuyAdventure.ProductsDB inventory =   new IBuyAdventure.ProductsDB(getConnStr());   string specialOffer;   specialOffer = ConfigurationSettings.AppSettings["specialOffer"];   DataSet specialDetails = inventory.GetProduct(specialOffer);   // Update UI with product details   ProductImageURL.Src = Context.Request.ApplicationPath + "/images/" +   (String) specialDetails.Tables[0].Rows[0]["ProductImageURL"];   ProductName.Text =   (String) specialDetails.Tables[0].Rows[0]["ProductName"];   ProductDescription.Text =   (String) specialDetails.Tables[0].Rows[0]["ProductDescription"];   ProductCode.Text =   (String) specialDetails.Tables[0].Rows[0]["ProductCode"];   UnitPrice.Text = String.Format("{0:C}",   specialDetails.Tables[0].Rows[0]["UnitPrice"]);   OrderAnchor.HRef = Request.ApplicationPath +   "/ShoppingCart.aspx?ProductCode=" +   (String) specialDetails.Tables[0].Rows[0]["ProductCode"];   if ( (int) specialDetails.Tables[0].Rows[0]["OnSale"] == 0 )   sale.Visible = false;   }     </script>     <table width="400" align=center border="1" cellpadding="0" cellspacing="0">   <tr bgcolor="#F7EFDE">   <td>   <font face="verdana" size="2" ><b> &nbsp;Today's Special! </b></font>   </td>   </tr>   <tr>   <td>   <table>   <tr>   <td align="left" valign="top" width="1"10>   &nbsp;<img id="ProductImageURL" runat="server">   </td>   <td align="left" valign="top">   <font size="2">   <b><asp:label id="ProductName" runat="server"/></b>,   <asp:label id="ProductDescription" runat="server"/><br><br>   <table>   <tr>   <td>   <font size="2">   Product Code: <asp:label id="ProductCode" runat="server"/><br>   Price: <b><asp:label id="UnitPrice" runat="server"/></b>   </font>   </td>   <td>   &nbsp;&nbsp;   <img src="../images/saleTag1.gif" id="sale" runat="server" >   </td>   </tr>   </table>   <br>   <a id="OrderAnchor"   href='../ShoppingCart.aspx?ProductCode=AW109-15'   runat="server">   <img src="images/order.gif" width="55" height="15"   alt="Order" border="0">   </a><br><br>   </font>   </td>   </tr>   </table>   </td>   </tr>   </table>  

The user control code is very simple and just updates the server controls with values from the DataSet returned by the function call to GetProduct . The String.Format function is called using a format string of {0:C} to show the UnitPrice as a numerical value that represents a localespecific currency amount.

The Categories User Controls “ categories.ascx

The product category list ( categories.ascx ), shown on the left hand side of most of the pages (it is not on the checkout or account pages), is dynamically built using the asp:DataList control and the ProductsDB business object. The DataSource property for the control is set in the Page_Load event:

  <%@ Control Inherits="IBuyAdventure.ControlBase"   src="../components/stdctrl.cs" %>   <%@ OutputCache Duration="60" VaryByParam="none" %>     <script language="C#" runat="server">     void Page_Load( Object sender, EventArgs e ) {   String dsn = getConnStr();   IBuyAdventure.ProductsDB inventory =   new IBuyAdventure.ProductsDB(getConnStr());   CategoryList.DataSource = inventory.GetProductCategories();   CategoryList.DataBind();   }     </script>  

The ItemTemplate for this data list control is detailed in the user control, and specifies the layout of the data:

  <asp:datalist id="CategoryList" border="0" runat="server">   <itemtemplate>   <tr>   <td valign="top">   <asp:image imageurl="/IBuyAdventure/images/bullet.gif"   alternatetext="bullet" runat="server" />   </td>   <td valign="top">   <font face="Verdana, Arial, Helvetica" size="2">   <asp:hyperlink   NavigateURL='<%# "/IBuyAdventure/catalogue.aspx?ProductType=" +   DataBinder.Eval( Container.DataItem, "ProductType" )%>'   Text='<%# DataBinder.Eval( Container.DataItem, "ProductType" )%>'   runat="server"/>   </font>   </td>   </tr>   </itemtemplate>   </asp:datalist>  

The asp:DataList control was first seen in Chapter 7. It is bound to a data source of items in a collection, and renders the ItemTemplate for each of them.

The asp:DataList control outputs an HTML table; so the ItemTemplate outputs a <tr> element containing two columns ( <td> elements), which ensure the table and page are correctly rendered. The first column contains an asp:image control that renders the small 'rock' bullet bitmap, the second column contains an asp:hyperlink control that has two fields ( NavigateURL and Text) . These fields are bound to the current row of the DataSet returned by the ProductDB business object.

The hyperlink rendered in ItemTemplate allows the user to view the product details for a specific category. The NavigateURL attribute is a calculated field consisting of a fixed URL ( /IBuyAdventure/catalogue.aspx ) and a dynamic query parameter, ProductType , whose value is set to equal the ProductType field in the current dataset row. Finally, the Text attribute is a simple attribute with its value also assigned to equal the ProductType field in the current dataset row.

The DataBinder class is used to retrieve the values stored in these properties. In case you are wondering, the DataBinder class is just a helper class provided by ASP.NET to keep the code simpler (fewer casts) and more readable, especially if you also need to format a property.

Alternatively, the app could also have directly accessed the current DataSet row and retrieved the ProductType value using the following code:

  ((DataRowView)Container.DataItem)["ProductType"].ToString()  

This format is slightly more complex, but may be preferable if you are happy using casts and prefer the style. One advantage of this code is that it is early-bound, so it will execute faster than the late bound DataBinder syntax.

When one of the product category hyperlinks is clicked, the ASP.NET page Catalogue.aspx is displayed:

click to expand
Figure 24-9:

As you can see in Figure 24-9, this page shows the products for the selected category by using the ProductType query string parameter in the Page_Load event to filter the results returned from the ProductDB component:

  void Page_Load(Object sender, EventArgs e) {   if (!IsPostBack) {   // Determine what product category has been specified and update   // section image   String productType = Request.Params["ProductType"];   CatalogueSectionImage.Src = "images/hd_" + productType + ".gif";     // User business object to fetch category products and databind   // it to a <asp:datalist> control   IBuyAdventure.ProductsDB inventory =   new IBuyAdventure.ProductsDB(getConnStr());   MyList.DataSource = inventory.GetProducts(productType);   MyList.DataBind();   }   }  
Note

A design issue here is that the application uses a hyperlink, and not a postback, to change the products shown.

Each of the products displayed for the selected category has a number of details:

  • Product name :The name of the product (Everglades, Rockies, and so on).

  • Product info :Facts about the product that will interest customers and help them make purchasing decisions (whether the item is waterproof , its color , and so on).

  • Product code :The unique ID for the product across the site.

  • Price :The price of the product.

  • On sale :If the price is reduced, the SALE PRICE image is displayed.

  • Order button :To add the product to the shopping basket the user clicks the Order image.

The main body of this page is also generated using an asp:DataList control, by setting the data source in the Page_Load event and using an ItemTemplate to control the rendering of each product. Unlike the category's User Control, the asp:datalist on this page takes advantage of the RepeatDirection and RepeatColumns attributes:

  <asp:datalist id="MyList" BorderWidth="0" RepeatDirection="vertical"   RepeatColumns="2" runat="server" OnItemDataBound="DataList_ItemBound">  

These attributes automatically perform the page layout for us, and make the application look professional. Your ItemTemplate will define a two-column table that contains the image in the first column, and the details in the second. The asp:DataList control then works out how to flow the rows “ so changing the page to use horizontal flowing is simply a matter of changing one attribute value:

  <asp:datalist id="MyList" BorderWidth="0" RepeatDirection="horizontal"   RepeatColumns="2" runat="server" OnItemDataBound="DataList_ItemBound">  

Now, the app shows a different layout of the items. See Figure 24-10:

click to expand
Figure 24-10:

Without the asp:DataList control providing this functionality, you would have to write considerable amount of code to achieve this.

Note

If you review the original ASP Adventure Works application, you will see it required around 100 lines of code in total!

When an item is on sale, the bitmap shown in Figure 24-11 is displayed:


Figure 24-11:

To determine whether or not this image is displayed, you need to handle the OnItemDataBound event of the asp:DataList object. This event is raised whenever an item in the datalist is created. To do this, set the Visible property of the saleItem server control (the img element) to false if the OnSale property is equal to zero. In order to get a reference to the saleItem control, use the FindControl method of the DataList item object. This method will search all the child controls of the DataList item being added to the page to get a reference to the saleItem control of that item:

  ...   void DataList_ItemCreated(Object sender , DataListItemEventArgs e ) {     DataRowView myRowView;   DataRow myRow;     myRowView = (DataRowView) e.Item.DataItem;   myRow = myRowView.Row;     if ( (int) myRow["OnSale"] == 0 )   e.Item.FindControl("saleItem").Visible = false;   ...     <img src="images/saleTag1.gif" id="saleItem" runat="server" />     ...  
Note

By setting the Visible property to false , the ASP.NET runtime does not render the control or any child controls “ as it would if the application used something like a <span> element to contain the image and text. This is a very powerful approach for preventing partial page generation, and is much cleaner than the inline if...then statements that classic ASP required you to write.

An advantage of this approach is that the code is somewhat cleaner and easier to maintain, but more importantly, any changes made by the code to controls that persist their state survive postbacks. As inline code is executed during the render phase of ASP.NET page, viewstate (state saved by the page and/or any child controls) has already been saved, so any changes made in inline code will not be round-tripped during a postback. The reason for using inline code in this chapter is to show that while ASP.NET applications can still make use of inline code, better (and sometimes mandatory) alternative approaches exist that allow you to maintain a much stronger separation of code from content.

Product Details

For each product shown in the catalogue.aspx page, the product name is rendered as a hyperlink. If a customer finds the product overview interesting, they can click the link to see more details about it (admittedly, there is not a great deal of extra detail in the sample application):

click to expand
Figure 24-12:

The additional information on this screen includes the date when the product was first introduced and a product rating assigned by the reviewer team at IbuyAdventure as you can see in Figure 24-12. The team always tests out the gear it sells first hand and assigns a rating. The Rating field in the Products table determines the rating bar shown for each product. The bar itself is generated using a custom server control written for IBuyAdventure. The sourcecode for this rating meter control is located in the controls directory, and should be easily understood if you have read Chapter 18. Like the components directory, the controls directory contains a make.bat file for building the control.

The control is registered and assigned an element name (tag prefix) at the top of the details page:

  <%@ Register TagPrefix="Wrox" Namespace="WroxControls" %>  

Although it only shows a single product, the details.aspx page still uses an asp:DataList control. The motivation for this was that future versions of IBuyAdventure could potentially allow multiple products to have their details viewed at the same time for product comparison purposes. The rating control is therefore declared within the ItemTemplate for the asp:DataList control as the Score property, using the field named Rating in the database table:

  <Wrox:RatingMeter runat="server"   Score=<%#(double)DataBinder.Eval(Container.DataItem, "Rating")%>   Votes="1"   MaxRating="5"   CellWidth="51"   CellHeight="10" />  

While the properties of the rating control may seem a little confusing at first, you should understand that it is a generic control that is suitable for many tasks . If you have seen the ASPToday.com article rating system it will probably make sense, but if not, consider the case where 200 people have rated a product, so you have 200 votes. For each vote a score between 0 and MaxRating is assigned, and the Score attribute reflects the overall average for all votes.

The rating control actually supports more functionality than is needed by the IBuyAdventure application, so set the Votes property to 1 , since only a single staff member rates the products. The idea is that future versions of the application will support customer ratings and reviews.

The functionality of the rating control will not be covered any further in this chapter, but here is a run down of the properties of this control:

Property

Description

CellWidth

The size of each cell within the bar.

MaxRating

The maximum rating that can be assigned by a single vote. This value determines the number of cells that the bar has.

CellHeight

The height of each cell.

Votes

The number of votes that have been cast.

Score

The current score or rating.

The Shopping Cart

When surfing through the site, a customer can add items to their shopping basket at any time by hitting the Order image button shown in Figure 24-13:


Figure 24-13:

This image button is inserted into the catalogue.aspx page as it is created, and clicking it results in the browser navigating to the ShoppingCart.aspx page:

 <asp:ImageButton runat="server" id="OrderButton"                  ImageUrl="images/order.gif"                  OnCommand="OrderButton_Command"                  CommandName="Order"/> 

Two additional pieces of code need to be added to the page to support this button. First, since this button will appear multiple times on the page, you will need to tie each instance to the specific product. This will allow the app to figure out which product the user selected when the button is clicked.

  void DataList_ItemBound(Object sender , DataListItemEventArgs e ) {     DataRowView myRowView;   DataRow myRow;   myRowView = (DataRowView) e.Item.DataItem;   myRow = myRowView.Row;   if ( (int) myRow["OnSale"] == 0 )   e.Item.FindControl("saleItem").Visible = false;   ((ImageButton)e.Item.FindControl("OrderButton")).CommandArgument =   myRow["ProductCode"].ToString();   ((ImageButton)e.Item.FindControl("OrderButton")).AlternateText =   "Click to order " + myRow["ProductName"];   }  

The DataList_ItemBound() method is called every time a product from the database is added to the DataList control. Set the CommandArgument property for the specific ImageButton to be the product code for the specific product. You will see later how this is used to select the proper product. Also, use the AlternateText property to set the tooltip that will appear when the user hovers the mouse over the order button.

Next , handle the postback event that occurs when users click an order button for the product they want to purchase. This will trigger a server roundtrip and fire the OrderButton_Command event:

  void OrderButton_Command(object sender, CommandEventArgs e) {     if (e.CommandName == "Order") {   String prodCode = e.CommandArgument.ToString();   Response.Redirect ("ShoppingCart.aspx?ProductCode=" + prodCode);   }   }  

When this event is handled, check to see what the CommandName of the button firing this event is. If it matches Order, then the app knows it was caused by the user pressing the order button for a specific product. The CommandArgument property will contain the product code for this product. Then you can redirect the execution to the ShoppingCart.aspx page and pass the product code as a parameter.

You can see the ShoppingCart.aspx page in Figure 24-14:

click to expand
Figure 24-14:

When the ShoppingCart.aspx page is being generated, the Page_Load event checks to see if a new product is being added to the cart, by looking for a Request parameter called ProductCode . This is added to the URL as a query string by the code in the Catalogue.aspx page (as shown earlier). The AddShoppingCartItem function of the CartDB object is then invoked to add it to the shopping cart for the current user.

The Page_Load event handler for the ShoppingCart.aspx page is shown here:

  void Page_Load(Object sender, EventArgs e) {     IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());   // If page is not being loaded in response to postback   if (Page.IsPostBack == false) {   // If a new product to add is specified, add it   // to the shopping cart   if (Request.Params["ProductCode"] != null) {   cart.AddShoppingCartItem(   GetCustomerID(), Request.Params["ProductCode"]);   }   PopulateShoppingCartList();   UpdateSelectedItemStatus();   }   }  

The ProductCode parameter is optional because the shopping cart can also be displayed by clicking on the shopping cart symbol shown in the navigation bar. If this is the method by which the page is accessed, then don't add any items to the shopping cart. The CustomerID function used here returns the unique ID for the current customer, which is then passed as a parameter to the AddShoppingCartItem function. If the customer has not registered and logged in, the ID returned by the CustomerID function is the current ASP.NET session ID; otherwise it is the current user name:

  String GetCustomerID() {   if (User.Identity.Name != "") {   return Context.User.Identity.Name;   }   else {   if (Session["AnonUID"] == null)   Session["AnonUID"] = Guid.NewGuid();   return Session["AnonUID"].ToString();   }   }  

The implementation of the AddShoppingCartItem() method of the CartDB business object is worth reviewing at this point, because it contains two interesting sections of code:

  public void AddShoppingCartItem(string customerName, string productCode) {     DataSet previousItem = GetShoppingCartItem(customerName, productCode);     if (previousItem.Tables[0].Rows.Count > 0) {   UpdateShoppingCartItem((int)   previousItem.Tables[0].Rows[0]["ShoppingCartID"],   ((int)previousItem.Tables[0].Rows[0]["Quantity"]) + 1);   }   else {     IBuyAdventure.ProductsDB products;   products = new IBuyAdventure.ProductsDB(m_ConnectionString);   DataSet productDetails = products.GetProduct(productCode);     String description =   (String) productDetails.Tables[0].Rows[0]["ProductDescription"];   String productName =   (String) productDetails.Tables[0].Rows[0]["ProductName"];   double unitPrice =   (double) productDetails.Tables[0].Rows[0]["UnitPrice"];   String insertStatement = "INSERT INTO ShoppingCarts (ProductCode, "   + "ProductName, Description, UnitPrice, CustomerName, "   + "Quantity) values ('" + productCode + "', @productName, "   + "@description, " + unitPrice + ", '" + customerName + "' , 1)";     SqlConnection myConnection = new SqlConnection(m_ConnectionString);   SqlCommand myCommand = new SqlCommand(insertStatement, myConnection);   myCommand.Parameters.Add(   new SqlParameter("@ProductName", SqlDbType.VarChar, 50));   myCommand.Parameters["@ProductName"].Value = productName ;     myCommand.Parameters.Add(   new SqlParameter("@description", SqlDbType.VarChar, 255));   myCommand.Parameters["@description"].Value = description;   myCommand.Connection.Open();   myCommand.ExecuteNonQuery();   myCommand.Connection.Close();   }   }  

The first interesting point about the code is that it checks whether the item is already in the shopping cart by calling GetShoppingCartItem , and if it does already exist, simply increases the quantity for that item and updates it in the database using the UpdateShoppingCartItem function.

The second interesting point comes about because the ADO.NET code that adds a new cart item uses the SqlCommand class. Since the IBuyAdventure product descriptions can contain single quotes, the app needs to ensure that any quotes within the description do not conflict with the quotes used to delimit the field. To do this the SqlCommand object is used to execute the query, making use of parameters in the SQL, like @description , to avoid any conflict. The values for the parameters are then specified using the Parameters collections of the SqlCommand object:

 myCommand.Parameters.Add(    new SqlParameter("@description", SqlDbType.VarChar, 255)); 

Once the SQL statement is built, the command object can be connected, the statement executed, and then disconnected:

 myCommand.Connection.Open(); myCommand.ExecuteNonQuery(); myCommand.Connection.Close(); 

Displaying the Shopping Cart and Changing an Order

The shopping cart allows customers to specify a quantity for each product in the cart, and displays the price per item, and total price for the quantity ordered. At any time, a customer can change the order quantity or remove one or more items from the cart by checking the Remove box and clicking Recalculate. An item will also be removed if the customer enters a quantity of zero.

To implement this functionality, use the asp:Repeater control. Implementing this functionality in straight ASP pages isn't an easy task, and requires significant code. In ASP.NET it is fairly simple.

The asp:Repeater control was used as the base for building the shopping cart as it doesn't need to use any of the built-in selection and editing functionality provided by the other list controls such as the asp:DataList and asp:DataGrid . All of the items are always checked and processed during a postback, and the cart contents (the dataset bound to the asp:Repeater control) is always generated during each postback.

The asp:Repeater control is also 'lookless' (it only generates the HTML element specified using templates), which fits in well with the design of the shopping cart page “ a complete table does not need to be generated by the control (the table's start and header rows are part of the static HTML).

The shopping cart data source is provided by the CartDB component, which is bound to the myList asp:repeater control:

  void PopulateShoppingCartList() {     IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());   DataSet ds = cart.GetShoppingCartItems(GetCustomerID());     MyList.DataSource = ds;   MyList.DataBind();   ...  

The HTML used to render the shopping cart, including the ItemTemplate rendered for each item in the MyList.DataSource is shown next, although some parts of the HTML page formatting (for example the font settings) have been removed to keep it short and easily readable:

  <table colspan="8" cellpadding="5" border="0" valign="top"> <tr valign="top">    <td align="center" bgcolor="#800000">Remove</td>    <td align="center" bgcolor="#800000">Product Code</td>    <td align="center" bgcolor="#800000">Product Name</td>    <td align="center" bgcolor="#800000" width="250">Description</td>    <td align="center" bgcolor="#800000">Quantity</td>    <td align="center" bgcolor="#800000">Unit Price</td>    <td align="center" bgcolor="#800000">Unit Total</td> </tr>      <asp:Repeater id="MyList" runat="server">         <itemtemplate>       <tr>       <td align="center" bgcolor="#f7efde">          <asp:checkbox id="Remove" runat="server" />       </td>       <td align="center" bgcolor="#f7efde">          <input id="ShoppingCartID" type="hidden"          value='<%#DataBinder.Eval(Container.DataItem,"ShoppingCartID", "{0:g}")%>'          runat="server" />          <%#DataBinder.Eval(Container.DataItem, "ProductCode")%>       </td>       <td align="center" bgcolor="#f7efde">          <%#DataBinder.Eval(Container.DataItem, "ProductName")%>       </td>       <td align="center" bgcolor="#f7efde">          <%#DataBinder.Eval(Container.DataItem, "Description")%>       </td>       <td align="center" bgcolor="#f7efde">          <asp:textbox id="Quantity"                   text='<%#DataBinder.Eval(Container.DataItem, "Quantity","{0:g}")%>'                   width="30"                   runat="server" />       </td>       <td align="center" bgcolor="#f7efde">          <asp:label id="UnitPrice" runat="server">             <%#DataBinder.Eval(Container.DataItem, "UnitPrice", "{0:C}")%>          </asp:label>       </td>       <td align="center" bgcolor="#f7efde">          <%# String.Format("{0:C}",             (((int)DataBinder.Eval(Container.DataItem, "Quantity"))             * ((double) DataBinder.Eval(Container.DataItem, "UnitPrice")) )) %>          </td>       </tr>    </itemtemplate>      </asp:Repeater>      <tr>    <td colspan="6"></td>    <td colspan="2" align="right">    Total is <%=String.Format(fTotal.ToString(), "{0:C}") %>    </td> </tr>      <tr>    <td colspan="8" align="right">       <asp:button text="Recalculate" OnClick="Recalculate_Click" runat="server" />       <asp:button text="Go To Checkout" OnClick="Checkout_Click" runat="server" />    </td> </tr>      </table>  

This code is similar to that seen earlier, so it should be easy to follow. The important point to note is that all the fields that need to be available when a postback occurs are marked with the id and runat="server" attributes.

When the customer causes a postback by pressing the Recalculate button, the ASP.NET page can access the Remove checkbox control, the database cart ID hidden field control, and the Quantity field control for each list item, and update the database accordingly .

For each row in the ShoppingCarts table for this customer, the asp:Repeater control will contain a list item containing these three controls, which can be programmatically accessed. Refer to Figure 24-15 to get a better understanding:

click to expand
Figure 24-15:

To associate each list item within the asp:Repeater control with a specific database cart item, a hidden field is used to store the unique ID for the entry:

  <input id="ShoppingCartID" type="hidden"        value='<%#DataBinder.Eval(                Container.DataItem, "ShoppingCartID", " {0:g}") %>'        runat="server">  

As discussed earlier, the contents of the shopping cart are always stored in the SQL Server table named ShoppingCarts , and manipulated using the business object named CartDB . To populate the shopping cart with items, the ASP.NET page invokes the PopulateShoppingCartList function. This occurs when the page is loaded for the first time (that is, when Page.PostBack is false ), and after each postback that leads to the database being modified “ items added, deleted, or changed. To retrieve the cart items and data bind the asp:Repeater control, this function uses the GetShoppingCartItems method of the CartDB object:

  void PopulateShoppingCartList() {     IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());     DataSet ds = cart.GetShoppingCartItems(GetCustomerID());     MyList.DataSource = ds;   MyList.DataBind();   ...  

Once the list is bound, the dataset is then enumerated to calculate the total value of the items within the cart:

  DataTable dt;   dt = ds.Tables[0];     int lIndex;   double UnitPrice;   int Quantity;     for ( lIndex =0; lIndex < dt.Rows.Count; lIndex++ ) {     UnitPrice = (double) dt.Rows[lIndex]["UnitPrice"];     Quantity = (int) dt.Rows[lIndex]["Quantity"];     if ( Quantity > 0 ) {   fTotal += UnitPrice * Quantity;   }   }   }  

The total stored in the fTotal parameter is defined as a Double earlier in the page definition:

  // Total for shopping basket   double fTotal = 0;  

and then referenced by inline code that executes just after the asp:Repeater control:

 ... </asp:repeater> <tr>    <td colspan="6"></td><td colspan="2" align="right">  Total is <%=String.Format("{0:C}", fTotal ) %>  </td> </tr> ... 

When customers change the order quantity for products in their cart, or mark items to be removed, they click the Recalculate button. This button was created using the asp:button control with its OnClick event wired up to the Recalculate_Click function:

  <asp:button text="Recalculate" OnClick="Recalculate_Click" runat="server" />  

The Recalculate_Click function updates the database based on the changes users made to the quantities , and the items they have added or deleted. It then retrieves the updated cart items from the database, rebinds the repeater control to the updated data set, and finally creates a status message informing the user how many items (if any) are currently in the cart. These functions are, in turn , delegated within the event handler to three different functions:

  void Recalculate_Click(Object sender, EventArgs e) {   // Update Shopping Cart   UpdateShoppingCartDatabase();   // Repopulate ShoppingCart List   PopulateShoppingCartList();   // Change status message   UpdateSelectedItemStatus();   }  

The UpdateShoppingCartDatabase method is called first in the event handler, when the postback data for the asp:Repeater control describing the cart, and any changes made, will be available. The function can therefore access this postback data and make any database updates and deletions that may be required. Next, calling PopulateShoppingCartList causes the shopping cart to be re-read from the database and bound to the asp:Repeater control. This will cause the page to render an updated view of the cart to the user.

To perform the required database updates, the UpdateShoppingCartDatabase function iterates through each of the list items (the rows) within the asp:Repeater control and checks each item to see if it should be deleted or modified:

  void UpdateShoppingCartDatabase() {     IBuyAdventure.ProductsDB inventory =   new IBuyAdventure.ProductsDB(getConnStr());   IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());   for (int i=0; i<MyList.Items.Count; i++) {   TextBox quantityTxt =   (TextBox) MyList.Items[i].FindControl("Quantity");   CheckBox remove =   (CheckBox) MyList.Items[i].FindControl("Remove");   HtmlInputHidden shoppingCartIDTxt =   (HtmlInputHidden) MyList.Items[i].FindControl("ShoppingCartID");     int Quantity = Int32.Parse(quantityTxt.Text);     if (remove.Checked == true  Quantity == 0)   cart.DeleteShoppingCartItem(   Int32.Parse(shoppingCartIDTxt.Value));   else {   cart.UpdateShoppingCartItem(   Int32.Parse(shoppingCartIDTxt.Value), Quantity );   }   }  

This code takes a brute-force approach by updating every item in the shopping cart that isn't marked for deletion. In a commercial application, consider having a hidden field that stores the original quantity and only updates items when the two quantity fields differ . This could potentially reduce database I/O considerably if you have users who keep changing their order quantities and deleting items. Another alternative would be to handle the OnChange events for the controls in the list, and only update the database when events are invoked.

Checkout Processing and Security

When customers are ready to commit to purchasing the goods that are currently in their shopping cart, they can click the Go to Checkout button in the shopping cart page, or click the shopping basket image located on the navigation bar. The security system used in IBuyAdventure takes advantage of forms based authentication (also called cookie-based security), as introduced in Chapter 14. When a customer hits any of the pages that require authentication, if they haven't already signed in, the page login.aspx is displayed. The login.aspx page is as shown in Figure 24-16:

click to expand
Figure 24-16:

The ASP.NET runtime knows to display this page if a user is not logged in because all pages that require authentication are located in a directory called SECURE . It contains a web.config file, which specifies that anonymous access is not allowed:

  <configuration>   <system.web>   <authorization>   <deny users="?" />   </authorization>   </system.web>   </configuration>  
Note

Remember that " ? " means 'anonymous users'.

Using a specific directory to contain secure items is a simple yet flexible way of implementing security in ASP.NET applications. When the ASP.NET runtime determines that an anonymous user is trying to access a page in a secure directory of the application, it knows which page to display because the web.config file located in the root directory has a cookie element with a loginurl attribute that specifies it:

  <configuration>   <system.web>   <authentication mode="Forms">   <forms name=".ibuyadventurecookie" loginUrl="login.aspx"   protection="All" timeout="60">   </forms>   </authentication>   <authorization>   <allow users="*" />   </authorization>   </system.web>   </configuration>  

This configuration basically says, if the .ibuyadventurecookie cookie is present and it has not been tampered with, the user has been authenticated and so can access secure directions, if authorized; if not present, redirect to the URL specified by loginurl .

Forms-Based Authentication in Web Farms

For forms-based authentication to work within a web farm environment, the decryptionkey attribute of the cookie element must be set, and not left blank or specified as the default value of autogenerate . The decryptionkey attribute should be set the same on all machines within the farm. The length of the string is 16 characters for DES encryption (56/64 bit), or 48 characters for Triple DES encryption (128 bit). If you do use the default value it will cause a different encryption string to be generated by each machine in the farm, and cause the session authentication to fail between different machines as a user moves between servers. If this happens a CryptographicException will be thrown and the user will be presented with a screen saying the data is bad, or could not be decoded.

The Login.aspx Page Event Handlers

The Login button on the login form is created using an asp:button control, which has the OnClick event wired up to the LoginBtn_Click event handler:

  ...   <td colspan="2" align="right">   <asp:button Text=" Login " OnClick="LoginBtn_Click" runat="server" />   </td>  

When the button is clicked, the LoginBtn_Click event handler is invoked. It validates users, and then redirects them to the original page. The validation and redirection code is shown here:

  void LoginBtn_Click(Object sender, EventArgs e) {   IBuyAdventure.UsersDB users = new IBuyAdventure.UsersDB(getConnStr());   IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());     if (users.ValidateLogin(UserName.Text, Password.Text)) {     cart.MigrateShoppingCartItems(Session.SessionID, UserName.Text);   FormsAuthentication.RedirectFromLoginPage(   UserName.Text, Persist.Checked);   }   else {   Message.Text =   "Login failed, please check your details and try again.";   }   }  

The code initially creates the two business objects that are required, using the 'code behind' function getConnStr to collect details of the data source to connect to. Once the UsersDB object is created, its ValidateLogin method is invoked to determine if the user credentials are OK (the user details are stored in the Account table rather than the web.config file). If the details are invalid, the Text property of the Message control is updated to show the error. If the login is successful, the following steps occur:

  1. The client is marked as authenticated by calling the RedirectFromLoginPage method of the FormsAuthentication object, which was discussed in Chapter 14.

  2. This causes the cookie named . ibuyadventurecookie to be sent back to the client, so from here on it indicates that the client has been authenticated.

  3. The user is redirected back to the page that initially caused the login form to be displayed.

If customers have previously registered, they can login via the Login page. This will then redirect them back to the original page that caused the Login page to be displayed. This redirection code is actually implemented by the Login page, and does require some extra code.

Handling Page Return Navigation During Authentication

When the ASP.NET runtime determines that a secure item has been accessed, it will redirect the user to the Login page, and include a query string parameter named ReturnURL . As the name suggests, this is the page that users will be redirected to once they are allowed access to it. When displaying the page, save this value, as it will be lost during the postbacks where the user is validated . The approach used is to store the value in a hidden field during the Page_Load event:

  void Page_Load(Object sender, EventArgs e)   {   // Store Return Url in Page State   if (Request.QueryString["ReturnUrl"] != null)   {   ReturnUrl.Value = Request.QueryString["ReturnUrl"];   ((HyperLink)RegisterUser).NavigateUrl =   "Register.aspx?ReturnUrl=" + ReturnUrl.Value;   }   }  

The hidden field is defined as part of the Login form, and includes the runat="server" attribute so that the app can programmatically access it in its event handlers:

  <input type="hidden" value="/advworks/default.aspx"   id="ReturnUrl" runat="server" />  
Note

The hidden field is given a default value, as it is possible for the user to go directly to the login page via the navigation bar. Without a default value, the redirection code that is executed after login would not work.

So, when customers click the Login button, you can validate their details and then redirect them to the page whose value is stored in the ReturnUrl hidden control.

First Time Customer “ Registration

If customers have not registered with the application before, they can click the Registration hyperlink, and will be presented with a user registration form to fill in. See Figure 24-17:

click to expand
Figure 24-17:
Note

The form is kept simple for this case study, and only asks for an e-mail address and password. In a commercial application, this form would probably include additional information such as the name and address of the customer.

As the registration page ( Register.aspx ) is opened from a hyperlink in the login page ( login.aspx ), ensure that the app passes on the ReturnUrl parameter, so that the registration page knows where to redirect users once they have completed the form. To do this, dynamically create the hyperlink in the registration form during the Page_Load event of the login page:

  ((HyperLink)RegisterUser).NavigateUrl =   "Register.aspx?ReturnUrl=" + ReturnUrl.Value;  

Also, make sure that the hyperlink is marked as a server control in the login.aspx page:

  ...   <font size="2">   <asp:HyperLink NavigateUrl="Register.aspx" id="RegisterUser" runat="server" />   Click Here to Register New Account   </asp:hyperlink>   </font>   ...  

Those of you with a keen eye will have spotted that customers can actually log in at any time by clicking the Sign In / Register hyperlink located in the page header. Once a user is successfully authenticated, this hyperlink changes to say Sign Out as shown in Figure 24-18:

click to expand
Figure 24-18:

The sign in or out code is implemented in the header user control ( UserControl/header.ascx ) where the Page_Load event handler dynamically changes the text of the signInOutMsg control, depending on the authentication state of the current user:

  <%@ Import Namespace="System.Web.Security" %>   <script language="C#" runat="server">     private void Page_Load( Object Sender, EventArgs e ) {   updateSignInOutMessage();   }     private void SignInOut( Object Sender, EventArgs e ) {     if ( Context.User.Identity.Name != "" ) {   IBuyAdventure.CartDB cart =   new IBuyAdventure.CartDB(   ConfigurationSettings.AppSettings["connectionString"]);   cart.ResetShoppingCart(GetCustomerID());   FormsAuthentication.SignOut();   Response.Redirect("/IBuyAdventure/default.aspx");   }   else {   Response.Redirect("/IBuyAdventure/login.aspx");   }   }   private void updateSignInOutMessage() {     if ( Context.User.Identity.Name != "" ) {   signInOutMsg.Text = "Sign Out (" + Context.User.Identity.Name+ ")";   }   else {   signInOutMsg.Text = "Sign In / Register";   }   }   </script>   ...  

The updateSignInOutMessage function actually updates the text, and the SignInOut method is called when the user clicks the sign in/out text. If a user is signing out, the CookieAuthentication.SignOut function is called to invalidate the authentication cookie. If signing in, the user is redirected to the login page.

The SignInOut code is wired up as part of the control declaration:

  ...   <td>   <asp:linkbutton style="font:8pt verdana" id="signInOutMsg"   runat="server" OnClick="SignInOut" />   </td>   ...  

Checkout Processing

Once a customer is authenticated, they are taken to the checkout page ( secure/checkout.aspx ), presented with their shopping list, and asked to confirm that the list is correct. Figure 24-19 illustrates the Checkout.aspx page:

click to expand
Figure 24-19:

The checkout page uses very similar code to the ShoppingCart.aspx page, except for the controls that allow the customer to remove items or edit the order quantities. If the customer confirms an order by pressing the Confirm Order button, a new database record is created for the order containing the date, and the total order value. Then the current shopping basket is cleared, and the customer is presented with a confirmation screen as shown in Figure 24-20:

click to expand
Figure 24-20:

The code invoked for confirming an order is as follows :

  void Confirm_Order(Object sender, EventArgs e) {     IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());   double totalOrderValue;     totalOrderValue = cart.GetOrderValueForCart(GetCustomerID());     IBuyAdventure.OrdersDB orders = new IBuyAdventure.OrdersDB(getConnStr());   orders.AddNewOrder(GetCustomerID(), DateTime.Now.ToString("G",   DateTimeFormatInfo.InvariantInfo), totalOrderValue );     cart.ResetShoppingCart( GetCustomerID() );   Response.Redirect("confirmed.aspx");   }  

The total value of the order is calculated using the GetOrderValueForCart function of the CartDB object. The customer name that is passed into this function, as returned by a call to GetCustomerID , will always be the name that the user entered when registering, as it is not possible to access this page without being authenticated.

Once the total value of the order has been calculated, the AddNewOrder function of the OrdersDB object is called to create an entry in the orders table. Since the app will be adding the date and time of the order to the database make sure that the date is in a known format “as the standard formatting routines take into account the locale of the server, the app may end up with a date format that SQL Server doesn't recognize.

To get around this, use an overloaded version of the ToString() method. Usually, this method takes no parameters, but in this case you will use two. The first specifies the general date and time format. The DateTimeFormatInfo.InvariantInfo is a static object that ensures that the date string will be formatted the same regardless of the locale of the server. Finally, the shopping cart contents are cleared from the database and the browser is redirected to the order confirmation screen.

Important

In a commercial application you would want to keep the contents of the shopping cart so you could actually process the order. As this is only a simple demonstration application, this is not implemented here.

Canceling the Order

If an order is canceled before it is completed, the current shopping cart is cleared and the customer is taken back to the IBuyAdventure home page ( default.aspx ). The code for canceling an order is shown here:

  void Cancel_Order(Object sender, EventArgs e) {     IBuyAdventure.ProductsDB inventory =   new IBuyAdventure.ProductsDB(getConnStr());   IBuyAdventure.CartDB cart = new IBuyAdventure.CartDB(getConnStr());   cart.ResetShoppingCart( GetCustomerID() );     Response.Redirect("/IBuyAdventure/default.aspx");   }  

Order History and Your Account

Customers can review their order history at any time by clicking the Your Account image located at the top of each page. When clicked, this page displays all the orders they have previously placed to date, showing the date when the order was created and the total value of the order as shown in Figure 24-21:

click to expand
Figure 24-21:

This page is generated using the asp:Repeater control, and simply shows the entries in the Orders table for the current customer. The PopulateOrderList function databinds the controls just as in previous pages:

  void PopulateOrderList() {     IBuyAdventure.OrdersDB orders = new IBuyAdventure.OrdersDB(getConnStr());     DataSet ds = orders.GetOrdersForCustomer(GetCustomerID());     MyList.DataSource = ds;   MyList.DataBind();     if ( MyList.Items.Count == 0 ) {   ClearButton.Visible = false;   MyList.Visible = false;   Status.Text = "No orders have been placed to date.";   }   }  

The last few lines of the code hide the Clear Order History button if there are no orders for the customer. If there are orders, then clicking this button invokes the ClearOrderHistory() function:

  void ClearOrderHistory(Object sender, EventArgs e) {     IBuyAdventure.OrdersDB orders = new IBuyAdventure.OrdersDB(getConnStr());   orders.DeleteOrdersForCustomer( GetCustomerID() );   PopulateOrderList();   }  

This code clears all orders for the customer by calling the DeletesOrdersForCustomer() function provided by the OrdersDB object.




Professional ASP. NET 1.1
Professional ASP.NET MVC 1.0 (Wrox Programmer to Programmer)
ISBN: 0470384611
EAN: 2147483647
Year: 2006
Pages: 243

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net