The problem with most code samples in books is that they are way too simple. When building a full application, the world is often much more complex than the simple world that the book describes.
In this chapter, we build a complete ASP.NET application from start to finish. We build an entire online store with the ASP.NET Framework.
This chapter has several goals. The first is to discuss the issues that I encountered while building the application. Hard decisions had to be faced. Trade-offs were made.
Second, this book covers a lot of material. The ASP.NET 2.0 Framework includes an overwhelming number of new features (a fact which is both cool and scary). This chapter draws together many of the new ASP.NET 2.0 technologies discussed separately in previous chapters and shows you how you can apply these technologies in the context of a real-world application.
Finally, an important goal of this chapter is to provide you with a functioning application that you can use as a starting point for your projects. All the code for the e-commerce application is included on the CD that accompanies this book in both a Visual Basic .NET and C# version. If you need to build an e-commerce application, you can take advantage of this code to save yourself a significant amount of time.
In this chapter, we build the ASP.NET Beer Store. The e-commerce application is loaded with sample data that represents beer product information (see Figure 34.1).
The sample images used for the ASP.NET Beer Store were generously provided by Nathan Wiger and Corey Gray, the world-renowned experts on all excellent forms of beer. Visit their website at www.BeerLabels.com.
Before we get into the technical details of how the application works, I want to provide you with an overview of the different parts of the application. This application has two halves: The half that the world sees and the half that the store administrators see.
The public half of the ASP.NET Beer Store consists of the following ASP.NET pages:
Default.aspx
—. The home page of the ASP.NET Beer Store. This page displays a list of featured products.
Products.aspx
—. This page displays a list of products contained in a particular product category. If the category contains sub-categories, the sub-categories are also displayed.
ProductDetails.aspx
—. This page displays details for a particular product. It also contains a link for adding a product to a shopping cart.
ShoppingCart.aspx
—. This page displays a customer shopping cart. Customers can use this page to edit the items in their shopping carts.
CheckOutDefault.aspx
—. This page enables customers to enter billing and shipping information and purchase the products in their shopping carts.
CheckOutConfirmation.aspx
—. This page displays an order confirmation number after a customer has placed an order.
ContactInfo.aspx
—. This page displays contact information for the store.
Login.aspx
—. This page enables an existing customer to log in or a new customer to register.
PasswordReminder.aspx
—. This page enables customers to reset their passwords when they have forgotten their original passwords.
When you request the home page of the application, a list of featured products is displayed. Every page in the application also displays a list of product categories in a menu in the left column.
If you navigate to a product category, then you arrive at the Products.aspx
page. This page displays a list of products contained in the category in a DataList
control (see Figure 34.2). Next to each product description, a View Details link is rendered. If you click the View Details link, then you arrive at the ProductDetails.aspx
page.
The ProductDetails.aspx
page displays information on a particular product. You can click the Add to Cart link to add the product to your shopping cart.
You can view the contents of your shopping cart by clicking the Shopping Cart menu link that appears at the top of any page. The shopping cart is displayed by the ShoppingCart.aspx
page. This page enables you to remove items from the shopping cart. It also includes a Check Out link (see Figure 34.3).
If you click the Check Out link, and you are not authenticated, then you arrive at the Login.aspx
page. This page enables existing customers to log in and new users to register. After you log in or register, you are redirected to the CheckOutDefault.aspx
page (see Figure 34.4).
The CheckOutDefault.aspx
page contains a form that enables a customer to enter credit card information, billing address, and shipping address. When the customer submits the form, the shopping cart associated with the customer is converted into a new product order.
The private section of the application is no less important. A store manager uses this section to list new products and view customer orders. The private half of the ASP.NET Beer Store consists of the following pages:
ManageDefault.aspx
—. Contains a list of links to other pages in the store management section.
ManageCategoriesDefault.aspx
—. This page enables store managers to create new product categories and edit existing product categories.
ManageProductsDefault.aspx
—. This page enables store managers to list new products and edit existing products.
ManageOrdersDefault.aspx
—. This page enables store managers to view customer product orders.
When the ASP.NET Beer Store first starts, an administrator role and user is created automatically. The role is named StoreAdmins
and the user is named Admin
. The Admin user has the password secret.
If you login with the Admin account, then an additional menu item labeled Manage appears at the top of every page. If you click the Manage link, then you are brought to the ManageDefault.aspx
page. This page displays a menu of management options.
Make sure that you modify the Admin password. Everyone who reads this book knows that the Admin password defaults to secret. You can modify the Admin password by opening the Web Site Administration Tool when the application is loaded in Visual Web Developer. Launch this tool by selecting the menu option Website, ASP.NET Configuration.
If you navigate to the ManageCategoriesDefault.aspx
page, then you can add, delete, and edit product categories. The hierarchy of current product categories is displayed in a TreeView
control. Child categories of the selected category in the TreeView
are displayed in a GridView
control. If you click the Add Category link, a floating virtual window appears that enables you to add a new product category (see Figure 34.5).
The ManageProductsDefault.aspx
page enables you to add, delete, and edit products. The list of current products is displayed in a GridView
control. If you click the Add New Product link, a new floating virtual window appears that contains a form for entering a new product (see Figure 34.6).
The ManageOrdersDefault.aspx
page displays a list of product orders. This page doesn’t display any product orders until a customer submits a shopping cart. You can click the Select link next to any order to view detailed order information, including the customer credit card information, billing address, and shipping address (see Figure 34.7).
The ASP.NET Beer Store is designed with Master Pages, Themes, and User Controls. Master Pages and User Controls are used to share content across multiple pages. Themes are used to give the pages in the application a common style.
Master Pages, Themes, and User Controls are covered in Part II of this book, “Designing ASP.NET Websites.”
The application uses two Master Pages named Store.master
and Manage.master
. The Store.master
Master Page contains the layout for the public section of the website. The Manage.master
page contains the layout for the private section of the website.
The Store.master
Master Page contains two of the standard ASP.NET Navigation controls: the Menu
control and the SiteMapPath
control. The Menu
control is used to display the list of product categories. The SiteMapPath
control is used to display the breadcrumb that appears near the top of every page.
The application takes advantage of two User Controls to create a standard layout for displaying products and product categories. The TemplatesProductTemplate.ascx
User Control is used in both the Featured Products DataList and the Products DataList to display product information. The TemplatesCategoryTemplate.ascx
User Control is used in the Categories DataList to display product category information.
All the application logic for the e-commerce application was pushed out of the pages and into a separate component library. Placing as much of your application logic as possible into separate components makes it easier to reuse the same methods across multiple pages in your website.
The components are contained in the App_Code folder. The contents of this folder are dynamically compiled. You don’t need to perform an explicit Build before using the components in the App_Code folder in your pages.
Components are discussed in Part IV of this book, “Building Components.”
The App_Code folder contains the following components:
Category
—. Represents a product category. Contains methods for adding, deleting, and editing product categories.
Product
—. Represents a product. Contains methods for adding, deleting, and editing products.
ShoppingCart
—. Represents a shopping cart. Contains methods for adding and deleting shopping cart items.
Order
—. Represents a product order. Contains methods for converting a shopping cart into a product order and retrieving orders.
Each component performs a dual role. Each component represents a particular type of object and it also represents methods for working with objects of that type. For example, the Product
component represents a product. It includes all of the product properties such as the product Name
and Price
properties.
However, the Product
component also contains all the methods for interacting with products. For example, it includes a method named SelectByCategoryId()
that retrieves all the products contained in a particular category.
One of the first issues that I encountered when building the e-commerce application was the problem of representing product categories.
On the one hand, I wanted to use all the standard Navigation controls. I wanted to use the Menu
, SiteMapPath
, and TreeView
controls to display the product categories. For example, as you move from page to page in the website, I wanted the SiteMapPath
control to display the current product category automatically so that a customer could navigate back up the category hierarchy. Because I wanted to use the product categories with the standard navigation controls, I needed to represent the categories in a Site Map.
Navigation controls and Site Maps are discussed in Part V of this book, “Site Navigation.”
On the other hand, I wanted to enable administrators of the application to be able to modify product categories easily through a form interface. I wanted an administrator to be able to add and edit categories through the form interface contained in the ManageCategoriesDefault.aspx
page.
Unfortunately, the default Site Map provider doesn’t support both requirements. The default Site Map provider uses XML files to represents the navigational structure of a website. An administrator cannot easily update an XML file through a form interface. Therefore, I decided to write a custom Site Map provider.
Product categories are represented with the CategorySiteMapProvider
. This class is contained in the App_Code folder. The CategorySiteMapProvider
stores navigation information in a SQL database table. The CategorySiteMapProvider
is configured to retrieve the categories from a database table named Categories.
The ASP.NET Beer Store actually uses both the standard XmlSiteMapProvider
and the CategorySiteMapProvider
to represent a Site Map. The root Web.sitemap
file is contained in Listing 34.1.
Example 34.1. Web.sitemap
<?xml version="1.0" encoding="utf-8" ?> <siteMap xmlns="http://schemas.microsoft.com/AspNet/SiteMap-File-1.0"> <siteMapNode url="~/default.aspx" title="Home" description="Home page"> <siteMapNode provider="CategorySiteMapProvider" /> <siteMapNode url="~/shoppingcart.aspx" title="Shopping Cart" description="View Shopping Cart" /> <siteMapNode url="~/contactinfo.aspx" title="Contact Us" description="Contact us by phone or email" /> <siteMapNode siteMapFile="~/manage/web.sitemap" /> </siteMapNode> </siteMap>
Notice that the second siteMapNode
element in Listing 34.1 specifies a particular provider. This node retrieves all its subnodes from the CategorySiteMapProvider
.
Notice, furthermore, that the last siteMapNode
element uses the siteMapFile
attribute. The Site Map for the management section of the website is contained in a separate XML Site Map file located at ManageWeb.sitemap
.
The ASP.NET Framework is smart enough to merge the SiteMapNode
s from these different providers and locations seamlessly in the background. I was surprised with how little work I had to perform to combine the SiteMapNode
s from the XML Site Map files and the SQL Server database table.
I also struggled with the issue of how to represent customer shopping carts. The obvious choice here is the ASP.NET Profile object. Using the Profile object offers several advantages.
Browser cookies, Session
state, and the Profile
object are discussed in Chapter 22, “Maintaining Application State.”
First, the Profile
object is persistent. A customer can add items to a shopping cart during one visit and return many months later to complete a purchase.
Second, the Profile
object is designed to handle both anonymous and authenticated users. Requiring a customer to register before adding items to a shopping cart does not create a good customer experience. An e-commerce application should make the process of adding an item to a shopping cart as easy as possible.
Finally, when you take advantage of the Profile
object, you don’t need to write any database logic to store shopping cart information. The Framework does all the hard work for you.
This all sounds good. Unfortunately, I encountered one issue with the Profile
object that I could not overcome. I wanted to be able to perform database joins between the items in a shopping cart and other database tables such as the Products database table.
For example, when a customer views his shopping cart, the shopping cart should not display the price of a product when the customer added the item to their shopping cart. Instead, the shopping cart should display the current price of the product (imagine that a customer adds an item to the shopping cart while the item is on sale and returns many months later).
When you store items with the Profile
object, all the items are stored as a blob. You can’t perform database queries against the individual items contained in a Profile
. In particular, you can perform database joins between items in a Profile
and other database tables.
Therefore, I created a custom ShoppingCart
component to represent customer shopping carts. The ShoppingCart
component is in the App_Code folder.
The custom ShoppingCart
component persists customer shopping carts. The component stores shopping carts in a database table named ShoppingCart
s.
The custom ShoppingCart
component handles both anonymous and authenticated users. It does it the same way that the Profile
object does. Anonymous Identification is enabled in the web configuration file. When Anonymous Identification is enabled, a persistent cookie containing a unique identifier is added to each anonymous customer’s browser. This unique identifier is used when an anonymous customer’s shopping cart is stored and retrieved.
The Global.asax
file includes a Profile_OnMigrateAnonymous()
event handler. This event handler calls a method of the ShoppingCart
class named AuthenticateCart()
when an anonymous user logs in or registers. This method updates the ShoppingCarts database table by replacing the customer’s anonymous identifier with the customer’s authenticated username.
Finally, the custom ShoppingCart
component caches customer shopping carts in Sessions
state. A customer’s shopping cart does not need to be retrieved from the database with each page request. Instead, the shopping cart is stored in the web server’s memory while the customer browses the website. This is done to improve performance.
Storing credit card numbers in plain text in the database is an extremely bad idea. If a customer trusts you with a credit card number, you should do everything in your power to protect the information.
The best option is to never store credit card numbers at all. If you process a customer credit card number immediately after the customer submits it, then you can discard the credit card number when the transaction completes.
If you want to modify the e-commerce application to process credit cards immediately, one easy way to do this is to take advantage of the PayPal SDK. To learn more about the PayPal SDK, visit the following website:
http://www.paypal.com/cgi-bin/webscr?cmd=xpt/cps/general/SoftwareDevKit-outside
The e-commerce application stores credit card numbers in the Orders database table. Credit card numbers are not stored in plain text. Instead, they are encrypted before being added to the database.
The e-commerce application uses a component named Secret
to encrypt and decrypt credit card numbers. The Secret
component is located in the App_Code folder.
You should use a Secure Sockets Layer (SSL) connection between a browser and web server whenever a user submits sensitive information, such as a credit card number, in a form. SSL encrypts the data that is passed across the Internet. You can enable SSL when serving pages with Internet Information Server by installing an SSL certificate. You need to purchase an SSL certificate from a Certificate Authority such as Verisign (www.verisign.com) or Thawte (www.thawte.com).
The Secret
component uses the RijndaelManaged
class from the System.Security.Cryptography
namespace to encrypt and decrypt strings. The Rijndael algorithm is also known as the Advanced Encryption Standard (AES). It is the United States government encryption standard.
To use the RijndaelManaged
class to encrypt a string, you must supply an encryption key and an initialization vector (IV). The encryption key must be kept secret. The IV, on the other hand, does not need to be kept secret. You need both the encryption key and IV to decrypt an encrypted string.
The Secret
component loads the encryption key from the machineKey
section of the web configuration file. The component reads the value of the decryptionKey
attribute. The component uses the same key that is used by the ASP.NET Membership framework. The IV is generated from the first bytes of the encryption key.
If you change the value of the decryptionKey
attribute in the web configuration file, then you can’t retrieve any of the credit card numbers stored in the database. Credit card numbers are retrieved as a string of question marks.
The sample application contains a machineKey
section with a decryptionKey
attribute in the web configuration file. You need to change the value of the decryptionKey
attribute to a new value. You can generate a new decryptionKey
by using the GenerateKeys.aspx
page described in Chapter 21, “Using ASP.NET Membership.”
Of course, all this encryption is meaningless if a hacker gets access to the ManageOrdersDefault.aspx
page. This page displays order information, including the credit card number associated with an order. The page is password protected so that only members of the StoreAdmins role can access the page. However, if a hacker manages to bypass the ASP.NET Authentication framework, then all bets are off.
When you add a product with the ManageProductsDefault.aspx
page, you can add an image for the product. The product image is stored in the Products database table.
The FileUpload
control in is covered in Chapter 4, “Using the Rich Controls.”
The ManageProductsDefault.aspx
page uses the standard ASP.NET FileUpload
control to upload the image. The image is read from the FileUpload
control and inserted into the database with the Product.InsertImage()
method. To avoid clobbering the entire memory of your web server with a large image, this method adds the image to the database incrementally in 8040-byte chunks.
The product images are displayed by the Default.aspx
, Products.aspx
, and ProductDetails.aspx
pages. All these pages use a Generic Handler named ProductImage.aspx
to retrieve the image. This handler retrieves the image in 8040-byte chunks from the database and sends the image bytes to the browser.
Generic Handlers are discussed in Chapter 25, “Working with the HTTP Runtime.”
No ASP.NET 2.0 application should be written without at least a little bit of AJAX (and a lot is great). The e-commerce application includes an AjaxRotator
control that displays a random content item retrieved from the web server. The AjaxRotator
updates its contents on the browser automatically every 15 seconds.
The AjaxRotator
is included in the Store.master
Master Page, so it is included on every public page in the website (see Figure 34.8). In the case of the ASP.NET Beer Store, the AjaxRotator
is used to randomly display different beer facts (of questionable veracity).
The AjaxRotator
retrieves its content items from an XML file named AjaxRotatorContent.config
. This XML file contains a list of <item>
elements, each of which represents a content item. The <item>
elements can contain HTML content just as long as the content is XHTML-compliant.
AJAX is covered in Chapter 7, “Creating Custom Controls with User Controls,” and Chapter 32, “Integrating JavaScript in Custom Controls.”
The best way to improve the performance of an ASP.NET application is through caching. The e-commerce application takes extensive advantage of the new caching features of the ASP.NET 2.0 Framework.
The list of products, list of featured products, and list of categories are displayed with three user controls named ProductView.ascx
, FeaturedProductView.ascx
, and CategoryView.ascx
. All three of these user controls include an <%@ OutputCache %>
directive that includes a SqlDependency
attribute.
All three user controls use a Polling SQL Cache dependency. The user controls cache data in memory until the data changes in the underlying database. For example, the list of featured products displayed on the home page (Default.aspx
) is cached by the FeaturedProductView.ascx
user control. The rendered output of this user control is cached in memory until the contents of the Products database table is changed.
The product information displayed by the ProductDetails.aspx
page is also cached. However, in this case, the caching is performed at the level of the DataSource
control rather than at the level of a User Control. The product information is retrieved from an ObjectDataSource
control. The ObjectDataSource
control is configured to use a Polling SQL Cache Dependency.
The e-commerce application is configured to poll the database for changes every 15 seconds so data can be up to 15 seconds out of date. You can configure a shorter interval by modifying the pollTime
attribute of the <sqlCacheDependency>
element in the web configuration file.
Finally, the customer shopping carts are cached in Session
state. When a shopping cart is first retrieved from the database for a customer, the shopping cart is added to Session
state and remains there until the shopping cart is modified or the customer leaves the website.
The e-commerce application is standards friendly. It was written to conform to the XHTML 1.0 Transitional standard. You’ll notice that the footer includes an icon from the World Wide Web Consortium (W3C) that indicates that the website has successfully passed their XHTML 1.0 Transitional validator (see Figure 34.9).
You can validate any page against the W3C validator by visiting http://validator.w3.org.
The e-commerce application does not use HTML tables for layout. All page layout is performed with Cascading Style Sheets. For example, the Store.master
Master Page contains three <div>
tags that represent the three page columns. The three columns are laid out from left to right with the following three CSS classes:
.leftColumn { float:left; width:100px; padding:5px; } .middleColumn { float:left; width:450px; border-left:solid 1px blue; padding:8px; } .rightColumn { float:right; width:200px; border-left:solid 1px blue; border-bottom:solid 1px blue; padding:5px; }
Furthermore, the e-commerce application was designed to be accessible to persons with disabilities. All images include an ALT
attribute. All form elements are explicitly associated with a label. For example, the input field for a product name is created with the following Label
and TextBox
controls:
<asp:Label id="lblName" Text="Name:" AssociatedControlID="txtName" Runat="server" /> <asp:TextBox id="txtName" Text='<%# Bind("Name") %>' Runat="server" />
The AssociatedControlID
property is used to explicitly associate the Label
control with the TextBox
control.
The e-commerce application illustrates many of the new features of the ASP.NET 2.0 Framework. First, it illustrates how you can take advantage of Master Pages and Themes when designing your website. The e-commerce application takes advantage of both technologies to make it easy for you to change the appearance of the website.
The e-commerce application also takes advantage of the new Navigation controls and Site Map infrastructure included in the ASP.NET 2.0 Framework. The application uses TreeView
, Menu
, and SiteMapPath
controls. These controls are bound to Site Map data retrieved from either the standard XmlSiteMapProvider
or the custom CategorySiteMapProvider
.
The e-commerce application also takes advantage of the new performance-enhancing features of the ASP.NET 2.0 Framework. The application uses Polling SQL Cache Dependencies to cache product and category data in memory just as long as the data does not change in the database.
Finally, the e-commerce application conforms to W3C standards such as XHTML and accessibility standards. The entire website validates as XHTML 1.0 Transitional.