Professional ASP.NET 1.1 [Electronic resources] نسخه متنی

اینجــــا یک کتابخانه دیجیتالی است

با بیش از 100000 منبع الکترونیکی رایگان به زبان فارسی ، عربی و انگلیسی

Professional ASP.NET 1.1 [Electronic resources] - نسخه متنی

Alex Homeret

| نمايش فراداده ، افزودن یک نقد و بررسی
افزودن به کتابخانه شخصی
ارسال به دوستان
جستجو در متن کتاب
بیشتر
تنظیمات قلم

فونت

اندازه قلم

+ - پیش فرض

حالت نمایش

روز نیمروز شب
جستجو در لغت نامه
بیشتر
لیست موضوعات
توضیحات
افزودن یادداشت جدید






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=" %>

<%@ Register TagPrefix="IBA" TagName="Header" Src=" %>

<%@ Register TagPrefix="IBA" TagName="Categories"

src=" %>

<%@ Register TagPrefix="IBA" TagName="Special" src="

%>

<%@ Register TagPrefix="IBA" TagName="Footer" src=" %>


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=" %>

<%@ Register TagPrefix="IBA" TagName="Header" src=" %>

<%@ Register TagPrefix="IBA" TagName="Categories"

src=" %>

<%@ Register TagPrefix="IBA" TagName="Special" src=" %>

<%@ Register TagPrefix="IBA" TagName="Footer" src=" %>

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


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 (

) to display today's special product, so the page structure is slightly more complex than it would otherwise. See Figure 24-8:


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=" %>

<%@ 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 = + "/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 = +

"/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="/image/library/english/10209_saleTag1.gif" id="sale" runat="server" >

</td>

</tr>

</table>

<br>

<a id="OrderAnchor"

href=''

runat="server">

<img src="/image/library/english/10209_order.gif" width="55" height="15"

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=" %>

<%@ 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:


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 = " + 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:


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="/image/library/english/10209_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):


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 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="/image/library/english/10209_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:


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:


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:


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:



    The client is marked as authenticated by calling the

    RedirectFromLoginPage method of the

    FormsAuthentication object, which was discussed in Chapter 14.



    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.



    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:


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:


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:


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:


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:


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.

/ 244