CHAPTER 10 ■ DEALING WITH CUSTOMER ORDERS
399
9. Switch OrdersAdmin.aspx to Source View and disable the view state for the
OrderDetailsAdmin.ascx instance. Change its name from OrderDetailsAdmin1 to
orderDetailsAdmin:
</asp:GridView>
<br />
<uc1:OrderDetailsAdmin EnableViewState="false"
id="orderDetailsAdmin" runat="server">
</uc1:OrderDetailsAdmin>
</span>
</asp:Content>
How It Works: OrderDetailsAdmin.ascx
Whew, you’ve written a lot of code for this control. The code itself isn’t complicated, but you had to deal with a lot
of user interface elements. The two important details to understand are as follows:
• You used the session to persist data about the selected order. This is read in the OrderDetailsAdmin
control to obtain the ID of the selected order.
• You used Page_PreRender instead of Page_Load, because it executed after the session gets the
chance to be updated in the parent form.
Because we talked about each method while writing the code, it should be pretty clear how the page works. Run it
now and play with the buttons to make sure everything works as it should.
Summary
We covered a lot of ground in this chapter. You implemented a system by which you can both
take orders and manually administer them.
You accomplished this in two separate stages. You added a Proceed to Checkout button
onto the shopping cart control to allow the visitor to order the products in the shopping cart.
You implemented a simple orders administration page, where the site administrator could
view and handle pending orders.
In addition, we looked at the use of validation controls and also, importantly, set the scene
for entirely automating the order system.
Because order data is now stored in the database, you can create various statistics and run
In this chapter, you’ll implement a dynamic recommendations system with both up-selling
and cross-selling strategies. This system has the advantage of not needing manual maintenance.
Because at this point BalloonShop retains what products were sold, you can now implement a
“customers who bought this product also bought . . .” feature.
Darie-Watson_4681C11.fm Page 401 Monday, September 19, 2005 10:02 AM
402
CHAPTER 11
■ MAKING PRODUCT RECOMMENDATIONS
Increasing Sales with Dynamic
Recommendations
In BalloonShop, you’ll implement the dynamic recommendations system in the visitor’s shopping
cart and in the product details page. After adding the new bits to your shop, the product details
page will contain the product recommendations list at the bottom of the page, as shown in
Figure 11-1.
Figure 11-1. The product details page with the dynamic recommendations system implemented
The shopping cart page gets a similar addition, as shown in Figure 11-2.
Darie-Watson_4681C11.fm Page 402 Monday, September 19, 2005 10:02 AM
CHAPTER 11 ■ MAKING PRODUCT RECOMMENDATIONS
403
Figure 11-2. The product details page with the dynamic recommendations system implemented
Implementing the Data Tier
Before writing any code, you first need to understand the logic you’ll implement for making
product recommendations. We’ll focus here on the logic of recommending products that were
ordered together with another specific product. Afterward, the recommendations for the shop-
ping cart page will function in a similar way, but will take more products into consideration.
So you need to find out what other products were bought by customers who also bought
the product for which you’re calculating the recommendations (in other words, determine
“customers who bought this product also bought . . .” information). Let’s develop the SQL logic
to achieve the list of product recommendations step by step.
Darie-Watson_4681C11.fm Page 403 Monday, September 19, 2005 10:02 AM
22
26
30
1
4
7
10
14
18
4
14
18
22
26
30
Darie-Watson_4681C11.fm Page 404 Monday, September 19, 2005 10:02 AM
CHAPTER 11 ■ MAKING PRODUCT RECOMMENDATIONS
405
Starting from this list of results, you need to get the products that are most frequently
bought along with this product. The first problem with this list of products is that it includes
the product with the ProductID of 4. To eliminate it from the list (because, of course, you can’t
put it in the recommendations list), you simply add one more rule to the WHERE clause:
SELECT od2.ProductID
FROM OrderDetail od1
JOIN OrderDetail od2 ON od1.OrderID = od2.OrderID
WHERE od1.ProductID = 4 and od2.ProductID != 4
Not surprisingly, you get a list of products that is similar to the previous one, except it
doesn’t contain the product with a ProductID of 4 anymore:
ProductID
This query now returns a list such as the following:
Darie-Watson_4681C11.fm Page 405 Monday, September 19, 2005 10:02 AM
8213592a117456a340854d18cee57603
406
CHAPTER 11
■ MAKING PRODUCT RECOMMENDATIONS
ProductID rank
14 3
18 3
22 2
26 2
30 2
1 2
7 2
10 2
If you don’t need the rank to be returned, you can rewrite this query by using the COUNT
aggregate function directly in the ORDER BY clause. You can also use the TOP keyword to specify
how many records you’re interested in. If you want the top five products of the list, this query
does the trick:
SELECT TOP 5 od2.ProductID
FROM OrderDetail od1
JOIN OrderDetail od2 ON od1.OrderID = od2.OrderID
WHERE od1.ProductID = 4 AND od2.ProductID != 4
GROUP BY od2.ProductID
ORDER BY COUNT(od2.ProductID) DESC
The results of this query are
ProductID
18
Alternatively, you might want to calculate the product recommendations only using data
from the orders that happened in the last n days. For this, you need an additional join with the
orders table, which contains the date_created field. The following query calculates product
recommendations based on orders placed in the past 30 days:
SELECT ProductID, Name
FROM Product
WHERE ProductID IN
(
SELECT TOP 5 od2.ProductID
FROM OrderDetail od1
JOIN OrderDetail od2 ON od1.OrderID = od2.OrderID
JOIN Orders ON od1.OrderID = Orders.OrderID
WHERE od1.ProductID = 4 AND od2.ProductID != 4
AND DATEDIFF(dd, Orders.DateCreated,GETDATE()) < 30
GROUP BY od2.ProductID
ORDER BY COUNT(od2.ProductID) DESC
)
We won’t use this trick in BalloonShop, but it’s worth keeping in mind as a possibility.
Adding Product Recommendations
Make sure you understand the data tier logic explained earlier, as you’ll implement it in the
GetProductRecommendations stored procedure. The only significant difference from the queries
shown earlier is that you’ll also ask for the product description, which will be truncated at a
specified number of characters.
The GetProductRecommendations stored procedure is called when displaying Product.aspx to
show what products were ordered together with the selected product. Add this stored procedure to
the BalloonShop database:
CREATE PROCEDURE GetProductRecommendations
(@ProductID INT,
@DescriptionLength INT)
AS
FROM Product
WHERE ProductID IN
(
Returns the products that were ordered together with @ProductID
SELECT TOP 5 ProductID
FROM OrderDetail
WHERE OrderID IN
(
Returns the orders that contain @ProductID
SELECT DISTINCT OrderID
FROM OrderDetail
WHERE ProductID = @ProductID
)
Must not include products that already exist in the visitor's cart
AND ProductID <> @ProductID
Group the ProductID so we can calculate the rank
GROUP BY ProductID
Order descending by rank
ORDER BY COUNT(ProductID) DESC
)
Darie-Watson_4681C11.fm Page 408 Monday, September 19, 2005 10:02 AM
CHAPTER 11 ■ MAKING PRODUCT RECOMMENDATIONS
409
Adding Shopping Cart Recommendations
The logic for showing shopping cart recommendations is very similar to what you did earlier,
except now you need to take into account all products that exist in the shopping cart, instead
of a single product. Add the following procedure to your BalloonShop database:
CREATE PROCEDURE GetShoppingCartRecommendations
(@CartID CHAR(36),
@DescriptionLength INT)
like this:
CREATE PROCEDURE GetShoppingCartRecommendations2
(@CartID CHAR(36),
@DescriptionLength INT)
AS
Darie-Watson_4681C11.fm Page 409 Monday, September 19, 2005 10:02 AM
410
CHAPTER 11
■ MAKING PRODUCT RECOMMENDATIONS
Returns the product recommendations
SELECT ProductID,
Name,
SUBSTRING(Description, 1, @DescriptionLength) + ' ' AS Description
FROM Product
WHERE ProductID IN
(
Returns the products that exist in a list of orders
SELECT TOP 5 ProductID
FROM OrderDetail
WHERE OrderID IN
(
Returns the orders that contain certain products
SELECT DISTINCT OrderID
FROM OrderDetail
WHERE ProductID IN
(
Returns the products in the specified shopping cart
SELECT ProductID
FROM ShoppingCart
WHERE CartID = @CartID
comm.CommandText = "GetProductRecommendations";
// create a new parameter
DbParameter param = comm.CreateParameter();
param.ParameterName = "@ProductID";
param.Value = productId;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// create a new parameter
param = comm.CreateParameter();
param.ParameterName = "@DescriptionLength";
param.Value = BalloonShopConfiguration.ProductDescriptionLength;
param.DbType = DbType.Int32;
comm.Parameters.Add(param);
// execute the stored procedure
return GenericDataAccess.ExecuteSelectCommand(comm);
}
Add this version of the GetRecommendations method to your ShoppingCartAccess class:
// gets product recommendations for the shopping cart
public static DataTable GetRecommendations()
{
// get a configured DbCommand object
DbCommand comm = GenericDataAccess.CreateCommand();
// set the stored procedure name
comm.CommandText = "GetShoppingCartRecommendations";
// create a new parameter
DbParameter param = comm.CreateParameter();
param.ParameterName = "@CartID";
param.Value = shoppingCartId;
param.DbType = DbType.String;
param.Size = 36;
font-size: 10px;
}
.RecommendationText
{
color: Black;
font-family: Verdana, Helvetica, sans-serif;
font-size: 10px;
}
.RecommendationLink
{
color: Black;
font-family: Verdana, Helvetica, sans-serif;
text-decoration: underline;
font-size: 10px;
}
a.RecommendationLink:hover
{
color: Red;
}
2. Add a new Web User Control named ProductRecommendations.ascx to your UserControls folder.
Darie-Watson_4681C11.fm Page 412 Monday, September 19, 2005 10:02 AM
8213592a117456a340854d18cee57603
CHAPTER 11 ■ MAKING PRODUCT RECOMMENDATIONS
413
3. Write this code in the Source View window of the control, representing a DataList showing the
product recommendations list:
<asp:Label ID="recommendationsHeader" runat="server"
CssClass="RecommendationHead" />
<asp:DataList ID="list" runat="server">
<ItemTemplate>
list.DataBind();
// display header
if (table.Rows.Count > 0)
recommendationsHeader.Text =
"Customers who bought this product also bought:";
else
recommendationsHeader.Text = "";
}
Darie-Watson_4681C11.fm Page 413 Monday, September 19, 2005 10:02 AM
414
CHAPTER 11
■ MAKING PRODUCT RECOMMENDATIONS
// If we're in ShoppingCart.aspx
else if (currentLocation == "~/ShoppingCart.aspx")
{
// get product recommendations
DataTable table;
// display recommendations
table = ShoppingCartAccess.GetRecommendations();
list.DataSource = table;
list.DataBind();
// display header
if (table.Rows.Count > 0)
recommendationsHeader.Text =
"Customers who bought these products also bought:";
else
recommendationsHeader.Text = "";
}
}
5. Open Product.aspx in Design View and drag ProductRecommendations.ascx from the Solution
little further. By cutting out PayPal from the ordering process, you can gain better control and
reduce overheads. This isn’t as complicated as you might think, but you must be careful to do
things right.
This chapter lays the groundwork for this task by implementing a customer account system.
To make e-commerce sites more user-friendly, details such as credit card numbers are
stored in a database so that visitors don’t have to retype this information each time they place
an order. The customer account system you’ll implement will do this and will include all the
web pages required for entering such details.
As well as implementing these web pages, you’ll need to take several other factors into
account. First, simply placing credit card numbers, expiry dates, and so on into a database in
plain text isn’t ideal. This method might expose this data to unscrupulous people with access
to the database. This could occur remotely or be perpetrated by individuals within your client’s
organization. Rather than enforcing a prohibitively restrictive access policy to such data, it’s
much easier to encrypt sensitive information and retrieve it programmatically when required.
You’ll create a security library to make this easier.
Secondly, secure communications are important because you’ll be capturing sensitive
information such as credit card details via the Web. You can’t just put a form up for people to
access via HTTP and allow them to send it to you because the data could be intercepted. You’ll
learn how to use SSL over HTTPS connections to solve this problem.
You’ll be taking the BalloonShop application to the point where you can move on and
implement your own backend order pipeline in the next chapters.
Handling Customer Accounts
You can handle customer account functionality in web sites in many ways. In general, however,
they share the following features:
• Customers log in via a login page or dialog box to get access to secured areas of the web site.
• Once logged in, the Web Application remembers the customer until the customer logs
out (either manually via a Log Out button or automatically if the session times out or a
server error occurs).
Darie-Watson_4681C12.fm Page 417 Monday, September 12, 2005 6:50 AM
418
stored on the hosting server or in the domain of the hosting server. This solution is great for
intranet sites, where users already have domain accounts, but is difficult to set up and maintain
for Internet sites, and is usually avoided in such cases. Alternatively, you could implement your
own custom system, which gives you the most flexibility at the cost of increased development time.
One important thing you’d have to do in a custom system, as mentioned in Chapter 9, is to
secure user passwords. It isn’t a good idea to store user passwords in your database in plain
text, because this information is a potential target for attack. Instead, you should store what is
known as the hash of the password. A hash is a unique string that represents the password, but
cannot be converted back into the password itself. To validate the password entered by the
user, you simply need to generate a hash for the password entered and compare it with the
hash stored in your database. If the hashes match, the passwords entered match as well, so you
can be sure that the customer is not an imposter. The ASP.NET forms authentication system
you’ll use in this chapter handles this side of things for you, and ensures that user passwords
are stored securely (and in a case-sensitive way, providing enhanced security). However, it’s
Darie-Watson_4681C12.fm Page 418 Monday, September 12, 2005 6:50 AM
CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS
419
still worth looking at as a general technique, and so the security library you’ll create shortly
includes hashing capabilities.
Hashing is a one-way system, but to store credit card details securely, you’ll need to use a
more advanced, bidirectional form of encryption. This enables you to store credit card details
securely, but get access to them when you need to; that is, when the customer pays for an order.
The specifics of implementing this scheme in your application include the following tasks:
• Adding a user profile schema to the application
• Modifying the site to allow customer accounts, including registration and detail editing
pages
•Modifying ShoppingCart.ascx, which will now redirect the user to a checkout page called
Checkout.aspx
The SecurityLib Classes
The two areas you’ve seen so far where security functionality is required are
Hashing
Hashing, as has already been noted, is a means by which a unique value can be obtained that
represents an object. In practice, this means doing the following:
1. Serialize the object being hashed into a byte array.
2. Hash the byte array, obtaining a new hashed byte array.
3. Convert the hashed byte array into the format required for storage.
For passwords this is simple because converting a string (which is an array of characters)
into a byte array is no problem. Converting the resultant hashed byte array into a string for
database storage and quick comparison is also simple.
The actual method used to convert the source byte array into a hashed byte array can vary.
The System.Security.Cryptography namespace in .NET contains several algorithms for hashing
and allows you to provide your own if necessary, although we won’t go into details of this here.
The two main hashing algorithms found in the .NET Framework are SHA1 (Secure Hash Algo-
rithm) and MD5 (Message Digest, another name for the hash code generated). SHA1 generates
a 160-bit hash (regardless of the size of the input data), whereas MD5 generates a 128-bit hash;
therefore, SHA1 is generally considered more secure (although slower) than MD5. The Framework
also contains other versions of the SHA1 hash algorithm that generate longer hashes, up to 512 bits,
as well as hash algorithms that work using a key (shared secret) and the data to be hashed.
In the SecurityLib implementation, you’ll use SHA1, although it’s easy to change this if
you require stronger security. You’ll see the code that achieves this in the PasswordHasher class
in the following exercise.
Exercise: Implementing the PasswordHasher Class
1. Create a new subdirectory in the App_Code directory of BalloonShop called SecurityLib.
2. Add a new class file called PasswordHasher.cs with code as follows:
using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography;
namespace SecurityLib
{
Enter your password again:<br />
<asp:TextBox ID="pwdBox2" runat="server" />
<br />
<asp:Button ID="processButton" runat="server" Text="Process"
OnClick="processButton_Click" />
<br />
<asp:Label ID="result" runat="server" />
</asp:Content>
5. Modify SecurityLibTester.aspx.cs as follows:
using System;
using System.Text;
using SecurityLib;
public partial class SecurityLibTester : System.Web.UI.Page
{
Darie-Watson_4681C12.fm Page 421 Monday, September 12, 2005 6:50 AM
422
CHAPTER 12
■ ADDING CUSTOMER ACCOUNTS
protected void processButton_Click(object sender, EventArgs e)
{
string hash1 = PasswordHasher.Hash(pwdBox1.Text);
string hash2 = PasswordHasher.Hash(pwdBox2.Text);
StringBuilder sb = new StringBuilder();
sb.Append("The hash of the first password is: ");
sb.Append(hash1);
sb.Append("<br />The hash of the second password is: ");
sb.Append(hash2);
if (hash1 == hash2)
passwordHash.Length);
All the hash algorithm classes in the .NET Framework use this ComputeHash method to get a hash from an input
array of bytes. To increase the size of the hash, you can replace the hasher with another one of these, for example:
public static class PasswordHasher
{
private static SHA512Managed hasher = new SHA512Managed();
}
This change would result in a 512-bit hash, which is probably a bit excessive in this sort of application!
The client page, SecurityLibTest.aspx, hashes two passwords and compares the result. The code is basic
enough to ignore for now, but it’s important to note that the generated hashes vary a great deal for even simple
changes to the input data, even just changes of case—one of the defining features of good hash generation.
Encryption
Encryption comes in many shapes and sizes and continues to be a hot topic. No definitive solution
to encrypting data exists, although plenty of advice can be given. In general, there are two
forms of encryption:
• Symmetric encryption: A single key is used both to encrypt and decrypt data.
• Asymmetric encryption: Separate keys are used to encrypt and decrypt data. The encryption
key is commonly known as the public key, and anyone can use it to encrypt information.
The decryption key is known as the private key, because it can only be used to decrypt
data that has been encrypted using the public key.
Darie-Watson_4681C12.fm Page 423 Monday, September 12, 2005 6:50 AM