Putting It All Together

We can now start implementing the checkout process for the online store, which involves creating a database template, a utility class that wraps database access, and finally the login and checkout forms.

12.6.1 Creating the Data Source

Let's begin the implementation by creating a new spreadsheet and entering the fields from Table 12.2 into the first row. Before saving the file as database.xls to a location on the Web server's hard drive, rename the first sheet Customers (see Figure 12.4).

Figure 12.4. Creating the Customer Table

Office Suite

If you don't have access to Microsoft Excel, you can download the free office suite OpenOffice at http://www.openoffice.org (also on the CD that accompanies this book). With the included Calc program you can create spreadsheets and save them in Excel format.

Next, we add the spreadsheet to the ODBC data sources using the ODBC Data Source Administrator (accessible via Control Panel | Administrative Tools). Choose the System DNS tab, and press the Add button. Then choose Microsoft Excel Driver and complete the ODBC Microsoft Excel Setup dialog (see Figure 12.5):

Figure 12.5. Adding an ODBC Data Source

Next, we change the security attributes of the file folder where the database is stored to allow Internet guests to modify the database. By default, only read access is granted, and adding any new users in our Online Photo Shop application would result in a rather misleading error message: "Operation must use an updateable query."

To change the folder access permissions, open Windows Explorer. If your computer is not registered in a domain, first click on Menu Tools | Folder Options and choose the View tab. Then uncheck the advanced setting Use Simple File Sharing. Otherwise, the Security tab in the file properties dialog is always hidden. Next, select the folder in Windows Explorer where the database is stored, and open its properties dialog. In the Security tab, add the Internet guest accounts (LOCALHOSTIUSR_LOCALHOST and LOCALHOSTIUSR_COMPTERNAME) with full control permissions to the users.

12.6.2 The Data Access Layer

In the design workflow earlier in this chapter, it was decided to implement a layer on top of the database access to allow the replacement of the Excel sheet by a different data source.

The CustomerDB Class

We begin with the implementation of the abstract class CustomerDB. In addition to defining the abstract method CreateDataAdapter it implements the public methods Login and NewUser. The Login method provides the application with an API function to verify the login credentials entered by the customer. It returns true if a record matching the e-mail address and password was found. NewUser first verifies that the e-mail address does not already exist in the customer database, and if no matching record was found it inserts a new row in the customer table. Listing 12.2 shows the implementation of the CustomerDB class.

Listing 12.2 CustomerDB.cs: Abstract Customer Database Representation

using System; using System.Data; using System.Data.Common; namespace OnlinePhotoShop { ///

/// Abstract class providing API functions to access the customer /// database. ///

public abstract class CustomerDB { abstract public DbDataAdapter CreateDataAdapter(string selectCondition); ///

/// Authenticates a customer. ///

///Customer's e-mail address. ///Secret password used for /// authentication. /// Returns true if the authentication was /// successful. public bool Login(string email, string password) { // find matching record // (e.g., WHERE Email='user@mail.com' AND Password= 'secret' ) DbDataAdapter dataAdapter = this.CreateDataAdapter( " WHERE Email = '" + email + "' AND Password = '" + password + "'"); // query customer DB DataTable customer = new DataTable(); dataAdapter.Fill(customer); if (customer.Rows.Count == 1) { // query successful return true; } // e-mail or password is incorrect return false; } ///

/// Adds a new customer to the database. ///

///Customer's e-mail address. ///Secret password used for /// authentication. /// Returns true if the customer was added to the /// database. public bool NewUser(string email, string password) { // see whether the e-mail already exists DbDataAdapter dataAdapter = this.CreateDataAdapter( " WHERE Email = '" + email + "'"); // query customer DB DataTable customer = new DataTable(); dataAdapter.Fill(customer); if (customer.Rows.Count != 0) { // account already created return false; } // insert new user into database string [] newCustomer = new string[2]; newCustomer[0] = email; newCustomer[1] = password; customer.Rows.Add(newCustomer); // commit changes to database dataAdapter.Update(customer); return true; } } }

The OdbcCustomerDB Class

To access our database in the form of an Excel sheet, we need an ODBC-specific implementation for the execute method. For this purpose we add a new class, OdbcCustomerDB, which overrides the abstract method CreateDataAdapter inherited from CustomerDB. This method must create a new OdbcDataAdapter object and set the commands for selecting, inserting, and updating a customer in the table. The connection string and database table name are accessed via class properties. For the Online Photo Shop application, the connection string is "DSN=OnlinePhotoShopDB" and the table name is "[Customers$]", referring to the first sheet in the Excel file. But instead of coding those numbers in the source code, we add them to the Web.config file in the section:

 

The get accessors for ConnectionString and TableName can now simply return the configuration setting. Listing 12.3 shows the implementation of this class.

Listing 12.3 OdbcCustomerDB.cs: ODBC-Specific Database Access

[View full width]

using System; using System.Data; using System.Data.Common; using System.Data.Odbc; using System.Configuration; namespace OnlinePhotoShop { ///

/// From CustomerDB-inherited class that creates data adapter /// for ODBC data sources. ///

public class OdbcCustomerDB : CustomerDB { ///

/// Returns the database connection string read from the /// application setting DBConnection. ///

protected string ConnectionString { get { return ConfigurationSettings.AppSettings["DBConnection"]; } } ///

/// Returns the table where the customer records are stored /// read from the application setting CustomerTable. ///

protected string TableName { get { return ConfigurationSettings.AppSettings["CustomerTable"]; } } ///

/// Creates a new data adapter for the customer database. ///

///Condition attached to the /// SELECT command. /// DbDataAdapter to customer database. public override DbDataAdapter CreateDataAdapter(string selectCondition) { OdbcConnection conn = new OdbcConnection(ConnectionString); OdbcDataAdapter dataAdapter = new OdbcDataAdapter(); // Select Command OdbcCommand cmdSelect = new OdbcCommand(); dataAdapter.SelectCommand = cmdSelect; cmdSelect.CommandText = "SELECT * FROM " + this.TableName + " " + selectCondition; cmdSelect.CommandType = CommandType.Text; cmdSelect.Connection = conn; //Update Command OdbcCommand cmdUpdate = new OdbcCommand(); dataAdapter.UpdateCommand = cmdUpdate; cmdUpdate.CommandText = "UPDATE [Customers$] SET " + "Name = ?,Address1 = ?,Address2 = ?,City = ?, " + "State = ?,Zip = ?,CCType = ?,CCNumber = ?," + "CCExpMonth = ?,CCExpYear = ? " + "WHERE Email = ?"; cmdUpdate.CommandType = CommandType.Text; cmdUpdate.Connection = conn; cmdUpdate.Parameters.Add(new OdbcParameter("Name", OdbcType.VarChar, 50, "Name")); cmdUpdate.Parameters.Add(new OdbcParameter("Address1", OdbcType.VarChar, 50, "Address1")); cmdUpdate.Parameters.Add(new OdbcParameter("Address2", OdbcType.VarChar, 50, "Address2")); cmdUpdate.Parameters.Add(new OdbcParameter("City", OdbcType.VarChar, 50, "City")); cmdUpdate.Parameters.Add(new OdbcParameter("State", OdbcType.VarChar, 2, "State")); cmdUpdate.Parameters.Add(new OdbcParameter("Zip", OdbcType.Int, 5, "Zip")); cmdUpdate.Parameters.Add(new OdbcParameter("CCType", OdbcType.VarChar, 10, "CCType")); cmdUpdate.Parameters.Add(new OdbcParameter("CCNumber", OdbcType.VarChar, 16, "CCNumber")); cmdUpdate.Parameters.Add(new OdbcParameter("CCExpMonth", OdbcType.Int, 2, "CCExpMonth")); cmdUpdate.Parameters.Add(new OdbcParameter("CCExpYear", OdbcType.Int, 4, "CCExpYear")); cmdUpdate.Parameters.Add(new OdbcParameter("Email", OdbcType.VarChar, 50, "Email")); //Insert Command OdbcCommand cmdInsert = new OdbcCommand(); dataAdapter.InsertCommand = cmdInsert; cmdInsert.CommandText = "INSERT INTO [Customers$] (Email, Password) VALUES (?,?)"; cmdInsert.CommandType = CommandType.Text; cmdInsert.Connection = conn; cmdInsert.Parameters.Add(new OdbcParameter("Email",OdbcType.VarChar,50,"Email")); cmdInsert.Parameters.Add(new OdbcParameter("Password",OdbcType.VarChar,50, "Password")); return dataAdapter; } } }

Testing Database Access

The implementation of the utility classes for database access is not complete until we add a unit test. As usual, we do this before continuing with the integration into the Web pages. In Chapter 11, we added the class UnitTest, which contains all unit test cases for Online Photo Shop. Now we add another test case, OdbcCustomerDBTest, to this class.

In a real-life scenario we would use a dedicated data source just for testing. But to keep things simple we will use a dedicated customer account for testing. The test procedure is defined as follows:

  1. Create a dedicated test login if it does not yet exist.
  2. Set all fields for this customer to known values.
  3. Commit the changes to the database.
  4. Create a new DataTable using the same test account.
  5. Check whether all fields of the test account have been set to the values assigned previously.

Listing 12.4 shows the implementation of these steps to test database access.

Listing 12.4 UnitTest.cs: Tester for OdbcCustomerDB Class

... using System.Data; using System.Data.Common; ... ///

/// Tests the OdbcCustomerDB class. ///

/// F:customer_login [Test] public void OdbcCustomerDBTest() { OdbcCustomerDB db = new OdbcCustomerDB(); // create account to test with db.NewUser("tester@unit.test", ".netACompleteDevelopmentCycle2003"); // log in Assertion.Assert(db.Login("tester@unit.test", ".netACompleteDevelopmentCycle2003")); // log in using wrong password Assertion.Assert(!db.Login("tester@unit.test", "wrongPassword")); // create new record DbDataAdapter dataAdapter = db.CreateDataAdapter(" WHERE Email = 'tester@unit.test'"); DataTable customer = new DataTable(); dataAdapter.Fill(customer); customer.Rows[0]["Name"] = "Name"; customer.Rows[0]["Address1"] = "Address1"; customer.Rows[0]["Address2"] = "Address2"; customer.Rows[0]["City"] = "City"; customer.Rows[0]["State"] = "ST"; customer.Rows[0]["Zip"] = "12345"; customer.Rows[0]["CCType"] = "CCType"; customer.Rows[0]["CCNumber"] = "CCNumber"; customer.Rows[0]["CCExpMonth"] = "12"; customer.Rows[0]["CCExpYear"] = "2003"; // update and read to record2 dataAdapter.Update(customer); DataTable customer2 = new DataTable(); dataAdapter.Fill(customer2); // check whether identical Assertion.AssertEquals(customer2.Rows[0]["Name"], "Name"); Assertion.AssertEquals(customer2.Rows[0]["Address1"], "Address1"); Assertion.AssertEquals(customer2.Rows[0]["Address2"], "Address2"); Assertion.AssertEquals(customer2.Rows[0]["City"], "City"); Assertion.AssertEquals(customer2.Rows[0]["State"], "ST"); Assertion.AssertEquals(customer2.Rows[0]["Zip"], "12345"); Assertion.AssertEquals(customer2.Rows[0]["CCType"], "CCType"); Assertion.AssertEquals(customer2.Rows[0]["CCNumber"], "CCNumber"); Assertion.AssertEquals(customer2.Rows[0]["CCExpMonth"], "12"); Assertion.AssertEquals(customer2.Rows[0]["CCExpYear"], "2003"); }

When executing the test case given in Listing 12.4 from the NUnit test runner application, an error is reported in opening the database. At first this seems to be surprising; when the test is called directly from code of the Online Photo Shop project itself, everything works fine. So it must be a configuration problem. Indeed, the Web.config file is used only when the application runs within IIS. We need to create a separate configuration file for the test runner. To do this, we create an XML file named OnlinePhotoShop.dll.config in the folder where OnlinePhotoShop.dll is located (…wwwrootOnlinePhotoShopin) and add to it the configuration settings for the connection string and table name:

Executing the test now should result in a failure-free run.

12.6.3 The Database Singleton

Now it is time to put the implemented and tested classes for the database access to work. Because we inherit a data-source-specific class from an abstract base class, the interfaces cannot just be declared static. Instead, an instantiation of a data-source-specific class such as OdbcCustomerDB is necessary to access the database. However, to minimize code changes when switching data sources later in the project, we should avoid instantiating classes of type OdbcCustomerDB or SqlCustomerDB directly wherever the database is accessed. Instead, we can use the Class Factory and Singleton design patterns to instantiate only a single object of, for example, OdbcCustomerDB per application, which returns a new data adapter object inherited from the data-provider-independent class DbDataAdapter. When the data provider needs to be changed, we simply replace the instantiation of this singleton.

Open the file Global.asax.cs and add the following lines to the Application_Start handler:

// add class through which the customer database is accessed Application.Add("CustomerDB", new OdbcCustomerDB());

An instance derived from CustomerDB is now accessible throughout the entire application via the global setting "CustomerDB", and if the data source changes, only one code line needs to be touched. You could go even further and store a configuration item that specifies which class to instantiate for the customer database.

12.6.4 The Login Page

During the design workflow of this iteration, we added a section to the Web.config file that defines the authorization for all pages located in the checkout folder. There, a link to Login.aspx is given, and we implement that functionality next.

Add a new form, Login.aspx, to the project. As with Browse.aspx and Cart.aspx, we first switch the page layout to FlowLayout in the property window of the form. Then drag and drop the needed elements according to Figure 12.1 and the property tables given next.

Properties of Hyperlink1

NavigateUrl

Cart.aspx

Text

Back to Shopping Cart

Properties of TextBox1

NavigateUrl

Cart.aspx

(ID)

Email

Properties of RequiredFieldValidator1

ControlToValidate

Email

ErrorMessage

Please enter your email.

Properties of TextBox2

TextMode

Password

(ID)

Password

Properties of RequiredFieldValidator2

ControlToValidate

Password

ErrorMessage

Please enter a password.

Properties of RadioButton1

Text

I am a returning customer.

Checked

True

GroupName

CustomerGroup

(ID)

ReturningCustomerButton

Properties of RadioButton2

Text

I am a new customer.

Checked

False

GroupName

CustomerGroup

(ID)

NewCustomerButton

Properties of Label1

Text

Invalid email or password!

Visible

False

ForeColor

Red

(ID)

ErrorLabel

Properties of Button1

Text

Login

(ID)

LoginButton

The required field validators as well as the Error label are used to inform the customer if login credentials have not been entered or are invalid. Figure 12.6 shows the design view of the login page.

Figure 12.6. Design of Login.aspx

For the Login button, an event handler is needed that validates the given credentials. The handler can easily be added by double-clicking the button. If the customer has been authenticated (either by validating the password or by adding a new user to the customer database), we issue a Forms Authentication ticket. For security reasons (see section 12.4) this ticket must be encrypted because it is stored in a cookie, which is transferred to the client. The cookie is recognized by IIS and allows the user to access pages that require authorization for a given time. Listing 12.5 shows the implementation of the Login button event handler. Before this code can be compiled, the namespace System.Web.Security must be added to the form at the beginning of the code file.

Listing 12.5 Login.aspx.cs: Code Behind the Login Page

[View full width]

using System; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Web.Security; namespace OnlinePhotoShop { ///

/// Summary description for Login. ///

public class Login : System.Web.UI.Page { protected System.Web.UI.WebControls.HyperLink HyperLink1; protected System.Web.UI.WebControls.TextBox Email; protected System.Web.UI.WebControls.TextBox Password; protected System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator1; protected System.Web.UI.WebControls.RadioButton ReturningCustomerButton; protected System.Web.UI.WebControls.RadioButton NewCustomerButton; protected System.Web.UI.WebControls.Button LoginButton; protected System.Web.UI.WebControls.Label ErrorLabel; protected System.Web.UI.WebControls.RequiredFieldValidator RequiredFieldValidator2; private void Page_Load(object sender, System.EventArgs e) { // Put user code to initialize the page here } #region Web Form Designer generated code override protected void OnInit(EventArgs e) { // // CODEGEN: This call is required by the ASP.NET Web Form Designer. // InitializeComponent(); base.OnInit(e); } ///

/// Required method for Designer support - do not modify /// the contents of this method with the code editor. ///

private void InitializeComponent() { this.LoginButton.Click += new System.EventHandler(this.LoginButton_Click); this.Load += new System.EventHandler(this.Page_Load); } #endregion private void LoginButton_Click(object sender, System.EventArgs e) { bool issueTicket = false; CustomerDB db = (CustomerDB) Application.Get("CustomerDB"); if (ReturningCustomerButton.Checked) { // verify login credentials issueTicket = db.Login(Email.Text, Password.Text); } else { // new customer issueTicket = db.NewUser(Email.Text, Password.Text); } if (issueTicket) { // issue new authentication ticket FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, Email.Text, System.DateTime.Now, System.DateTime.Now.AddMinutes(20), false, "Customer", FormsAuthentication.FormsCookiePath); // encrypt the ticket string encTicket = FormsAuthentication.Encrypt(ticket); // create the cookie Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket)); // redirect back to original URL Response.Redirect(FormsAuthentication.GetRedirectUrl(Email.Text, false)); } else { ErrorLabel.Visible = true; } } } }

As set in the Web.config file section, all users having the role of a customer are authorized to access files in the checkout folder. For Online Photo Shop this is the only role currently used, but in the future other roles (such as administrator) or different levels of memberships might be added. Because no roles are yet stored in the database, it has been hard-coded to Customer in the event handler shown in Listing 12.5. We pass the roles to the user data argument in the form's authorization ticket so that this information also gets encrypted and cannot be modified by a client. Next, we change the Application_AuthenticateRequest method in the Global.asax.cs file to change the current user from anonymous to one that has the customer role (see Listing 12.6).

Listing 12.6 Global.asax.cs: Application_AuthenticateRequest

using System.Web.Security; using System.Security.Principal; . . . protected void Application_AuthenticateRequest(Object sender, EventArgs e) { if (Request.IsAuthenticated) { // if authenticated, change the current user from anonymous FormsIdentity user = (FormsIdentity) User.Identity; FormsAuthenticationTicket auth = user.Ticket; // assume all user roles are passed via comma-separated list in UserData HttpContext.Current.User = new GenericPrincipal(user, auth.UserData.Split(',')); } }

12.6.5 The Checkout Form

The core functionality of this chapter will be visible through the checkout form, which summarizes the order and collects shipping as well as payment information. Add a new Web form Checkout.aspx to the project's checkout folder. You need to place the form into this folder so that it is located in an area with restricted access. Now set the page layout to FlowLayout and start the design of the form as shown in Figure 12.2. Table 12.3 summarizes the ID names and control types to be used in the design of this form.

The State DropDownList control must be set to automatic post-back so that sales tax and shipping can be immediately updated whenever the customer selects a different state. Figure 12.7 shows the completed design of the checkout form.

Figure 12.7. Design of Checkout.aspx

Table 12.3. Controls in Checkout.aspx

ID Name

Control Type

HyperLink1

HyperLink (set NavigateUrl to ../Cart.aspx)

Email

Label

Name

TextBox

Address1

TextBox

Address2

TextBox

State

DropDownList

Zip

TextBox

LastPayment

RadioButton (use Payment in GroupName)

NewPayment

RadioButton (use Payment in GroupName)

CCType

DropDownList

CCNumber

TextBox

CCExpMonth

TextBox

CCExpYear

TextBox

Order

Label

Tax

Label

Shipping

Label

Total

Label (highlight)

Button

Button

Tax and Shipping Cost

During the design workflow of the preceding iteration (Chapter 11), we developed a class diagram that requests a dedicated class for computing tax and shipping for a given order. Let's first add a new class, TaxCalculator, that computes the sales tax on an order. Because the business is based in New Jersey, the sales tax of 6% is collected from New Jersey customers only (see Listing 12.7).

Listing 12.7 TaxCalculator.cs

using System; namespace OnlinePhotoShop.checkout { ///

/// Computes the sales tax for a given order and state. ///

public class TaxCalculator { static public double Compute(double total, string state) { switch (state) { case "NJ": return total * 0.06; } return 0; } } }

During the analysis workflow earlier in this chapter, we established the need for only one shipping method based on a fixed amount of $1.25 plus $3.00 per pound. The implementation for this is fairly trivial and is shown in Listing 12.8.

Listing 12.8 ShippingCostCalculator.cs

using System; namespace OnlinePhotoShop.checkout { ///

/// Computes the shipping cost for a given weight. ///

public class ShippingCostCalculator { static public double Compute(int method, double weight, string state) { // right now ground shipping only, $1.25 + $3 per pound // (all states same) return (1.25 + (weight * 3)); } } }

Computing the Order Total

Computing the total cost as well as the total weight of an order is a useful addition to the existing ShoppingCart class. This information is needed to compute the shipping and applicable sales tax. Listing 12.9 shows the implementation for the two new methods added to the ShoppingCart class.

Listing 12.9 ShoppingCart.cs: The GetTotalOrder() and GetTotalWeight() Methods

[View full width]

///

/// Computes the total of all items in the shopping cart. ///

///Product catalog file. /// Total cost of all items in the shopping cart. public double GetTotalOrder(string productCatalog) { double total = 0; ProductParser parser = new ProductParser(productCatalog); IDictionaryEnumerator i = this.GetEnumerator(); while(i.MoveNext()) { ShoppingItem item = (ShoppingItem) i.Value; // get the product details System.Collections.Hashtable details = parser.OptionDetails(item.Product, item. Option); // add to total double price = System.Convert.ToDouble((string) details["price"]); total += (price * item.Quantity); } return total; } ///

/// Computes the total weight of all items in the shopping cart. ///

///Product catalog file. /// Total weight of all items in the shopping cart. public double GetTotalWeight(string productCatalog) { double total = 0; ProductParser parser = new ProductParser(productCatalog); IDictionaryEnumerator i = this.GetEnumerator(); while(i.MoveNext()) { ShoppingItem item = (ShoppingItem) i.Value; // get the product details System.Collections.Hashtable details = parser.OptionDetails(item.Product, item. Option); // add to total double weight = System.Convert.ToDouble((string) details["weight"]); total += (weight * item.Quantity); } return total; }

Do It Yourself

The methods we have added to the ShoppingCart class for computing the total cost and weight of all items placed in a shopping cart require new test cases on the unit test level. It is your responsibility to develop a new test case that verifies the extended ShoppingCart class. The test case will be added to the UnitTest class of the project.

Finalizing the Order

All groundwork for the checkout process is now finished, and we complete the implementation by adding the code behind the form, which must address four tasks:

Listing 12.10 shows the complete implementation for the code behind the checkout form.

Listing 12.10 Checkout.aspx.cs: Finalizing Orders

[View full width]

using System; using System; using System.Collections; using System.Data; using System.Data.Common; using System.Web; using System.Web.SessionState; using System.Web.UI; using System.Web.UI.WebControls; using System.Web.UI.HtmlControls; using System.Web.Security; using System.Configuration; using System.Xml; using System.IO; namespace OnlinePhotoShop.checkout { ///

/// Checkout form collecting shipping and payment information. ///

/// F:order_checkout /// F:checkout_shipping /// C:checkout_shipping_cont /// F:checkout_payment /// F:checkout_payment_method /// F:checkout_summarize public class Checkout : System.Web.UI.Page { protected System.Web.UI.WebControls.HyperLink HyperLink1; protected System.Web.UI.WebControls.Label Email; protected System.Web.UI.WebControls.TextBox Name; protected System.Web.UI.WebControls.TextBox Address1; protected System.Web.UI.WebControls.TextBox Address2; protected System.Web.UI.WebControls.TextBox City; protected System.Web.UI.WebControls.DropDownList State; protected System.Web.UI.WebControls.TextBox Zip; protected System.Web.UI.WebControls.RadioButton LastPayment; protected System.Web.UI.WebControls.RadioButton NewPayment; protected System.Web.UI.WebControls.DropDownList CCType; protected System.Web.UI.WebControls.TextBox CCNumber; protected System.Web.UI.WebControls.TextBox CCExpMonth; protected System.Web.UI.WebControls.TextBox CCExpYear; protected System.Web.UI.WebControls.Label Order; protected System.Web.UI.WebControls.Label Tax; protected System.Web.UI.WebControls.Label Shipping; protected System.Web.UI.WebControls.Button Button; protected System.Web.UI.WebControls.Label Total; private void Page_Load(object sender, System.EventArgs e) { if (!this.IsPostBack) { // get user id (e-mail) and retrieve customer record FormsIdentity user = (FormsIdentity) User.Identity; FormsAuthenticationTicket auth = user.Ticket; CustomerDB db = (CustomerDB) Application.Get("CustomerDB"); DbDataAdapter dataAdapter = db.CreateDataAdapter(" WHERE Email = '" + auth.Name + "'"); DataTable customer = new DataTable(); dataAdapter.Fill(customer); // set fields with values loaded from database Email.Text = (string) customer.Rows[0]["Email"]; Name.Text = (string) customer.Rows[0]["Name"]; Address1.Text = (string) customer.Rows[0]["Address1"]; Address2.Text = (string) customer.Rows[0]["Address2"]; City.Text = (string) customer.Rows[0]["City"]; // create list of states ArrayList states = new ArrayList(); states.Add("AZ"); states.Add("NJ"); states.Add("NY"); states.Add("TX"); State.DataSource = states; State.DataBind(); // select state from database for (int i = 0; i < states.Count; i++) if (((string)states[i]) == (string) customer.Rows[0]["State"]) State.SelectedIndex = i; Zip.Text = (string) customer.Rows[0]["Zip"]; // create credit card types and select last used one ArrayList cctypes = new ArrayList(); cctypes.Add("Visa"); cctypes.Add("Master"); CCType.DataSource = cctypes; CCType.DataBind(); if ((string) customer.Rows[0]["CCType"] == "Master") CCType.SelectedIndex = 1; else CCType.SelectedIndex = 0; CCNumber.Text = (string) customer.Rows[0]["CCNumber"]; CCExpMonth.Text = (string) customer.Rows[0]["CCExpMonth"]; CCExpYear.Text = (string) customer.Rows[0]["CCExpYear"]; if (CCNumber.Text.Length == 0) NewPayment.Checked = true; else LastPayment.Checked = true; // update the order summary this.UpdateSummary(); } // do not cache this page Response.Expires = -1; } ///

/// Computes order total, tax, and shipping and updates labels on Web page. ///

public void UpdateSummary() { // retrieve shopping cart information from session object ShoppingCart cart = (ShoppingCart) Session["ShoppingCart"]; if (cart == null) { cart = new ShoppingCart(Session.SessionID); Session["ShoppingCart"] = cart; } if (cart.Count == 0) { Response.Redirect(@"..Empty.aspx"); } // update order label double total = cart.GetTotalOrder(Server.MapPath(@"..products.xml")); Order.Text = total.ToString("C"); // compute tax double tax = TaxCalculator.Compute(total, State.SelectedValue); Tax.Text = tax.ToString("C"); // compute shipping double weight = cart.GetTotalWeight(Server.MapPath(@"..products.xml")); double shipping = ShippingCostCalculator.Compute(0, weight, State.SelectedValue); Shipping.Text = shipping.ToString("C"); // update total total += shipping + tax; Total.Text = total.ToString("C"); } ///

/// Called when the state changes. Updates the order summary. ///

/// /// private void State_SelectedIndexChanged(object sender, System.EventArgs e) { // Update summary. Shipping or tax might change. this.UpdateSummary(); } ///

/// Creates a new order based on the items in the shopping cart and the given customer record. /// After the order is saved the items in the shopping cart are removed. ///

///Customer record. private void SaveOrder(DataTable customer) { ShoppingCart cart = (ShoppingCart) Session["ShoppingCart"]; if (cart != null) { // Get path for a temporary file string file = (string) ConfigurationSettings.AppSettings["OrderStore"] + "\ order_" + Session.SessionID.ToString() + ".xml"; // create a temp XML file XmlTextWriter writer = new XmlTextWriter(File.CreateText(file)); writer.Formatting = Formatting.Indented; writer.WriteStartDocument(); writer.WriteStartElement("doc"); writer.WriteStartElement("customer"); writer.WriteElementString("Email", (string) customer.Rows[0]["Email"]); writer.WriteElementString("Name", (string) customer.Rows[0]["Name"]); writer.WriteElementString("Address1", (string) customer.Rows[0]["Address1"]); writer.WriteElementString("Address2", (string) customer.Rows[0]["Address2"]); writer.WriteElementString("City", (string) customer.Rows[0]["City"]); writer.WriteElementString("State", (string) customer.Rows[0]["State"]); writer.WriteElementString("Zip", (string) customer.Rows[0]["Zip"]); writer.WriteElementString("CCType", (string) customer.Rows[0]["CCType"]); writer.WriteElementString("CCNumber", (string) customer.Rows[0]["CCNumber"]); writer.WriteElementString("CCExpMonth", (string) customer.Rows[0]["CCExpMonth"]); writer.WriteElementString("CCExpYear", (string) customer.Rows[0]["CCExpYear"]); writer.WriteStartElement("Charged"); writer.WriteAttributeString("Order", Order.Text); writer.WriteAttributeString("Tax", Tax.Text); writer.WriteAttributeString("Shipping", Shipping.Text); writer.WriteAttributeString("Total", Total.Text); writer.WriteEndElement(); // charged writer.WriteEndElement(); // customer writer.WriteStartElement("order"); IDictionaryEnumerator i = cart.GetEnumerator(); while(i.MoveNext()) { ShoppingItem item = (ShoppingItem) i.Value; writer.WriteStartElement("item"); writer.WriteAttributeString("product", item.Product); writer.WriteAttributeString("option", item.Option); writer.WriteAttributeString("quantity", item.Quantity.ToString()); writer.WriteAttributeString("image", item.ServerPath); writer.WriteEndElement(); // item } writer.WriteEndElement(); // order writer.WriteEndElement(); // doc writer.WriteEndDocument(); writer.Close(); // delete all shopping cart items cart.Clear(); } } #region Web Form Designer generated code override protected void OnInit(EventArgs e) { // // CODEGEN: This call is required by the ASP.NET Web Form Designer. // InitializeComponent(); base.OnInit(e); } ///

/// Required method for Designer support - do not modify /// the contents of this method with the code editor. ///

private void InitializeComponent() { this.Button.Click += new System.EventHandler(this.Button_Click); this.Load += new System.EventHandler(this.Page_Load); } #endregion ///

/// Called when the place order button is pressed. Updates the customer record and /// saves the order. ///

/// /// private void Button_Click(object sender, System.EventArgs e) { // get user id (e-mail) and retrieve customer record FormsIdentity user = (FormsIdentity) User.Identity; FormsAuthenticationTicket auth = user.Ticket; CustomerDB db = (CustomerDB) Application.Get("CustomerDB"); DbDataAdapter dataAdapter = db.CreateDataAdapter(" WHERE Email = '" + auth.Name + "'"); DataTable customer = new DataTable(); dataAdapter.Fill(customer); // update fields customer.Rows[0]["Name"] = Name.Text; customer.Rows[0]["Address1"] = Address1.Text; customer.Rows[0]["Address2"] = Address2.Text; customer.Rows[0]["City"] = City.Text; customer.Rows[0]["State"] = State.SelectedValue; customer.Rows[0]["Zip"] = Zip.Text; // update payment if necessary if (NewPayment.Checked) { customer.Rows[0]["CCType"] = CCType.SelectedValue; customer.Rows[0]["CCNumber"] = CCNumber.Text; customer.Rows[0]["CCExpMonth"] = CCExpMonth.Text; customer.Rows[0]["CCExpYear"] = CCExpYear.Text; } // store back into database dataAdapter.Update(customer); // now save the order this.SaveOrder(customer); // say thank you Response.Redirect("Completed.aspx"); } } }

As you can see from the code in Listing 12.10, the method UpdateSummary() redirects the request to the page Empty.aspx if the shopping cart does not contain any items. This page must be added to the project and should display an error message and provide a link that takes the customer back to the product browser. If a customer clicks on checkout with an empty shopping cart, it usually is not a case of disturbed customers but disabled cookies in Internet Explorer. Providing more information and help for the customer on how to enable cookies is definitely a good idea, too.

The other redirection added to the code takes place after the order has been saved and the shopping cart has been emptied. This page can provide some feedback that the order has been received. It could also display contact information for the customer service department and instructions on how to track an order if this service is provided. Add a form Completed.aspx to the project under the checkout directory, which is called for every completed order.

Finally, we add the new configuration parameter for the directory where the completed orders are stored. Open the Web.config file, and add the directory to the appSettings section:

 

Of course, this directory must also be created before we test the application.

Категории