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
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):
- Data Source Name: OnlinePhotoShopDB
- Description: Database for Online Photo Shop Project.
- Workbook: Point to the file shown in Figure 12.4.
- Read Only: Make sure this check box is unchecked in the Select Workbook dialog.
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,
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:
- Create a dedicated test login if it does not yet exist.
- Set all fields for this customer to known values.
- Commit the changes to the database.
- Create a new DataTable using the same test account.
- 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) |
|
Properties of RequiredFieldValidator1 |
|
---|---|
ControlToValidate |
|
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,
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
ID Name |
Control Type |
---|---|
HyperLink1 |
HyperLink (set NavigateUrl to ../Cart.aspx) |
|
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.
/// 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.
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: |
- Summarize the order: The total cost of all items in the shopping cart, shipping fees, and sales tax must be computed, and the appropriate labels on the form updated. This task is fairly trivial. We use the new methods of ShoppingCart to compute total cost and weight of an order and pass those values to the ShippingCostCalculator and TaxCalculator classes.
- Initialize all input fields: When the page is loaded, all input fields must be initialized with the profile stored in the customer database. The initialization of the input fields should be added to the existing method Page_Load(). We use the global CustomerDB object to query a customer record from the database.
- Update customer record: After the user has finished entering or updating the shipping and payment information, we must update the customer profile in the database. This can be added to the event handler of the order button, and again the global CustomerDB object can be used for the database transactions.
- Store order: The completed order, including shipping and payment information, must be finalized and stored in XML format to a dedicated directory. This is best done through a new method that saves the customer record as well as the items in the shopping cart in an XML file using the XmlTextWriter class provided by the .NET Framework.
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 +
/// 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. 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"] + "\
/// 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 +
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.