Putting It All Together

This chapter is excerpted from Learning ASP.NET 3.5, Second Edition: Build Web Applications with ASP.NET 3.5, AJAX, LINQ, and More by Jesse Liberty, Dan Hurwitz, Brian MacDonald, published by O'Reilly Media

Learning ASP.NET 3.5, Second Edition

Logo

Buy Now

You've now practiced using just about every major tool in the ASP.NET toolbox. You've made dozens of sample applications, and you've gotten a feel for just how easy it is to make functional web sites with just a few controls. Now it's time to put those skills to the test. In this chapter, you'll make a fully functional (if somewhat limited) shopping application for the Adventure Works company. Unlike the order form you made in Chapter 2, Building Web Applications, this application will use all the skills you've learned. It uses data controls to display the Adventure Works database and retrieve the content the user wants, done in AJAX to speed things along. It has a shopping cart to store the items the user has purchased. It uses session state to pass that information on to a purchasing page. It incorporates validation controls to make sure the user enters good data. It has master pages that provide a consistent look and feel to the site, and custom error pages in case of problems. Finally, it has login controls to ensure that only registered users can access the pages of the site. In short, it's a fully functional working application.

Getting Started

Create a new web site titled All Together. This is the site that you'll use throughout the example in this chapter. This chapter consists of a single large example. As we build up the example, we will provide full code listings and shorter snippets along the way. At the end of the chapter are complete code listings for the entire example so you can see how everything fits together.

Tip

You can also download this example, as well as all of the other examples in this book, from http://www.oreilly.com/catalog/9780596518455.

Add a Master page to the web site, MasterPage.master. Be sure that the "Place Code in separate file" checkbox is checked.

Close the Default.aspx file and then delete it; you won't need it.

Add an Images folder to the web site by right-clicking on the root folder in the Solution Explorer and selecting New Folder. Insert a folder and call it Images.

This next step is really important: Create the logo file Adventure Works Logo-250 x 70.gif using any image-editing tool you like (our logo file is 250 pixels wide x 70 pixels high), or download it from this book's web site. Once the image file is on your machine, it must be added to the project. Right-click on the Images folder and select Add Existing Item. Then, navigate to the logo file, wherever it is on your file system, and select it. It will automatically be copied to the Images folder and added to the project.

Adding Styles

You'll be using CSS styles for the various parts of your site, so you need to define the styles first. Add a CSS style sheet to the web site by selecting Website → Add New Item, and selecting Style Sheet. You can keep the default name of StyleSheet.css.

Copy in the styles from Example 11.1, "StyleSheet.css" any way you wish. You can type the styles in directly in the source code editing surface, or use the CSS editing tools described back in Chapter 6, Style Sheets, Master Pages, and Navigation.

Example 11.1. StyleSheet.css

body
{
   font-family: Arial; Helvetica; sans-serif;
}
.ButtonSelect
{
   font-weight: normal;
   font-size: x-small;
   background-color: Yellow;
   color: Blue;
}
.ButtonText
{
   font-weight: bold;
   font-size: x-small;
   color: Black;
}
.Hyperlink
{
   font-weight: normal;
   font-size: small;
   color: Blue;
   text-decoration: underline;
}
.LabelMedium
{
   font-weight: bold;
   font-size: Medium;
   color: Black;
}
.LabelSmall
{
   font-weight: bold;
   font-size: small;
   color: Black;
}
.ListHeading
{
   font-weight: bold;
   text-decoration: underline;
   font-size: x-small;
   color: Black;
}
.MenuText
{
   font-weight: normal;
   font-size: small;
   color: Blue;
}
.PageTitle
{
   font-weight: bold;
   font-size: xx-large;
   color: Green;
}
.PageSubTitle
{
   font-weight: bold;
   font-size: x-large;
   color: Blue;
}
.TableCells
{
   font-weight: normal;
   font-size: small;
   color: Black;
   text-align: left;
   vertical-align: top;
}
.TableColumnHeading
{
   font-weight: bold;
   text-decoration: underline;
   font-size: small;
   color: Black;
   text-align: left;
}
.TableColumnHeadingRight
{
   text-align: right;
}
.TableNumberDecimal
{
   font-weight: normal;
   font-size: small;
   color: Black;
   text-align: right;
}
.TableRowHeading
{
   font-weight: bold;
   text-decoration: none;
   font-size: small;
   color: Black;
   text-align: left;
}
.TextBold
{
   font-weight: bold;
   font-style: italic;
   font-size: medium;
   color: Black;
}
.TextNormal
{
   font-weight: normal;
   font-size: medium;
   color: Black;
}
.TextSmall
{
   font-weight: normal;
   font-size: small;
   color: Black;
}
.TextXSmall
{
   font-weight: normal;
   font-size: x-small;
   color: Black;
}
.ValidationError
{
   font-weight: normal;
   font-size: small;
}
.Warning
{
   font-weight: bold;
   font-size: Small;
   color: Red;
}
.WarningRoutine
{
   font-weight: normal;
   font-size: Small;
   color: Red;
}

Using Master Pages

Add a new page, Login.aspx. Check both checkboxes: "Place code in separate file" and "Select master page." When the Master Page dialog comes up, select MasterPage.master. Remove the Content control that refers to the ContentPlaceHolder control named head.

Add several other new pages: Home.aspx, Products.aspx, Cart.aspx, Purchase.aspx, and Confirm.aspx. For each of these, select the same master page. Edit each of these pages to remove the Content control referring to the head ContentPlaceHolder. Set Home.aspx to be the startup page.

Open MasterPage.master. Add a style statement to the <head> element to import the style sheet, as in the highlighted line in the following snippet:

<head runat="server">
    <title>Untitled Page</title>
    <link href="StyleSheet.css" rel="stylesheet" type="text/css" />
</head>

Delete the ContentPlaceHolder control with the ID of head.

Add an HTML table for layout, inside the <div> element, but before the content placeholder control. You can use the IDE tools or just type it manually in the Source view window. With the help of IntelliSense, I find it easier to type it manually.

Add the table structure and server controls below (above the ContentPlaceHolder):

      <table border="0">
         <tr>
            <td colspan="4">
               <table>
                  <tr>
                     <td width="10px">
                        &nbsp;
                     </td>
                     <td>
                        <asp:ImageButton ID="ibLogo" runat="server"
                           ImageUrl="~/images/AdventureWorksLogo-250x70.gif"
                           AlternateText="AdventureWorks logo"
                           PostBackUrl="~/Home.aspx" />
                     </td>
                     <td width="10px">
                        &nbsp;
                     </td>
                     <td width="500px" align="right">
                        <span class="PageTitle">Adventure Works</span>
                        <br />
                        <asp:Label ID="lblPageSubTitle" runat="server"
                           CssClass="PageSubTitle" Text="Page SubTitle" />
                        <br />
                        <asp:Label ID="lblTime" runat="server"
                           CssClass="TextXSmall" />
                     </td>
                     <td width="10px">
                        &nbsp;
                     </td>
                  </tr>
                  <tr>
                     <td colspan="5">
                        <hr />
                     </td>
                  </tr>
               </table>

            </td>
         </tr>
      </table>

<asp:contentplaceholder id="ContentPlaceHolder1" runat="server" >
</asp:contentplaceholder>

This code defines the table that you're going to use to hold the content of the master page. The first cell contains an ImageButton control to hold the logo for the site; when users click on the logo, it will take them to the Home.aspx page. The control displays the logo image file you created or downloaded earlier.

The cell to the right of the logo contains some text for the title and a pair of labels. Note the use of the <span> element on the page title; this allows you to apply a CSS class to it. The first label will contain the page subtitle, which will change depending on the page the user is on. The other label will contain the date and time, just for convenience.

Also note the use of the border="0" attribute in the opening <table> tag. This is a vestige of the development process. Although you might not want borders in the finished site, it is often helpful to make the cell borders visible during development by setting the border thickness to 1 pixel with border="1". Then, when you are satisfied with the layout, set the borders back to 0 so they are no longer visible.

You'll need to populate the Label that shows the time, so open MasterPage.master.vb, the code-behind file for the master page. Create an event handler for the Page_Load event by selecting (PageEvents) from the left drop-down menu and Load from the right drop-down menu. Enter the following line of code:

lblTime.Text = DateTime.Now.ToString()

Open Home.aspx. Edit the Page directive at the top of the file to set the title attribute to Home Page; also add the trace attribute at this time, but set it to false.

You'll need this because you know you are going to want to turn trace on or off during various phases of development.

Add a MasterType directive to the file also. This will enable the content page to access properties declared in the master page: if you haven't already, delete the Content control which refers to the Head ContentPlaceHolder that we deleted above. The complete markup for Home.aspx should look like the following:

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
    CodeFile="Home.aspx.vb" Inherits="Home" title="Home Page" Trace="false"%>
<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2 ContentPlaceHolderID="ContentPlaceHolder1"
    Runat="Server">
</asp:Content>

Run the site now, to see what you've done so far. You should see something like Figure 11.1, "Here's how the home page looks with nothing on it and the page subtitle not yet set.".

Figure 11.1. Here's how the home page looks with nothing on it and the page subtitle not yet set.

Here's how the home page looks with nothing on it and the page subtitle not yet set.

You want the page subtitle to display the current page; for example, Home or Products. The label is already in place in the master page.

Go to the code-behind file for the master page. Add the following code outside the Page_Load method and inside the class definition (the line that says Partial Class MasterPage) to create a public property called PageSubTitle, of type Label. (IntelliSense can be a big help here. To insert a good starting code snippet, click on Edit → IntelliSense → Insert Snippet → Code Patterns → Properties, Procedures, Events → Define a Property.)

Public Property PageSubTitle() As Label
    Get
        Return lblPageSubTitle
    End Get
    Set(ByVal value As Label)
        lblPageSubTitle = value
    End Set
End Property

Then, in the code-behind of the home page, create a Page_Load method with the following highlighted line of code:

Protected Sub Page_Load(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles Me.Load
    Me.Master.PageSubTitle.Text = "Home"
End Sub

Tip

If the IDE draws squiggly lines indicating some sort of problem, try building the web site by clicking on the Build menu item and selecting Build Web Site. The spurious error indicators will go away.

Switch over to the Source view of the home page and add some content inside the Content control, such as listed in Example 11.2, "Markup for the home page - Home.aspx".

Example 11.2. Markup for the home page - Home.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
    CodeFile="Home.aspx.vb" Inherits="Home" title="Home Page" Trace="false"%>
<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
    <h2>This Is Your Home Page</h2>
    <div class="TextNormal">
        You can put some stuff about your company here. Perhaps some links.
        Of course, in a real world application, the navigation would probably
        much more complex. Also, the buttons would actually do something,
        rather than just wave their arms and say <span class="TextBold">Look
        at me!</span>
    </div>
</asp:Content>

Running the site now results in a page that looks like Figure 11.2, "The home page now has the subtitle set and some content added.".

Figure 11.2. The home page now has the subtitle set and some content added.

The home page now has the subtitle set and some content added.

Setting Up Roles and Users

Your page has a good foundation, but you should add a measure of security to it to separate the customers from the managers. The next step is to enable security and then create a few users for your site.

Go to the WAT by selecting Website → ASP Configuration. Click on Security, and then click on "Select authentication type" under the Users column. Because this site will be available on the Internet, forms-based security is the way to go. Change the Authentication type to Forms by selecting "From the internet." You'll be setting up some roles to group the Adventure Works users into customers, employees, and managers. Back on the Security page, click on Enable roles. The security page should now look something like Figure 11.3, "You've switched to Forms authentication, and enabled roles for your site, but there aren't any users just yet.".

Click on "Create or Manage roles", and create three roles: Manager, Employee, and Customer.

Click on the Back button to go back to the Security page. Click on Create user, and create four users, as in the following table.

Figure 11.3. You've switched to Forms authentication, and enabled roles for your site, but there aren't any users just yet.

You've switched to Forms authentication, and enabled roles for your site, but there aren't any users just yet.

User

Password

Role

bmacdonald

brian123!

Customer

dhurwitz

dan123!

Employee

jliberty

jesse123!

Customer

rhampster

rich123!

Manager

You must also provide an email address and a security question and answer for each user. We will not be using that information in this example, so it does not matter what you enter. Close the WAT. Now you have four users to work with for this example.

Logging In

Now that you have your users, you need a way for them to log in. Edit the master page markup file to add some login functionality.

Add another table row to the layout table, listed in Example 11.3, "Code snippet from MasterPage.master containing the login controls". Note that the ContentPlaceHolder control has been moved to within one of the table cells.

Example 11.3. Code snippet from MasterPage.master containing the login controls

<tr>
    <td width="5px">&nbsp;</td>
    <td width="150px" valign="top">
        <asp:LoginStatus ID="LoginStatus1" runat="server" CssClass="Hyperlink" />
        <br />
        <asp:LoginView ID="LoginView1" runat="server" >
            <LoggedInTemplate>
                <span class="WarningRoutine">Welcome</span>
                <asp:LoginName ID="LoginName1" runat="server"
                    CssClass="WarningRoutine"/>
            </LoggedInTemplate>
            <AnonymousTemplate>
                <span class="WarningRoutine">You are not logged in.
                     Please click the login link to log in to this website.</span>
            </AnonymousTemplate>
        </asp:LoginView>
    </td>
    <td width="5px">&nbsp;</td>
    <td width="700px" valign="top" bgcolor="yellow">
        <asp:contentplaceholder id="ContentPlaceHolder1" runat="server" >
        </asp:contentplaceholder>
    </td>
</tr>

This code adds a new row to the table on the master page. The first and third cells are just spacers. The second cell holds a LoginStatus control and a LoginView control to go with it. Notice that the CssClass properties of both controls have been set to apply styles to them. The LoginView control has text added to it to present appropriate messages to either logged-in or anonymous users.

The fourth cell in the row now holds the ContentPlaceHolder control, so be sure to move the ContentPlaceHolder control that was outside the table to this cell.

Edit the Login.aspx page that you created earlier. Set the title in the Page directive, if you haven't already, remove the extra Content control, and add the same MasterType directive that you added to the home page. Drag a Login control into the Content area. Switch to Design view, click on the Smart Tag of the Login control, and click on Auto Format. Select the "Professional" scheme. Set the DestinationPageUrl property to ~/Home.aspx so that users will be returned to the home page after they log in. You will end up with something like Example 11.4, "Login.aspx" for the markup for the Login page, with the changes highlighted.

Example 11.4. Login.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
    CodeFile="Login.aspx.vb" Inherits="Login" title="Login" Trace="false" %>
<%@ MasterType TypeName="MasterPage" %>
<asp:C ontent ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" Runat="Server">
    <asp:Login ID="Login1" runat="server" DestinationPageUrl="~/home.aspx"
            BackColor="#F7F6F3" BorderColor="#E6E2D8" BorderPadding="4"
            BorderStyle="Solid" BorderWidth="1px" Font-Names="Verdana"
            Font-Size="0.8em" ForeColor="#333333">
        <TitleTextStyle BackColor="#5D7B9D" Font-Bold="True"
            Font-Size="0.9em" ForeColor="White" />
        <InstructionTextStyle Font-Italic="True" ForeColor="Black" />
        <TextBoxStyle Font-Size="0.8em" />
        <LoginButtonStyle BackColor="#FFFBFF" BorderColor="#CCCCCC"
            BorderStyle="Solid" BorderWidth="1px"
            Font-Names="Verdana" Font-Size="0.8em" ForeColor="#284775" />
    </asp:Login>
</asp:Content>

Tip

When you use the AutoFormat feature of a Smart Tag, it generally (but not always) uses hex values of red, green, and blue (RGB) to specify colors. You can also specify the colors with the common names, such as Red, White, and Blue.

Open the code-behind of the Login page. Create an event handler for the Page_load event and add the following highlighted line of code:

Protected Sub Page_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
    Me.Master.PageSubTitle.Text = "Login"
End Sub

All this does is set the page subtitle in the master page area.

Now run the site. You will see the first screen shown in Figure 11.4, "When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.".

Click on the Login link to get the Login page, shown as the second screen in Figure 11.4, "When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.". Enter the username and password for one of the user accounts you created earlier in this chapter. After you click on the Log In button, you will be brought back to the home page, shown as the third screen in Figure 11.4, "When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.".

Warning

It is important that you actually click on the Log In button, rather than just pressing the Enter key. The Log In button does not have focus, the ImageButton on the master page displaying the logo does. So, if you press the Enter key, it will take you back to the home page without logging you in.

Earlier, you enabled roles in the WAT and added each user to one of the three roles: Manager, Customer, and Employee. As you saw in Chapter 10, Personalization, you can use these roles to present customized content to the users who visit the page. You'll add two Panel controls to the home page that present content depending on the user's role.

Figure 11.4. When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.

When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.

Edit Home.aspx to see this in action. Add two Panel controls, as listed in Example 11.5, "Role-specific content in Home.aspx", to the page, inside the Content control, after the closing <div> for the text that all users see.

Example 11.5. Role-specific content in Home.aspx

<asp:Panel ID="pnlEmployee" runat="server" Visible="false" >
    <h3>Employee Information</h3>
    <div class="TextNormal">
        This panel should only be visible to users are a members of the
        <b>Employee</b> role. Turning on the visibility of this Panel occurs in the
        Page_Load event handler.
     </div>
 </asp:Panel>
 <asp:Panel ID="pnlManager" runat="server" Visible="false" >
     <h3>Manager Information</h3>
     <div class="TextNormal">
         This panel should only be visible to users are a members of the
         <b>Manager</b> role. Turning on the visibility of this Panel occurs in the
         Page_Load event handler.
     </div>
 </asp:Panel>

Switch over to the code-behind for the home page, Home.aspx.vb. Add the high-lighted lines of code from Example 11.6, "Controlling visibility based on roles in Home.aspx.vb" to the Page_Load event handler.

Example 11.6. Controlling visibility based on roles in Home.aspx.vb

Protected Sub Page_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
    Me.Master.PageSubTitle.Text = "Home"

    ' control the visibility of sections restricted to specific roles
    pnlManager.Visible = User.IsInRole("Manager")
    pnlEmployee.Visible = User.IsInRole("Employee")
End Sub

The code here is very simple; it sets the visibility of each panel depending on the value of the IsInRole method for the appropriate role.

Before logging in, the home page will still look like the first screen shown previously in Figure 11.4, "When you first run the site, you'll see the screen on the left. After you click the Login link, you'll be taken to the second screen, and after you've successfully logged in, you'll be taken back to the home page, which now looks like the third screen.". If you log in as rhampster, who is a member of the Managers role (it's only fitting that the boss is a rodent), you will see Figure 11.5, "If you log in as a member of the Manager role, you'll see the manager-specific information.".

Figure 11.5. If you log in as a member of the Manager role, you'll see the manager-specific information.

If you log in as a member of the Manager role, you'll see the manager-specific information.

Now, log out and log in as user dhurwitz, and you'll see just the content of pnlEmployee. Log in again as user jliberty or bmacdonald, and you won't see either panel because customers don't need to see employee-specific information. Of course, if you make a user a member of both the Manager and Employee roles, she would see both panels.

The front page of your site is looking pretty good. Users can identify themselves, and see the custom content. The master page is working as planned, and each page identifies itself appropriately. The next thing to do is add some navigation tools so that users can find their way around, which means you have to create a site map. Close the browser if it is open, and select Website → Add New Item, and choose Site Map. Accept the default name of Web.sitemap.

As you learned in Chapter 6, Style Sheets, Master Pages, and Navigation, the site map is an XML file, and you have to create it manually-the IDE won't do it for you. Open the web.sitemap file, and replace the default boilerplate with the highlighted code in Example 11.7, "Web.sitemap".

Example 11.7. Web.sitemap

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="https://schemas.microsoft.com/AspNet/SiteMap-File-1.0" >
    <siteMapNode title="Root" >
        <siteMapNode url="~/Home.aspx" title="Home" description="Home page" />
        <siteMapNode url="~/Products.aspx" title="Products"
            description="Products offered by AdventureWorks" />
        <siteMapNode url="~/Cart.aspx" title="Shopping Cart"
            description="Items selected for purchase" />
        <siteMapNode url="~/Purchase.aspx" title="Purchase"
            description="Purchase your selected items" />
    </siteMapNode>
</siteMap>

Now that you have the site map file, you'll add the navigation controls to the master page. Add the following code to MasterPage.master in the same table cell and after the LoginView control:

<hr />
<asp:SiteMapDataSource ID="SiteMapDataSource1" runat="server"
    ShowStartingNode="false" />
<asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"
    CssClass="MenuText" />

In this case, you're using a Menu control rather than a TreeView. Note that the DataSourceID property of the Menu control points to the SiteMapDataSource control that you just created. You've set the control's ShowStartingNode property of the SiteMapDataSource control to false to suppress display of the root node in the menu.

You don't want anonymous users to be able to use the menu, so add the following code to the Page_Load method of MasterPage.master.vb to disable the menu if the user is not logged in:

If Page.User.Identity.IsAuthenticated = False Then
    Menu1.Enabled = False
End If

Anonymous users will be able to see the menu; they just won't be able to click anything on it.

At this point, a user could bypass the login by entering the URL of any of the other pages directly into the browser. To prevent this, add the following code to the Page_ Load method of every page in the web site except Home and Login (where you want to allow anonymous users):

If User.Identity.IsAuthenticated = False Then
   Response.Redirect("login.aspx")
End If

Go ahead and try out the site now to make sure everything is working. When you first see the home page, you'll see that the navigation menu is disabled. Try entering cart.aspx in the address field of your browser. You'll see that you're taken back to the Login page instead.

One other navigation aid that you can add is setting the page subtitle in the master page to identify to the user where they are. You have already done this for the Home and Login pages. For the rest of the pages, add the MasterType directive to the top of the relevant markup file:

<%@ MasterType TypeName="MasterPage" %>

While in each page markup file, also set the title attribute of the Page directive. Then, go to the Page_Load method of each page code-behind file and add a line similar to the following (for the Purchase page), which sets the page subtitle:

Me.Master.PageSubTitle.Text = "Purchase"

Tip

Most real-world sites would have a somewhat deeper menu structure, and so might benefit from a SiteMapPath control to provide bread crumbs.

Test out everything to see how it works.

Products Page

The intent of the Products page is to allow the user to select a product category and then see a grid displaying all the products in that category. The user can select any of these products to see more detail about that product, and if she wants, she can add that item to the shopping cart by clicking on a button. The finished page can be seen in Figure 11.6, "The Products page is now finished, and has several items added to the cart.".

Figure 11.6. The Products page is now finished, and has several items added to the cart.

The Products page is now finished, and has several items added to the cart.

This page has several data-bound controls: a RadioButtonList for selecting the product category, a GridView for displaying the products (filtered by category), and a DetailsView for displaying details about the currently selected item.

In addition, visibility of some of the controls is turned off, depending on circumstances. Initially, only the RadioButtonList is visible. Once the user has selected a category, the GridView is made visible. When the user selects a product from the GridView, the DetailsView and its associated button for adding the item to the cart are made visible.

Open Products.aspx. You will start with a data source control. Go to Source view, if not there already, and drag a SqlDataSource control from the Data section of the Toolbox into the Content control on the page. Set the control's ID to sqlCategories. Switch to Design view and click on the Smart Tag. Configure it to point to the Adventure Works database-if you did the examples in Chapter 4, Saving and Retrieving Data, you may still have a data connection set up that refers to AdventureWorks.mdf. If not, you should probably flip back to Chapter 4, Saving and Retrieving Data and review the section on creating a database connection. In the "Configure the Select Statement" portion of the Wizard, select the "Specify a custom SQL statement" radio button. After you click Next, enter the following custom statement:

select Name, ProductCategoryID from Production.ProductCategory order by Name

You could have built this statement in the Wizard, but remember from Chapter 4, Saving and Retrieving Data that the IDE doesn't automatically include the Production schema in the Select statement, so this custom statement is easier. Test the query to make sure everything is working, and finish the Wizard.

Drag a RadioButtonList control from the Standard section of the Toolbox onto the content section of the page. Set its ID to rblCategories. In Design view, click on its Smart Tag and select Choose Data Source. In the Data Source Configuration Wizard, select sqlCategories as the data source, Name as the data field to display, and ProductCategoryID as the data field for the value, as shown in Figure 11.7, "If you can't see any of the data fields in the drop-down menu controls, click the "Refresh Schema" link to see them.".

Figure 11.7. If you can't see any of the data fields in the drop-down menu controls, click the "Refresh Schema" link to see them.

If you can't see any of the data fields in the drop-down menu controls, click the "Refresh Schema" link to see them.

Tip

If none of the fields are visible in the drop-down menus, click on the Refresh Schema link, indicated with the arrow in Figure 11.7, "If you can't see any of the data fields in the drop-down menu controls, click the "Refresh Schema" link to see them.".

Set the RepeatDirection property of rblCategories to Horizontal, and the AutoPostBack property to True, so that the page will post back as soon as a change is made (later you will add AJAX features to avoid the flicker), and the CssClass property to LabelSmall.

Run the web site, log in, and go to the Products page. You will see a set of four radio buttons, as shown in Figure 11.8, "Once you've logged in and navigated to the Products page, you'll see the list of product radio buttons.".

Figure 11.8. Once you've logged in and navigated to the Products page, you'll see the list of product radio buttons.

Once you've logged in and navigated to the Products page, you'll see the list of product radio buttons.

Stop the application, and then drag another SqlDataSource control onto the content area to be the data source for the products grid. Set its ID to sqlProducts. This data source will return all the products of the category specified in the radio buttons, so you need to pass the value of the selected radio button to the data source as a parameter. Unfortunately, the Data Source Configuration Wizard shown previously in Figure 11.7, "If you can't see any of the data fields in the drop-down menu controls, click the "Refresh Schema" link to see them." does not do parameterized queries, so you need to enter the code directly into Source view, as shown in the following code snippet:

<asp:SqlDataSource id="sqlProducts" runat="server"
    ConnectionString=
        "<%$ ConnectionStrings:AdventureWorksConnectionString %>">
    <SelectParameters>
        <asp:ControlParameter ControlID="rblCategories"
            Name="ProductCategoryID"
            PropertyName="SelectedValue" />
    </SelectParameters>
</asp:SqlDataSource>

The ConnectionString attribute points to the previously configured connection string. Your connection string may have a different name than that shown here. The SelectParameters element specifies that the parameter will be called ProductCategoryID and will come from the SelectedValue property of the rblCategories control.

But where is the SQL Select command, and where is this parameter used? You could declare a SelectCommand attribute, as you did for the first SqlDataSource, but this query is sort of long and complex, with a subquery as well as the parameter. So you will set the SelectCommand property of the control programmatically in the Page_Load of the Products page. Open the Page_Load method in Products.aspx.vb, and add the following code:

Dim strCommand As String = String.Empty
strCommand = "select ProductID, Name, ProductNumber, ListPrice from " + _
                "Production.Product "
strCommand += "where ProductSubcategoryID in "
strCommand += "(select ProductSubcategoryID from " + _
                "Production.ProductSubcategory "
strCommand += "where ProductCategoryID = "
strCommand += "@ProductCategoryID)"
sqlProducts.SelectCommand = strCommand

The parameter, @ProductCategoryID (highlighted in the above code snippet), assumes the value of the selected radio button. When the page first loads and none of the radio buttons are selected, this query returns nothing, so the GridView does not display. But as soon as a value is selected, the query returns rows and they display in the GridView.

To see this, drag a GridView control from the Data section of the Toolbox onto the content area. Set its ID property to gvProducts and its DataKeyNames property to ProductID. In Design view, click on its Smart Tag and set its Data Source to be sqlProducts. While the Smart Tag is open, check the Enable Paging, Enable Sorting, and Enable Selection checkboxes, as shown in Figure 11.9, "After you've selected the data source, and enabled Paging, Sorting, and Selection, the gvProducts GridView will look like this.".

Click on the Edit Columns link in the Smart Tag, where you'll specify the columns from the SELECT query: ProductID, Name, ProductNumber, and ListPrice. Be sure to uncheck the Auto-generate fields checkbox. Although you want all the fields from the query to display, manually adding the columns to the GridView allows you to fully specify the appearance and behavior of each column.

To add each field from the query, make sure BoundField is selected in the Available fields list, and then click the Add button. For the first column, set the DataField for this BoundField to ProductID, the SortExpression to ProductID, the HeaderText to ID, and the ItemStyleWidth to 50px, as shown in Figure 11.10, "Specify the ProductID bound field in the Fields dialog box.". Then, add each of the other columns accordingly.

Alternatively, you can declare all the fields directly in Source view, or any combination of techniques that works for you. In any case, you should end up with the following declaration for the products GridView, including several attributes of the GridView itself and all the columns within the <Columns> element:

Figure 11.9. After you've selected the data source, and enabled Paging, Sorting, and Selection, the gvProducts GridView will look like this.

After you've selected the data source, and enabled Paging, Sorting, and Selection, the gvProducts GridView will look like this.

<asp:GridView id="gvProducts" runat="server"
    DataSourceID="sqlProducts" DataKeyNames="ProductID"
    AllowSorting="True" AllowPaging="True"
    AutoGenerateColumns="False"
    HeaderStyle-CssClass="TableColumnHeading"
    RowStyle-CssClass="TableCells">
    <Columns>
        <asp:CommandField ShowSelectButton="True" ItemStyle-Width="50"
            ControlStyle-CssClass="ButtonSelect" />
        <asp:BoundField DataField="ProductID" HeaderText="ID"
            SortExpression="ProductID">
            <ItemStyle Width="50px" />
        </asp:BoundField>
        <asp:BoundField DataField="Name" HeaderText="Name"
            SortExpression="Name">
            <ItemStyle Width="225px" />
        </asp:BoundField>
        <asp:BoundField DataField="ProductNumber"
            HeaderText="Product Number"
            SortExpression="ProductNumber">
            <ItemStyle Width="90px" />
        </asp:BoundField>
        <asp:BoundField DataField="ListPrice" HeaderText="Cost"
            SortExpression="ListPrice"
            ItemStyle-CssClass="TableNumberDecimal"
            HeaderStyle-CssClass="TableColumnHeadingRight">
            <ItemStyle Width="60px" />
        </asp:BoundField>
    </Columns>
</asp:GridView>

Figure 11.10. Specify the ProductID bound field in the Fields dialog box.

Specify the ProductID bound field in the Fields dialog box.

The DataKeyNames attribute is very important. It specifies the name (or names) of the field(s) that make up the primary key for the items displayed. In this example, the primary key is a single field, ProductID.

As you can see, there are many CSS-related attributes, all of which allow you to apply a style to a specific type of element in the grid.

Run the site, log in, navigate to the Products page, and select a category. You'll see that all the products for that category are listed in the grid.

Now you need to display the item details when the user selects an item from the grid. Drag a Panel control onto the page, inside the Content area but after gvProducts. Set its ID to pnlProduct. Inside the Panel is going to be a layout table with a DetailsView control data bound to another SqlDataSource.

We haven't used the DetailsView control previously in this book. It is a databound control, similar to the GridView, but it is used to display or edit a single record at a time. In this example, it displays the details about the single record selected from the GridView.

The contents of the pnlProduct are listed in Example 11.8, "Panel pnlProduct on Products page".

Example 11.8. Panel pnlProduct on Products page

<asp:Panel id="pnlProduct" runat="server" Visible="false">
    <table width="100%">
        <tr>
            <td valign="top">
                <asp:Button id="btnAddToCart" runat="server"
                    Text="Add To Cart" OnClick="btnAddToCart_Click"
                    CssClass="ButtonText" />
                <div class="ListHeading">Items In Cart</div>
                <asp:Label ID="lblCart" runat="server" CssClass="TextSmall"
                    Width="90"/>
            </td>
            <td valign="top">
                <asp:SqlDataSource id="sqlDetailsView" runat="server"
                    ConnectionString=
                        "<%$ ConnectionStrings:AdventureWorksConnectionString %>">
                    <SelectParameters>
                        <asp:ControlParameter ControlID="gvProducts"
                            Name="ProductID"
                            PropertyName="SelectedDataKey.Values['ProductID']" />
                    </SelectParameters>
                </asp:SqlDataSource>
                <asp:DetailsView id="DetailsView1" runat="server"
                    DataSourceID="sqlDetailsView" DataKeyNames="ProductID"
                    AutoGenerateRows="false"
                    CssClass="TableCells" BorderWidth="0"
                    FieldHeaderStyle-CssClass="TableRowHeading"
                    CellSpacing="2" CellPadding="2" Width="500px" Height="50px">
                    <Fields>
                        <asp:BoundField DataField="ProductID"
                                    HeaderText="Product ID:"
                                    SortExpression="ProductID" />
                        <asp:BoundField DataField="Name" HeaderText="Name:"
                                    SortExpression="Name" />
                        <asp:BoundField DataField="ProductNumber"
                                    HeaderText="Product #:"
                                    SortExpression="ProductNumber" />
                        <asp:BoundField DataField="ListPrice" HeaderText="Cost:"
                                    SortExpression="ListPrice"
                                    DataFormatString="{0:C}" HtmlEncode="false"/>
                        <asp:BoundField DataField="Color" HeaderText="Color:"
                                    SortExpression="Color" />
                        <asp:BoundField DataField="CategoryName"
                                    HeaderText="Category:"
                                    SortExpression="CategoryName" />
                        <asp:BoundField DataField="SubcategoryName"
                                    HeaderText="SubCategory:"
                                    SortExpression="SubcategoryName" />
                        <asp:BoundField DataField="Description"
                                    HeaderText="Description:"
                                    SortExpression="Description" />
                    </Fields>
                </asp:DetailsView>
            </td>
        </tr>
    </table>

</asp:Panel>

As with the previous parameterized query, you will set the SelectCommand for the data source, sqlDetailsView, in the Page_Load of the Products page. In this case, the ProductID value of the row selected in gvProducts is passed as the parameter to the SQLDataSource named sqlDetailsView. Add the following code to the Page_Load method, after setting the SelectCommand property of the previous data source:

strCommand = String.Empty
strCommand += "select product.*, subcat.ProductSubcategoryID, " + _
            "subcat.Name as SubcategoryName, "
strCommand += "cat.ProductCategoryID, cat.Name as CategoryName, "
strCommand += "model.Name as ModelName, model.CatalogDescription, " + _
            "model.Instructions, "
strCommand += "description.Description "
strCommand += "from Production.Product product "
strCommand += "join Production.ProductSubcategory subcat on " + _
            "product.ProductSubcategoryID = subcat.ProductSubcategoryID "
strCommand += "join Production.ProductCategory cat on subcat.ProductCategoryID = " +_
            "cat.ProductCategoryID "
strCommand += "join Production.ProductModel model on product.ProductModelID = " + _
            "model.ProductModelID "
strCommand += "join Production.ProductModelProductDescriptionCulture culture on " + _
            "model.ProductModelID = culture.ProductModelID "
strCommand += "join Production.ProductDescription description on " + _
            "culture.ProductDescriptionID = description.ProductDescriptionID "
strCommand += "where product.ProductID = @ProductID and culture.CultureID = 'en' "
sqlDetailsView.SelectCommand = strCommand

Inside the Panel is also a Button, btnAddToCart. Switch to Design view and double-click the button to open the code-behind in the skeleton of an event handler, ready for you to type. The event handler code is included in Example 11.9, "Products.aspx.vb event handlers". This method retrieves the ProductID of the selected item using the Value of the SelectedDataKey property of the GridView. Then, it checks if the Session object exists, and in either case updates it with the currently selected item as a comma-separated string. It also displays the contents of the cart in a Label control. (The space trailing the comma allows the content of the Label control to wrap when many items are listed.)

SQL CHEAT SHEETJoins

All of the queries you have seen in this book so far have been simple SELECT statements from a single table. The true strength of a relational database comes from using multiple tables to contain normalized data. Data that has been normalized essentially means there is no duplicate data.

Suppose you have a database containing employment information. Each employee has not only a job title but also a job description. Rather than have identical job descriptions in the Employee table for every employee with the same job, it is much better to have the job titles and descriptions in a separate Jobs table, and then refer to that Jobs record in the Employees table. There is said to be a relationship between the Employees table the Jobs table.

Now, however, when you want to query the data, you must join the two tables back together in your query statement. This is done with the SQL keyword JOIN.

The JOIN keyword alone, as used in the preceding snippet, is the default join type, known as an inner join. This means that any rows in either table that do not match the selection criteria will not be included in the results.

There are many circumstances where you do not want to omit these records, in which case you must use an outer join. There are several different types of outer joins, including left, right, cross, and full, depending on which data specifically you want to include and which to omit.

For a complete discussion on SQL queries in general and joins in particular, we highly recommend Transact-SQL Programming by Kevin Kline et al. (O'Reilly). Although this book is a bit dated, only covering up through SQL Server 7.0, the basic syntax has not changed, and this book remains an excellent primary reference for SQL programming.

While you're in the code-behind file, add the single-line event handler for the SelectedIndexChanged event of the grid gvProducts, also listed in Example 11.9, "Products.aspx.vb event handlers". This displays the details of the selected item.

Also add an event handler to gvProducts for the RowDataBound event. This allows you to apply formatting to the cost display. There is an easier way to set the format in this case, which you will use later in the chapter, but this demonstrates a really powerful technique that comes in handy with almost every project. That technique involves looking at each row as it is bound to the data and applying some formatting on a row-by row basis. It is even possible to make different formatting decisions based on the content of each row.

Finally, add an event handler for the SelectedIndexChanged of the RadioButtonList rblCategories, which hides the detail Panel when a new category is selected. This prevents the details of the previous item remaining displayed.

Example 11.9. Products.aspx.vb event handlers

Protected Sub btnAddToCart_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs)
    ' the contents of the cart will be saved in a Session object as
    '     a string of comma-delimited values of ProductID's
    Dim strCart As String = String.Empty
    Dim strProductId As String = gvProducts.SelectedDataKey.Value.ToString()

    If Session("Cart") Is Nothing Then
        strCart = strProductId
    Else
        strCart = Session("Cart").ToString() + ", " + strProductId
    End If
    Session("Cart") = strCart
    lblCart.Text = strCart
End Sub

Protected Sub gvProducts_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles gvProducts.SelectedIndexChanged
    pnlProduct.Visible = True
End Sub

Protected Sub gvProducts_RowDataBound(ByVal sender As Object, _
        ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
        Handles gvProducts.RowDataBound
    Dim str As String = String.Empty
    If e.Row.RowType = DataControlRowType.DataRow Then
        Dim cell As TableCell = e.Row.Cells(4) ' ListPrice cell
        Dim nCost As Decimal
        Try
            nCost = CType(cell.Text, Decimal)
            str = nCost.ToString("##,##0.00")
        Catch ex As ApplicationException
            str = "n.a."
        Finally
            cell.Text = str
        End Try
    End If
End Sub

Protected Sub rblCategories_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles rblCategories.SelectedIndexChanged
    pnlProduct.Visible = False
End Sub

The finished Products page, with several items added to the cart, is shown back in Figure 11.6, "The Products page is now finished, and has several items added to the cart.".

Tip

Notice the nifty way that the nCost variable, which is a number, is converted to a string with the proper format, including a comma and a decimal point. The ToString() method allows you to pass in an optional format string to control the appearance of its output. The zeros are placeholders for required digits; the # symbols are placeholders for optional digits.

Adding AJAX

It is easy to spice up the performance of the Products page with a little help from AJAX. All you need to do is wrap the entire contents of the Content control inside an UpdatePanel control. You can do this by dragging an UpdatePanel control from the AJAX Extensions section of the Toolbox onto the page in Design view, and then dragging all the existing content inside the UpdatePanel. Alternatively, go to Source view and add the following highlighted lines of code, wrapping the content of the Content control:

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1"
         Runat="Server">
    <asp:UpdatePanel id="UpdatePanel1" runat="server">
        <ContentTemplate>

               ... all the content goes here ...
        </ContentTemplate>
    </asp:UpdatePanel>
</asp:Content>

The ScriptManager control should be on the master page, so you don't have to add one to every content page. So, open MasterPage.master and drag a ScriptManager control onto the page, if it is not already there. Run the page to make sure everything works.

Cart Page

The Cart page displays the contents of the cart and allows you to remove items from the cart. It also provides a button to purchase the items in the cart, which would, of course, take you to the Purchase page. The finished page can be seen in Figure 11.11, "Here's what the cart page looks like after you've added some items to the cart.".

Tip

Of course, a full-featured cart would provide much more functionality than the simple cart shown here. For example, your fully featured commercial site might use personalization to remember what was added to the cart in previous sessions and restore that information in a new session. It would almost certainly allow the user to change the quantity ordered of a given item, not to mention things such as size or color.

Figure 11.11. Here's what the cart page looks like after you've added some items to the cart.

Here's what the cart page looks like after you've added some items to the cart.

Open Cart.aspx. Drag a SqlDataSource control onto the content area of the page. Set its ID to sqlCart. Configure it similar to the SqlDataSource shown previously in Example 11.8, "Panel pnlProduct on Products page". Here is the markup for the control. It looks complex, but really it is a straightforward SELECT statement against the Production.Product table, with five joins:

<asp:SqlDataSource ID="sqlCart" runat="server"
    ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
    SelectCommand= "select product.ProductID, product.Name, product.ProductNumber,
            product.Color,
            subcat.Name as SubcategoryName, cat.Name as CategoryName,
            description.Description
            from Production.Product product
            join Production.ProductSubcategory subcat on
                product.ProductSubcategoryID = subcat.ProductSubcategoryID
            join Production.ProductCategory cat on
                subcat.ProductCategoryID = cat.ProductCategoryID
            join Production.ProductModel model on
                product.ProductModelID = model.ProductModelID
            join Production.ProductModelProductDescriptionCulture culture on
                model.ProductModelID = culture.ProductModelID
            join Production.ProductDescription description on
                culture.ProductDescriptionID = description.ProductDescriptionID">
</asp:SqlDataSource>

Notice the use of aliases in the query above. This allows you to control the name of the columns returned. There are two columns called Name in this query, from two different tables, which would conflict if you used them both, so you alias subcat.Name as subcategoryName and cat.Name as categoryName. Another type of alias is used with the table names in the from and join clauses, where we alias Production.Product as product and Production.ProductSubcategory as subcat.

This query needs a WHERE clause. The parameter in the WHERE clause needs to come from the Session object. ASP.NET actually makes this really easy under some circumstances-but unfortunately not these circumstances, as we will now describe.

You saw previously in Example 11.8, "Panel pnlProduct on Products page" the use of the <SelectParameters> element of the SqlDataSource, reproduced here, with a parameter based on the value of another control on the page:

<SelectParameters>
    <asp:ControlParameter ControlID="gvProducts"
        Name="ProductID" PropertyName="SelectedDataKey.Values['ProductID']" />
</SelectParameters>

There are other types of SelectParameters controls, including a SessionParameter, which comes from a Session object. The reason that will not work here is due to a "quirk" of the SQL statement used to construct the query. We'll explain.

The cart is stored in a string as a comma-separated list of ProductIDs, which are stored in the database as integers. The query sent to the database has a where clause using the in keyword, as in:

where product.ProductID in (753,845,143) and culture.CultureID = 'en'

This where clause is created by the sqlDataSource control; you don't have to type it in. The two halves of this where clause derive from the join specified in the SelectCommand of the SqlDataSource control.

SQL Server knows that ProductID is an integer and is able to parse the contents of the parentheses as a list of integers. However, when you use the SessionParameter control, it encloses the contents of the parentheses with quotes, as in:

where product.ProductID in ("753,845,143") and culture.CultureID = 'en'

The quotes make it a string, and SQL Server cannot parse it as a set of integers. There may be a way to deal with this in SQL, but it is easier, and more instructive, to work around this by writing a handler for the Selecting event of the SqlDataSource control. This event is raised just before the query is sent to the database, and is a convenient time to modify the query.

Add the code from Example 11.10, "Cart.aspx.vb event handlers" to handle this event (as well as events for two controls you will place on the page in just a moment) to Cart.aspx.vb. It retrieves the Session object and constructs the where clause, setting the CommandText subproperty of the event argument's Command property. Because this event is raised before the query is executed, changing the CommandText of the query allows you to modify the query before it is run; using this technique, you can have the WHERE clause refer to a specific ProductID.

Warning

This example is not as secure as it should be for a production application. At the least, you would want to be careful of passing sensitive information in Session this way. When you're constructing SQL statements, all values should be validated and tested to prevent SQL injection attacks, which is well beyond the scope of this book.

Example 11.10. Cart.aspx.vb event handlers

Protected Sub sqlCart_Selecting(ByVal sender As Object, _
        ByVal e As System.Web.UI.WebControls.SqlDataSourceSelectingEventArgs) _
        Handles sqlCart.Selecting

   Trace.Warn("sqlCart_Selecting") '        to aid in debugging

   Dim strCart As String = String.Empty
   If Session("Cart") IsNot Nothing Then
       strCart = Session("Cart").ToString
        e.Command.CommandText &= " where product.ProductID in (" + _
           strCart + _
           ") and culture.CultureID = 'en' "
   Else
        e.Cancel = True
   End If
End Sub

Protected Sub btnPurchase_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles btnPurchase.Click
    Response.Redirect("Purchase.aspx")
End Sub

Protected Sub gvCart_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles gvCart.SelectedIndexChanged
    ' this method is actually hooked to the Remove button & is removing items from the
cart
    Dim strProductID As String = gvCart.SelectedRow.Cells(1).Text
    If Session("Cart") IsNot Nothing Then
        '  remove the selected ProductID from the Session string
        '  Retrieve the session string.
        Dim strCart As String = Session("Cart").ToString()
        Dim arIDs As String() = strCart.Split({",")

        ' iterate through the ID's comprising the string array
        '  rebuild the cart string, leaving out the matching ID
        strCart = String.Empty
        For Each str As String In arIDs
            '  use Trim to remove leading and trailing spaces
            If str.Trim() <> strProductID.Trim() Then
                 strCart += str + ", "
            End If
        Next

        ' remove the trailing space and comma
        If strCart.Length > 1 Then
            strCart = strCart.Trim()
            strCart = strCart.Substring(0, strCart.Length - 1)
        End If

        ' put it back into Session
        Session("Cart") = strCart

        ' rebind the GridView, which will force the SqlDataSource to requery
        gvCart.DataBind()
    End If        '  close for test for Session
End Sub

In the sqlCart_Selecting event handler, we test to see if the Session has data. If it does, we add a where clause to the SQL command by modifying the commandText property of the Command object of the SqlDataSourceSelectingEventArgs, called e. If the Session doesn't contain data, the event handler is cancelled by setting the Cancel property of e to True.

Now, drag a GridView onto the page, setting its ID to gvCart. Configure it similar to the previous GridView. Here is the markup for gvCart:

<asp:GridView ID="gvCart" runat="server"
        DataSourceID="sqlCart"
        AllowPaging="True" AllowSorting="True" Width="100%"
        AutoGenerateColumns="False"
        HeaderStyle-CssClass="TableColumnHeading"
        RowStyle-CssClass="TableCells">
    <Columns>
        <asp:CommandField ShowSelectButton="True" SelectText="Remove"
            ControlStyle-CssClass="ButtonSelect" ItemStyle-Width="50px"
                ItemStyle-HorizontalAlign="Center"/>
        <asp:BoundField DataField="ProductID" HeaderText="ID"
                ItemStyle-Width="50px"/>
        <asp:BoundField DataField="ProductNumber" HeaderText="Product Number"
                ItemStyle-Width="90px" />
        <asp:BoundField DataField="Color" HeaderText="Color"
                ItemStyle-Width="60px" />
        <asp:BoundField DataField="CategoryName" HeaderText="Cat"
                ItemStyle-Width="75px" />
        <asp:BoundField DataField="SubcategoryName" HeaderText="SubCat"
                ItemStyle-Width="75px" />
        <asp:BoundField DataField="Description" HeaderText="Description" />
    </Columns>
</asp:GridView>

Below the GridView place an HTML <br /> element and an ASP.NET Button control called btnPurchase:

<br />
<asp:Button ID="btnPurchase" runat="server" Text="Purchase Items in the Cart"
            CssClass="ButtonText"/>

The code to handle the Click event of this button is included in Example 11.10, "Cart.aspx.vb event handlers". All this event handler does is redirect readers to the Purchase page, which you'll create shortly.

The Remove button on each row of the GridView was not a normal ASP.NET Button control, but rather a CommandField with its SelectText property set to Remove. Clicking a CommandField in a GridView selects that row of the grid. This is handled with the gvCart_SelectedIndexChanged event handler, included previously in Example 11.10, "Cart.aspx.vb event handlers".

Run through the app, logging in and adding some items to the cart. Then switch to the Cart page. You will see something similar to Figure 11.11, "Here's what the cart page looks like after you've added some items to the cart.", shown earlier.

Purchase Page

Clicking on the Purchase button on the Cart page brings you to the Purchase page. This page, as shown in Figure 11.12, "The Purchase page looks like this after you've entered information, including an invalid zip code, and then clicked Buy Now.", is used to gather billing and shipping information from the customer. It has a layout table with a bunch of TextBox controls, a couple of RadioButtonLists, a Buy Now button, and a bunch of associated validation controls.

The first row of the layout table is just a heading. The second row collects the Name. This is a required field, so it has a RequiredFieldValidator.

<table border="0" class="TableRowHeading">
    <tr>
        <td colspan="4">
            Billing Information
        </td>
    </tr>
    <tr>
        <td>Name</td>
        <td colspan="3">
            <asp:TextBox ID="txtName" runat="server" Width="250" />
            <asp:RequiredFieldValidator ID="rfName" runat="server"
                ControlToValidate="txtName"
                Display="Dynamic" ErrorMessage="Name is a required field."
                CssClass="ValidationError">*</asp:RequiredFieldValidator></td>
    </tr>

All the validation controls on this page will use dynamic display, so room will only be allocated on the page if it is necessary to display the validation text. For this and all the other validation controls, the validation text is simply an asterisk to display next to the control with invalid input. A ValidationSummary control at the bottom of the page will gather all the ErrorMessages into a single location.

Figure 11.12. The Purchase page looks like this after you've entered information, including an invalid zip code, and then clicked Buy Now.

The Purchase page looks like this after you've entered information, including an invalid zip code, and then clicked Buy Now.

The next row is the Address, which is very similar to the Name row:

<tr>
    <td>Address</td>
    <td colspan="3">
        <asp:TextBox ID="txtAddress" runat="server" Width="250" />
        <asp:RequiredFieldValidator ID="rfAddress" runat="server"
            ControlToValidate="txtAddress"
            Display="Dynamic" ErrorMessage="Address is a required field."
            CssClass="ValidationError">*</asp:RequiredFieldValidator></td>
</tr>

Next is a row for both City and State. City is a straightforward TextBox, just like Name and Address. However, the State control is a DropDownList that is populated from the database. A SqlDataSource is used to populate this DropDownList and another one further down used as part of the shipping address with a list of state names from the database. It is a very simple query; there are no parameters necessary:

<tr>
    <td>City</td>
    <td style="width: 181px">
        <asp:TextBox ID="txtCity" runat="server" />
        <asp:RequiredFieldValidator ID="rfCity" runat="server"
            ControlToValidate="txtCity"
            Display="Dynamic" ErrorMessage="City is a required field."
            CssClass="ValidationError">*</asp:RequiredFieldValidator>
    </td>
    <td colspan="2">
        <asp:SqlDataSource ID="sqlStates" runat="server"
            ConnectionString=
                "<%$ ConnectionStrings:AdventureWorksConnectionString %>"
            SelectCommand="SELECT StateProvinceCode, [Name]
                    FROM Person.StateProvince
                    WHERE CountryRegionCode = 'US' order by [Name]">
        </asp:SqlDataSource>
        <asp:DropDownList ID="ddlStates" runat="server"
            DataSourceID="sqlStates"
            DataTextField="Name" DataValueField="StateProvinceCode" />
    </td>
</tr>

The next row gathers the zip code, validated by a RegularExpressionValidator to be a valid U.S. zip code, as well as being required. The regular expression requires either five digits or five digits plus four more separated by a dash:

<tr>
    <td>Zip</td>
    <td style="width: 181px" colspan="3">
        <asp:TextBox ID="txtZip" runat="server" />
        <asp:RequiredFieldValidator ID="rfZip" runat="server"
            ControlToValidate="txtZip"
            Display="Dynamic" ErrorMessage="Zip is a required field."
            CssClass="ValidationError">*</asp:RequiredFieldValidator>
        <asp:RegularExpressionValidator ID="reZip" runat="server"
            ErrorMessage="Invalid Zip format"
            ControlToValidate="txtZip"
            Display="Dynamic"
            ValidationExpression="^\d{5}$|^\d{5}-\d{4}$"
            CssClass="ValidationError">*</asp:RegularExpressionValidator>
    </td>
</tr>

Next is a row to gather credit card information. A RadioButtonList allows the user a choice of credit card type, and again validates that the user makes a choice.

Warning

Entering credit card numbers in a web site invites fraud. This example makes no pretense of preventing that fraud. A production-quality site would validate the input, including checksums that are built into the credit card number itself. Furthermore, each of the credit card processing companies has its own requirements for what constitutes valid data.

<tr>
    <td>Card</td>
    <td colspan="3" >
        <asp:RadioButtonList ID="rblCardType" runat="server"
                RepeatDirection="Horizontal">
            <asp:ListItem Value="am" Text="American Express" />
            <asp:ListItem Value="d" Text="Discover" />
            <asp:ListItem Value="mc" Text="MasterCard" />
            <asp:ListItem Value="v" Text="Visa" />
        </asp:RadioButtonList>
        <asp:RequiredFieldValidator ID="rfCreditCard" runat="server"
            ErrorMessage="Credit Card type is missing."
            ControlToValidate="rblCardType" Display="Dynamic"
            InitialValue="
            CssClass="ValidationError">*</asp:RequiredFieldValidator>
    </td>
</tr>

The next row gathers the credit card number and security code. Both are required and both use a RegularExpressionValidator to ensure valid formats:

<tr>
    <td>CC #</td>
    <td style="width: 181px">
        <asp:TextBox ID="txtCCNumber" runat="server" />
        <asp:RequiredFieldValidator ID="rfCCNumber" runat="server"
            ControlToValidate="txtCCNumber"
            Display="Dynamic"
            ErrorMessage="Credit Card Number is a required field."
            CssClass="ValidationError">*</asp:RequiredFieldValidator>
        <asp:RegularExpressionValidator ID="reCCNumber" runat="server"
            ErrorMessage="Invalid Credit Card Number"
            ControlToValidate="txtCCNumber"
            Display="Dynamic"
            ValidationExpression=
                "^(\d{4}-){3}\d{4}$|^(\d{4} ){3}\d{4}$|^\d{16}$"
            CssClass="ValidationError">*</asp:RegularExpressionValidator>
    </td>
    <td align="right">Security Code</td>
    <td>
        <asp:TextBox ID="txtSecurityCode" runat="server" />
        <asp:RequiredFieldValidator ID="rfSecurityCode" runat="server"
            ControlToValidate="txtSecurityCode"
            Display="Dynamic"
            ErrorMessage="Security Code is a required field."
            CssClass="ValidationError">*</asp:RequiredFieldValidator>
        <asp:RegularExpressionValidator ID="reSecurityCode" runat="server"
            ErrorMessage="Invalid Security Code"
            ControlToValidate="txtSecurityCode"
            Display="Dynamic"
            ValidationExpression="^\d{3}$"
            CssClass="ValidationError">*</asp:RegularExpressionValidator>
    </td>
</tr>

The credit card number formats allowed are any of the following:

1234-1234-1234-1234
1234 1234 1234 1234
1234123412341234

Warning

It drives me batty when web sites require a credit card number with no spaces or dashes. It is so easy to accept those characters and just remove them before submission, and it would greatly reduce input errors. Long numbers are much easier to enter and read when grouped by intervening spaces or dashes.

The security number is simply a three-digit number.

The next row contains a RadioButtonList to give the user the choice of shipping to the billing address or a different shipping address. Depending on the selected value of that control, a Panel control containing a field for the shipping address is either made visible or not. The code for doing this is contained in an event handler for the SelectedIndexChanged event of rblShippingAddress, included later in Example 11.11, "Purchase.aspx.vb event handlers".

<tr>
    <td colspan="2">
        Shipping Information
    </td>
    <td colspan="2">
        <asp:RadioButtonList ID="rblShippingAddress" runat="server"
            AutoPostBack="true" RepeatDirection="Horizontal">
            <asp:ListItem Value="billing" Text="Ship to Billing Address"
                    Selected="True" />
            <asp:ListItem Value="different"
                    Text="Ship to Different Address" />
        </asp:RadioButtonList>
    </td>
</tr>

AutoPostBack is set to true so that the page will respond immediately when the user changes the selection. If a different address is required, then a Panel contained in the next row is made visible:

<tr>
    <td colspan="4">
        <asp:Panel ID="pnlShippingAddress" runat="server" Visible="false" >
            <table border="0">
                <tr>
                    <td>Address</td>
                    <td colspan="3">
                        <asp:TextBox ID="txtShippingAddress" runat="server"
                            Width="250" />
                    </td>
                </tr>
                <tr>
                    <td>City</td>
                    <td>
                        <asp:TextBox ID="txtShippingCity" runat="server" />
                    </td>
                    <td>
                        <asp:DropDownList ID="ddlShippingStates"
                            runat="server"
                            DataSourceID="sqlStates"
                            DataTextField="Name"
                            DataValueField="StateProvinceCode" />
                    </td>
                    <td>Zip</td>
                    <td>
                        <asp:TextBox ID="txtShippingZip" runat="server" />
                        <asp:RegularExpressionValidator ID="reShippingZip"
                            runat="server"
                            ErrorMessage="Invalid Zip format"
                            ControlToValidate="txtShippingZip"
                            Display="Dynamic"
                            ValidationExpression="^\d{5}$|^\d{5}-\d{4}$"
                            CssClass="ValidationError">*
                        </asp:RegularExpressionValidator>
                    </td>
                </tr>
            </table>

        </asp:Panel>
    </td>
</tr>

Notice how this Panel control itself contains another table for laying out the controls used to gather the shipping address.

Finally, there is a row to contain the ValidationSummary control:

<tr>
    <td colspan="4">
        <asp:ValidationSummary ID="ValidationSummary1" runat="server"
               CssClass="ValidationError" />
    </td>
</tr>

And one more row to contain the Button for completing the purchase:

    <tr>
        <td colspan="4">
            <asp:Button ID="btnBuy" runat="server" Text="Buy Now"
                   CssClass="ButtonText" />
        </td>
    </tr>
</table>

When the Buy Now button is clicked, a real application would process the order, updating the database as necessary. In our simple example, it will stash the order info in Session in a Dictionary object, and then call the Confirm page for order confirmation. The event handler for the Buy Now button is included in Example 11.11, "Purchase.aspx.vb event handlers".

Note that in order for the Dictionary object to be properly instantiated, you must include the Imports statement. We've placed it at the top of Example 11.11, "Purchase.aspx.vb event handlers", but in Purchase.aspx.vb, the Imports statement must appear before the opening Partial Class Purchase statement.

Example 11.11. Purchase.aspx.vb event handlers

Imports System.Collections.Generic

Protected Sub rblShippingAddress_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles rblShippingAddress.SelectedIndexChanged
    If rblShippingAddress.SelectedValue = "billing" Then
        pnlShippingAddress.Visible = False
    Else
        pnlShippingAddress.Visible = True
    End If
End Sub

Protected Sub btnBuy_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles btnBuy.Click
    ' stash all the info in a dictionary object going to Session
    Dim dictBuy As Dictionary(Of String, String) = _
        New Dictionary(Of String, String)
    dictBuy.Add("Name", txtName.Text)
    dictBuy.Add("Address", txtAddress.Text)
    dictBuy.Add("City", txtCity.Text)
    dictBuy.Add("State", ddlStates.SelectedValue)
    dictBuy.Add("Zip", txtZip.Text)
    dictBuy.Add("Card", rblCardType.SelectedValue)
    dictBuy.Add("CardNumber", txtCCNumber.Text)
    dictBuy.Add("SecurityCode", txtSecurityCode.Text)

    If rblShippingAddress.SelectedValue = "billing" Then
        dictBuy.Add("ShippingAddress", txtAddress.Text)
        dictBuy.Add("ShippingCity", txtCity.Text)
        dictBuy.Add("ShippingState", ddlStates.SelectedValue)
        dictBuy.Add("ShippingZip", txtZip.Text)
    Else
        dictBuy.Add("ShippingAddress", txtShippingAddress.Text)
        dictBuy.Add("ShippingCity", txtShippingCity.Text)
        dictBuy.Add("ShippingState", ddlShippingStates.SelectedValue)
        dictBuy.Add("ShippingZip", txtShippingZip.Text)
    End If

    Session("BuyerInfo") = dictBuy

    Response.Redirect("Confirm.aspx")
End Sub

Just as you did with the Product page, you can spiff up the user experience by wrapping the entire contents of the Content control inside an UpdatePanel, as shown in the following code snippet:

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1"
         Runat="Server">
    <asp:UpdatePanel id="UpdatePanel1" runat="server">
        <ContentTemplate>

               ... all the content goes here ...
        </ContentTemplate>
    </asp:UpdatePanel>
</asp:Content>

Now, run the web site and navigate to the Purchase page, as shown previously in Figure 11.12, "The Purchase page looks like this after you've entered information, including an invalid zip code, and then clicked Buy Now.", after filling in most of the fields along with an invalid zip code.

Confirm Page

The Confirm page in this example does nothing more than retrieve the two Session objects, one containing the cart and one containing the buyer information, and display them on the page, as shown in the finished page in Figure 11.13, "Here's the last page the user will see: the confirmation page. All the information from the Cart and the Purchase page is passed here and displayed again.". The cart is displayed in a GridView and the buyer information is displayed in a ListBox.

Figure 11.13. Here's the last page the user will see: the confirmation page. All the information from the Cart and the Purchase page is passed here and displayed again.

Here's the last page the user will see: the confirmation page. All the information from the Cart and the Purchase page is passed here and displayed again.

Again, the page contains an HTML table for layout. The first row contains a GridView and its associated data source for the cart information:

<table>
   <tr>
      <td valign="top" class="ListHeading">
         Cart:
      </td>
      <td valign="top">
         <asp:SqlDataSource ID="sqlCart Confirm" runat="server"
           ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
            SelectCommand="select ProductID, Name, ProductNumber, Color, ListPrice
                     from Production.Product "></asp:SqlDataSource>
         <asp:GridView ID="gvCart" runat="server"
            DataSourceID="sqlCartConfirm" AllowPaging="True"
            AllowSorting="True" HeaderStyle-CssClass="TableColumnHeading"
            RowStyle-CssClass="TableCells" AutoGenerateColumns="false">
            <Columns>
               <asp:BoundField DataField="ProductID" HeaderText="ID" />
               <asp:BoundField DataField="Name" HeaderText="Name" />
               <asp:BoundField DataField="ProductNumber" HeaderText="Product #" />
               <asp:BoundField DataField="Color" HeaderText="Color" />
               <asp:BoundField DataField="ListPrice" HeaderText="Cost"
                  DataFormatString="{0:F2}" HtmlEncode="false" />
            </Columns>
         </asp:GridView>
      </td>
   </tr>

The SelectCommand of the SqlDataSource is updated with the contents of the Cart Session object in Confirm.aspx.vb, exactly as was done for the Cart page, using the Selecting event of the SqlDataSource control: Protected Sub sqlCartConfirm_Selecting(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.SqlDataSourceSelectingEventArgs) _ Handles sqlCartConfirm.Selecting

    Trace.Warn("sqlCartConfirm_Selecting") ' aid in debugging

    If Session("Cart") IsNot Nothing Then
        Dim strCart = Session("Cart").ToString
        e.Command.CommandText &= "where ProductID in (" + _
            strCart + ")"
    Else
         e.Cancel = True
    End If

End Sub

Back in Confirm.aspx, after a spacing row, the next row contains a ListBox for the buyer information:

    <tr>
        <td colspan="2">&nbsp;</td>
    </tr>
    <tr>
        <td valign="top" class="ListHeading">Buyer Info:</td>
        <td valign="top">
            <asp:ListBox ID="lbBuyerInfo" runat="server" Rows="12"
                Width="250" />
        </td>
    </tr>
</table>

The ListBox is populated in Page_Load the first time the page is loaded:

Protected Sub Page_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
    If User.Identity.IsAuthenticated = False Then
       Response.Redirect("login.aspx")
    End If

    Me.Master.PageSubTitle.Text = "Confirmation"

    If Not IsPostBack Then
        lbBuyerInfo.Items.Clear()
        If Session("BuyerInfo") IsNot Nothing Then
            Dim dictBuyerInfo As Dictionary(Of String, String) = Nothing
            dictBuyerInfo = CType(Session("BuyerInfo"), _
                Dictionary(Of String, String))
            For Each key As String In dictBuyerInfo.Keys
                lbBuyerInfo.Items.Add(key + ": " + dictBuyerInfo(key))
            Next
        Else
            lbBuyerInfo.Items.Add("There is no buyer info.")
        End If
    End If
End Sub

In order for the Dictionary to work in this method, you need to add the following line at the top of the code-behind file to import the proper namespace:

Imports System.Collections.Generic

The markup for this page is shown later in Example 11.14, "Confirm.aspx", and the code-behind is in Example 11.15, "Confirm.aspx.vb".

Running the site and navigating through the entire purchase process brings you to the confirmation page shown previously in Figure 11.13, "Here's the last page the user will see: the confirmation page. All the information from the Cart and the Purchase page is passed here and displayed again.".

Custom Error Pages

In case of any errors, you don't want your users to see the ugly generic error page provided by ASP.NET, so you will add some custom error pages, just like you did in Chapter 8, Errors, Exceptions, and Bugs, Oh My!. To do so, add the following section to the web.config file within the <system.web> section:

<!-- Valid values of customErrors mode: On, Off, RemoteOnly -->
<customErrors mode="RemoteOnly" defaultRedirect="CustomErrorPage.aspx">
    <error statusCode="400" redirect="CustomErrorPage400.aspx"/>
    <error statusCode="404" redirect="CustomErrorPage404.aspx"/>
    <error statusCode="500" redirect="CustomErrorPage500.aspx"/>
</customErrors>

This will provide for specific error pages to cover errors 400, "Bad Request," the ubiquitous 404, "Not Found," and the dreaded 500, "Internal Server Error." It will also specify a generic error page for any error not specifically covered. Setting the mode to RemoteOnly means while working on your local machine, you will see the generic error page, with all its helpful information, but remote users will see your custom error pages.

Tip

Notice that here the custom error pages have an extension of .aspx, rather than the .htm used in Chapter 8, Errors, Exceptions, and Bugs, Oh My!. This is so they can take advantage of the master pages.

Now you need to actually create those error pages. Add four new pages to the web site called CustomErrorPage.aspx, CustomErrorPage400.aspx, CustomErrorPage404.aspx, and CustomErrorPage500.aspx. Be sure to check the checkboxes for "Place code in a separate file" and "Select master page."

In the markup file for each of these new pages, add the following MasterType directive, after the Page directive but before the opening <asp:Content> tag:

<%@ MasterType TypeName="MasterPage" %>

This will allow each page to modify the master page, setting the page subtitle appropriately. To do this, add the following Page_Load method to each page:

Protected Sub Page_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
    Me.Master.PageSubTitle.Text = "Error"
End Sub

Finally, add some content to each page to indicate what the error is and what to do about it. While you are at it, add a HyperLink to take the user back to the home page.

Tip

The markup for the error pages is too trivial to waste space in the book. They are included with the code download from www.LibertyAssociates.com.

Summary

There you have it-a functional web site with user registration, data access, session state, and a consistent look and feel, all coded by you. You can now go out and create sites that you didn't dream were possible just a short time ago. There are more features covered in this book that you could add to this site, such as more complex validation, user profiles, and LINQ. This example is already fairly extensive, and provides a good start for additional exploration in ASP.NET.

Don't let this be the end of your learning, though. Although you're quite familiar with most of the controls we've discussed, they also have plenty of properties that you can still discover on your own. Experiment with the examples and exercises in this book to see what's possible. The Web is full of ASP.NET resources to continue your education-the AJAX community is adding new extenders all the time, just to pick one example. And, of course, there are other fine books out there, including Programming ASP.NET (O'Reilly), to help you learn about the advanced controls.

Source Code Listings

This section contains complete source code listings for the entire site. The style sheet, StyleSheet.css, is listed in Example 11.1, "StyleSheet.css". The site map file, Web.sitemap, is listed in Example 11.7, "Web.sitemap". Examples Example 11.12, "Cart.aspx" through Example 11.25, "web.config" show the markup and code-behind files for each of the pages in the site.

Cart Page

Example 11.12. Cart.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Cart.aspx.vb" Inherits="Cart" Title="Cart Page" Trace="false" %>

<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <asp:SqlDataSource ID="sqlCart" runat="server"
      ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
      SelectCommand="select product.ProductID, product.Name, product.ProductNumber,
            product.Color,
            subcat.Name as SubcategoryName, cat.Name as CategoryName,
            description.Description
            from Production.Product product
            join Production.ProductSubcategory subcat on
                product.ProductSubcategoryID = subcat.ProductSubcategoryID
            join Production.ProductCategory cat on
                subcat.ProductCategoryID = cat.ProductCategoryID
            join Production.ProductModel model on
                product.ProductModelID = model.ProductModelID
            join Production.ProductModelProductDescriptionCulture culture on
                model.ProductModelID = culture.ProductModelID
            join Production.ProductDescription description on
                culture.ProductDescriptionID = description.ProductDescriptionID">
   </asp:SqlDataSource>
   <asp:GridView ID="gvCart" runat="server" DataSourceID="sqlCart" AllowPaging="True"
      AllowSorting="True" Width="100%" AutoGenerateColumns="False"
      HeaderStyle-CssClass="TableColumnHeading"
      RowStyle-CssClass="TableCells">
      <Columns>
         <asp:CommandField ShowSelectButton="True" SelectText="Remove"
            ControlStyle-CssClass="ButtonSelect"
            ItemStyle-Width="50px" ItemStyle-HorizontalAlign="Center" />
         <asp:BoundField DataField="ProductID" HeaderText="ID" ItemStyle-Width="50px" />
         <asp:BoundField DataField="ProductNumber" HeaderText="Product Number"
            ItemStyle-Width="90px" />
         <asp:BoundField DataField="Color" HeaderText="Color" ItemStyle-Width="60px" />
         <asp:BoundField DataField="CategoryName" HeaderText="Cat"
            ItemStyle-Width="75px" />
         <asp:BoundField DataField="SubcategoryName" HeaderText="SubCat"
            ItemStyle-Width="75px" />
         <asp:BoundField DataField="Description" HeaderText="Description" />
      </Columns>
   </asp:GridView>
   <br />
   <asp:Button ID="btnPurchase" runat="server" Text="Purchase Items in the Cart"
      CssClass="ButtonText" />
</asp:Content>

Example 11.13. Cart.aspx.vb

Partial Class Cart
    Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles Me.Load
      If User.Identity.IsAuthenticated = False Then
         Response.Redirect("login.aspx")
      End If

      Me.Master.PageSubTitle.Text = "Cart"

   End Sub

Protected Sub sqlCart_Selecting(ByVal sender As Object, _
        ByVal e As System.Web.UI.WebControls.SqlData SourceSelectingEventArgs) _
        Handles sqlCart.Selecting

   Trace.Warn("sqlCart_Selecting") '        to aid in debugging

   Dim strCart As String = String.Empty
   If Session("Cart") IsNot Nothing Then
       strCart = Session("Cart").ToString
        e.Command.CommandText &= " where product.ProductID in (" + _
           strCart + _
           ") and culture.CultureID = 'en' "
   Else
        e.Cancel = True
   End If
End Sub

Protected Sub btnPurchase_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles btnPurchase.Click
    Response.Redirect("Purchase.aspx")
End Sub

Protected Sub gvCart_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles gvCart.SelectedIndexChanged
   ' this method is actually hooked to the Remove button &
   '    is removing items from the cart
   Dim strProductID As String = gvCart.SelectedRow.Cells(1).Text
   If Session("Cart") IsNot Nothing Then
     '  remove the selected ProductID from the Session string
     '  Retrieve the session string.
     Dim strCart As String = Session("Cart").ToString( )
     Dim arIDs As String( ) = strCart.Split(New [Char]( ) {","c})

     ' iterate through the ID's comprising the string array
     '  rebuild the cart string, leaving out the matching ID
     strCart = String.Empty
     For Each str As String In arIDs
         '  use Trim to remove leading and trailing spaces
         If str.Trim( ) <> strProductID.Trim() Then
              strCart += str + ", "
         End If
     Next

     ' remove the trailing space and comma
     If strCart.Length > 1 Then
         strCart = strCart.Trim()
         strCart = strCart.Substring(0, strCart.Length - 1)
     End If

     ' put it back into Session
     Session("Cart") = strCart

     ' rebind the GridView, which will force the SqlData Source to requery
     gvCart.DataBind( )
   End If         '  close for test for Session
End Sub

End Class

Confirm Page

Example 11.14. Confirm.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Confirm.aspx.vb" Inherits="Confirm" Title="Confirmation Page" Trace="false"
%>

<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <table>
      <tr>
         <td valign="top" class="ListHeading">
            Cart:
         </td>
         <td valign="top">
            <asp:SqlData Source ID="sqlCartConfirm" runat="server"
               ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
               SelectCommand="select ProductID, Name, ProductNumber, Color, ListPrice
                        from Production.Product "></asp:SqlDataSource>
            <asp:GridView ID="gvCart" runat="server" DataSourceID="sqlCartConfirm"
               AllowPaging="True" AllowSorting="True"
               HeaderStyle-CssClass="TableColumnHeading" RowStyle-CssClass="TableCells"
               AutoGenerateColumns="false">
               <Columns>
                  <asp:BoundField DataField="ProductID" HeaderText="ID" />
                  <asp:BoundField DataField="Name" HeaderText="Name" />
                  <asp:BoundField DataField="ProductNumber" HeaderText="Product #" />
                  <asp:BoundField DataField="Color" HeaderText="Color" />
                  <asp:BoundField DataField="ListPrice" HeaderText="Cost"
                     DataFormatString="{0:F2}" HtmlEncode="false" />
               </Columns>
            </asp:GridView>
         </td>
      </tr>
      <tr>
         <td colspan="2">
            &nbsp;
         </td>
      </tr>
      <tr>
         <td valign="top" class="ListHeading">
            Buyer Info:
         </td>
         <td valign="top">
            <asp:ListBox ID="lbBuyerInfo" runat="server" Rows="12" Width="250" />
         </td>
      </tr>
   </table>

</asp:Content>

Example 11.15. Confirm.aspx.vb

Imports System.Collections.Generic      ' neccesary for Dictionary
Partial Class Confirm
    Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles Me.Load
      If User.Identity.IsAuthenticated = False Then
         Response.Redirect("login.aspx")
      End If

      Me.Master.PageSubTitle.Text = "Confirmation"

      If Not IsPostBack Then
        lbBuyerInfo.Items.Clear( )
        If Session("BuyerInfo") IsNot Nothing Then
            Dim dictBuyerInfo As Dictionary(Of String, String) = Nothing
            dictBuyerInfo = CType(Session("BuyerInfo"),  _
                Dictionary(Of String, String))
            For Each key As String In dictBuyerInfo.Keys
                lbBuyerInfo.Items.Add(key + ": " + dictBuyerInfo(key))
            Next
        Else
            lbBuyerInfo.Items.Add("There is no buyer info.")
        End If
      End If

   End Sub

   Protected Sub sqlCartConfirm_Selecting(ByVal sender As Object, _
       ByVal e As System.Web.UI.WebControls.SqlData SourceSelectingEventArgs) _
       Handles sqlCartConfirm.Selecting

       Trace.Warn("sqlCartConfirm_Selecting") ' aid in debugging
       If Session("Cart") IsNot Nothing Then
           Dim strCart = Session("Cart").ToString
           e.Command.CommandText &= "where ProductID in (" + _
               strCart + ")"
       Else
            e.Cancel = True
      End If

   End Sub

End Class

Home Page

Example 11.16. Home.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Home.aspx.vb" Inherits="Home" Title="Home Page" Trace="false" %>

<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <h2>This Is Your Home Page</h2>
   <div class="TextNormal">
      You can put some stuff about your company here. Perhaps some links. Of course, in
      a real world application, the navigation would probably much more complex. Also,
      the buttons would actually do something, rather than just wave their arms and say
      <span class="TextBold">Look at me!</span>
   </div>
   <asp:Panel ID="pnlEmployee" runat="server" Visible="false">
      <h3>Employee Information</h3>
      <div class="TextNormal">
         This panel should only be visible to users are a members of the <b>Employee</b>
         role. Turning on the visibility of this Panel occurs in the Page_Load event
         handler.
      </div>
   </asp:Panel>
   <asp:Panel ID="pnlManager" runat="server" Visible="false">
      <h3>Manager Information</h3>
      <div class="TextNormal">
         This panel should only be visible to users are a members of the <b>Manager</b>
         role.
         Turning on the visibility of this Panel occurs in the Page_Load event handler.
      </div>
   </asp:Panel>
</asp:Content>

Login Page

Example 11.17. Login.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Login.aspx.vb" Inherits="Login" Title="Login" Trace="false" %>
<%@ MasterType TypeName="MasterPage" %>

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <asp:Login ID="Login1" runat="server" BackColor="#F7F6F3" BorderColor="#E6E2D8"
      BorderPadding="4" BorderStyle="Solid" BorderWidth="1px"
      DestinationPageUrl="~/Home.aspx"
      Font-Names="Verdana" Font-Size="0.8em" ForeColor="#333333">
      <TextBoxStyle Font-Size="0.8em" />
      <LoginButtonStyle BackColor="#FFFBFF"
         BorderColor="#CCCCCC" BorderStyle="Solid" BorderWidth="1px"
         Font-Names="Verdana" Font-Size="0.8em" ForeColor="#284775" />
      <InstructionTextStyle Font-Italic="True" ForeColor="Black" />
      <TitleTextStyle BackColor="#5D7B9D" Font-Bold="True" Font-Size="0.9em"
         ForeColor="White" />
   </asp:Login>
</asp:Content>

Example 11.18. Login.aspx.vb

Partial Class Login
    Inherits System.Web.UI.Page

    Protected Sub Page_Load(ByVal sender As Object, _
            ByVal e As System.EventArgs) Handles Me.Load
         Me.Master.PageSubTitle.Text = "Login"
    End Sub
End Class

Master Page

Example 11.19. MasterPage.master

<%@ Master Language="VB" CodeFile="MasterPage.master.vb"
   Inherits="MasterPage" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
   <title>Adventure Works</title>
   <link href="StyleSheet.css" rel="stylesheet" type="text/css" />
</head>
<body>
   <form id="form1" runat="server">
   <div>
      <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>
      <table border="0">
         <tr>
            <td colspan="4">
               <table>
                  <tr>
                     <td width="10px">
                        &nbsp;
                     </td>
                     <td>
                        <asp:ImageButton ID="ibLogo" runat="server"
                           ImageUrl="~/images/AdventureWorksLogo-250x70.gif"
                           AlternateText="AdventureWorks logo"
                           PostBackUrl="~/Home.aspx" />
                     </td>
                     <td width="10px">
                        &nbsp;
                     </td>
                     <td width="500px" align="right">
                        <span class="PageTitle">Adventure Works</span>
                        <br />
                        <asp:Label ID="lblPageSubTitle" runat="server"
                           CssClass="PageSubTitle" Text="Page SubTitle" />
                        <br />
                        <asp:Label ID="lblTime" runat="server" CssClass="TextXSmall" />
                     </td>
                     <td width="10px">
                        &nbsp;
                     </td>
                  </tr>
                  <tr>
                     <td colspan="5">
                        <hr />
                     </td>
                  </tr>
               </table>

            </td>
         </tr>
         <tr>
            <td width="5px">
               &nbsp;
            </td>
            <td width="150px" valign="top">
               <asp:LoginStatus ID="LoginStatus1" runat="server" CssClass="Hyperlink" />
               <br />
               <asp:LoginView ID="LoginView1" runat="server">
                  <LoggedInTemplate>
                     <span class="WarningRoutine">Welcome</span>
                     <asp:LoginName ID="LoginName1" runat="server"
                        CssClass="WarningRoutine" />
                  </LoggedInTemplate>
                  <AnonymousTemplate>
                     <span class="WarningRoutine">You are not logged in.
                         Please click the login link to log in to this website.</span>
                  </AnonymousTemplate>
               </asp:LoginView>
               <hr />
               <asp:SiteMapData Source ID="SiteMapDataSource1" runat="server"
                   ShowStartingNode="false" />
               <asp:Menu ID="Menu1" runat="server" DataSourceID="SiteMapDataSource1"
                   CssClass="MenuText" />
            </td>
            <td width="5px">
               &nbsp;
            </td>
            <td width="700px" valign="top" bgcolor="yellow">
               <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
               </asp:ContentPlaceHolder>
            </td>
         </tr>
      </table>

   </div>
   </form>
</body>
</html>

Example 11.20. MasterPage.master.vb

Partial Class MasterPage
    Inherits System.Web.UI.MasterPage
Public Property PageSubTitle() As Label
    Get
        Return lblPageSubTitle
    End Get
    Set(ByVal value As Label)
        lblPageSubTitle = value
    End Set
End Property

Protected Sub Page_Load(ByVal sender As Object, _
        ByVal e As System.EventArgs) Handles Me.Load
    If Page.User.Identity.IsAuthenticated = False Then
        Menu1.Enabled = False
    End If

    lblTime.Text = DateTime.Now.ToString()
End Sub
End Class

Products Page

Example 11.21. Products.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Products.aspx.vb" Inherits="Products" Title="Products Page" %>

<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <asp:UpdatePanel ID="UpdatePanel1" runat="server">
      <ContentTemplate>
         <asp:SqlData Source ID="sqlCategories" runat="server"
            ConnectionString="<%$ ConnectionStrings:AdventureWorksConnectionString %>"
            SelectCommand="select Name, ProductCategoryID from Production.ProductCategory
                           order by Name">
         </asp:SqlDataSource>
         <asp:RadioButtonList ID="rblCategories" runat="server" AutoPostBack="True"
            CssClass="LabelSmall"
            DataSourceID="sqlCategories" DataTextField="Name"
            DataValueField="ProductCategoryID"
            RepeatDirection="Horizontal">
         </asp:RadioButtonList>
         <asp:SqlDataSource ID="sqlProducts" runat="server" ConnectionString=
                      "<%$ ConnectionStrings:AdventureWorksConnectionString %>">
            <SelectParameters>
               <asp:ControlParameter ControlID="rblCategories" Name="ProductCategoryID"

                     PropertyName="SelectedValue" />
            </SelectParameters>
         </asp:SqlData Source>
         <asp:GridView ID="gvProducts" runat="server" DataKeyNames="ProductID"
            AllowPaging="True" AllowSorting="True" AutoGenerateColumns="False"
            HeaderStyle-CssClass="TableColumnHeading"
            RowStyle-CssClass="TableCells" DataSourceID="sqlProducts">
            <Columns>
               <asp:CommandField ShowSelectButton="True" ItemStyle-Width="50"
                  ControlStyle-CssClass="ButtonSelect" />
               <asp:BoundField DataField="ProductID" HeaderText="ID"
                  SortExpression="ProductID">
                  <ItemStyle Width="50px" />
               </asp:BoundField>
               <asp:BoundField DataField="Name" HeaderText="Name"
                  SortExpression="Name">
                  <ItemStyle Width="225px" />
               </asp:BoundField>
               <asp:BoundField DataField="ProductNumber" HeaderText="Product Number"
                  SortExpression="ProductNumber">
                  <ItemStyle Width="90px" />
               </asp:BoundField>
               <asp:BoundField DataField="ListPrice" HeaderText="Cost"
                  SortExpression="ListPrice">
                  <HeaderStyle CssClass="TableColumnHeadingRight" />
                  <ItemStyle CssClass="TableNumberDecimal" Width="60px" />
               </asp:BoundField>
            </Columns>
         </asp:GridView>
         <asp:Panel ID="pnlProduct" runat="server" Visible="false">
            <table width="100%">
               <tr>
                  <td valign="top">
                     <asp:Button ID="btnAddToCart" runat="server" Text="Add To Cart"
                        OnClick="btnAddToCart_Click" CssClass="ButtonText" />
                     <div class="ListHeading">Items In Cart</div>
                     <asp:Label ID="lblCart" runat="server" CssClass="TextSmall"
                        Width="90" />
                  </td>
                  <td valign="top">
                     <asp:SqlDataSource ID="sqlDetailsView" runat="server"
                        ConnectionString="<%$ ConnectionStrings:
                        AdventureWorksConnectionString  %>">
                        <SelectParameters>
                           <asp:ControlParameter ControlID="gvProducts" Name="ProductID"
                              PropertyName="SelectedDataKey.Values['ProductID']" />
                        </SelectParameters>
                     </asp:SqlDataSource>
                     <asp:DetailsView ID="DetailsView1" runat="server"
                        DataSourceID="sqlDetailsView"
                        DataKeyNames="ProductID"
                        AutoGenerateRows="false" CssClass="TableCells" BorderWidth="0"
                        FieldHeaderStyle-CssClass="TableRowHeading"
                        CellSpacing="2" CellPadding="2" Width="500px" Height="50px">
                        <Fields>
                           <asp:BoundField DataField="ProductID" HeaderText="Product ID:"
                              SortExpression="ProductID" />
                           <asp:BoundField DataField="Name" HeaderText="Name:"
                              SortExpression="Name" />
                           <asp:BoundField DataField="ProductNumber"
                              HeaderText="Product #:"
                              SortExpression="ProductNumber" />
                           <asp:BoundField DataField="ListPrice" HeaderText="Cost:"
                              SortExpression="ListPrice"
                              DataFormatString="{0:C}" HtmlEn code="false" />
                           <asp:BoundField DataField="Color" HeaderText="Color:"
                              SortExpression="Color" />
                           <asp:BoundField DataField="CategoryName" HeaderText="Category:"
                              SortExpression="CategoryName" />
                           <asp:BoundField DataField="SubcategoryName"
                              HeaderText="SubCategory:"
                              SortExpression="SubcategoryName" />
                           <asp:BoundField DataField="Description"
                              HeaderText="Description:"
                              SortExpression="Description" />
                        </Fields>
                     </asp:DetailsView>
                  </td>
               </tr>
            </table>

         </asp:Panel>
      </ContentTemplate>
   </asp:UpdatePanel>
</asp:Content>

Example 11.22. Products.aspx.vb

Partial Class Products
    Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles Me.Load
      If User.Identity.IsAuthenticated = False Then
         Response.Redirect("login.aspx")
      End If

      Me.Master.PageSubTitle.Text = "Products"

      Dim strCommand As String = String.Empty
      strCommand = "select ProductID, Name, ProductNumber, ListPrice from " + _
                      "Production.Product "
      strCommand += "where ProductSubcategoryID in "
      strCommand += "(select ProductSubcategoryID from " + _
                      "Production.ProductSubcategory "
      strCommand += "where ProductCategoryID = "
      strCommand += "@ProductCategoryID)"
      sqlProducts.SelectCommand = strCommand

      strCommand = String.Empty
      strCommand += "select product.*, subcat.ProductSubcategoryID, " + _
                  "subcat.Name as SubcategoryName, "
      strCommand += "cat.ProductCategoryID, cat.Name as CategoryName, "
      strCommand += "model.Name as ModelName, model.CatalogDescription, " + _
                  "model.Instructions, "
      strCommand += "description.Description "
      strCommand += "from Production.Product product "
      strCommand += "join Production.ProductSubcategory subcat on " + _
                  "product.ProductSubcategoryID = subcat.ProductSubcategoryID "
      strCommand += "join Production.ProductCategory cat on subcat.ProductCategoryID
 = " + _
                  "cat.ProductCategoryID "
      strCommand += "join Production.ProductModel model on product.ProductModelID = " + _
                  "model.ProductModelID "
      strCommand += "join Production.ProductModelProductDescriptionCulture culture on 
" + _
                  "model.ProductModelID = culture.ProductModelID "
      strCommand += "join Production.ProductDescription description on " + _
                  "culture.ProductDescriptionID = description.ProductDescriptionID "
      strCommand += "where product.ProductID = @ProductID and culture.CultureID = 'en' "
      sqlDetailsView.SelectCommand = strCommand

   End Sub

Protected Sub btnAddToCart_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs)
    ' the contents of the cart will be saved in a Session object as
    '     a string of comma-delimited values of ProductID's
    Dim strCart As String = String.Empty
    Dim strProductId As String = gvProducts.SelectedDataKey.Value.ToString()

    If Session("Cart") Is Nothing Then
        strCart = strProductId
    Else
        strCart = Session("Cart").ToString() + ", " + strProductId
    End If
    Session("Cart") = strCart
    lblCart.Text = strCart
End Sub

Protected Sub gvProducts_SelectedIndexChanged(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles gvProducts.SelectedIndexChanged
    pnlProduct.Visible = True
End Sub

Protected Sub gvProducts_RowDataBound(ByVal sender As Object, _
   ByVal e As System.Web.UI.WebControls.GridViewRowEventArgs) _
   Handles gvProducts.RowDataBound
    Dim str As String = String.Empty

    If e.Row.RowType = DataControlRowType.DataRow Then
        Dim cell As TableCell = e.Row.Cells(4) ' ListPrice cell
        Dim nCost As Decimal
        Try
            nCost = CType(cell.Text, Decimal)
            str = nCost.ToString("##,##0.00", Nothing)
        Catch ex As ApplicationException
            str = "n.a."
        Finally
            cell.Text = str
        End Try
    End If
End Sub

Protected Sub rblCategories_SelectedIndexChanged(ByVal sender As Object, _
   ByVal e As System.EventArgs) Handles rblCategories.SelectedIndexChanged
    pnlProduct.Visible = False
End Sub

End Class

Purchase Page

Example 11.23. Purchase.aspx

<%@ Page Language="VB" MasterPageFile="~/MasterPage.master" AutoEventWireup="false"
   CodeFile="Purchase.aspx.vb" Inherits="Purchase" Title="Purchase Page" Trace="false"%>

<%@ MasterType TypeName="MasterPage" %>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="Server">
   <asp:UpdatePanel ID="UpdatePanel1" runat="server">
      <ContentTemplate>
         <table border="0" class="TableRowHeading">
            <tr>
               <td colspan="4">
                  Billing Information
               </td>
            </tr>
            <tr>
               <td>
                  Name
               </td>
               <td colspan="3">
                  <asp:TextBox ID="txtName" runat="server" Width="250" />
                  <asp:RequiredFieldValidator ID="rfName" runat="server"
                     ControlToValidate="txtName"
                     Display="Dynamic" ErrorMessage="Name is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
               </td>
            </tr>
            <tr>
               <td>
                  Address
               </td>
               <td colspan="3">
                  <asp:TextBox ID="txtAddress" runat="server" Width="250" />
                  <asp:RequiredFieldValidator ID="rfAddress" runat="server"
                     ControlToValidate="txtAddress"
                     Display="Dynamic" ErrorMessage="Address is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
               </td>
            </tr>
            <tr>
               <td>
                  City
               </td>
               <td style="width: 181px">
                  <asp:TextBox ID="txtCity" runat="server" />
                  <asp:RequiredFieldValidator ID="rfCity" runat="server"
                     ControlToValidate="txtCity"
                     Display="Dynamic" ErrorMessage="City is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
               </td>
               <td colspan="2">
                  <asp:SqlData Source ID="sqlStates" runat="server"
                     ConnectionString="<%$ ConnectionStrings:
                     AdventureWorksConnectionString %>"
                     SelectCommand="SELECT StateProvinceCode, [Name]
                                FROM Person.StateProvince
                                WHERE CountryRegionCode = 'US' order by [Name]">
                  </asp:SqlDataSource>
                  <asp:DropDownList ID="ddlStates" runat="server"
                     DataSourceID="sqlStates" DataTextField="Name"
                     DataValueField="StateProvinceCode" />
               </td>
            </tr>
            <tr>
               <td>
                  Zip
               </td>
               <td style="width: 181px" colspan="3">
                  <asp:TextBox ID="txtZip" runat="server" />
                  <asp:RequiredFieldValidator ID="rfZip" runat="server"
                     ControlToValidate="txtZip"
                     Display="Dynamic" ErrorMessage="Zip is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
                  <asp:RegularExpressionValidator ID="reZip" runat="server"
                     ErrorMessage="Invalid Zip format"
                     ControlToValidate="txtZip" Display="Dynamic"
                     ValidationExpression="^\d{5}$|^\d{5}-\d{4}$"
                     CssClass="ValidationError">*</asp:RegularExpressionValidator>
               </td>
            </tr>
            <tr>
               <td>
                  Card
               </td>
               <td colspan="3">
                 <asp:RadioButtonList ID="rblCardType" runat="server"
                     RepeatDirection="Horizontal">
                     <asp:ListItem Value="am" Text="American Express" />
                     <asp:ListItem Value="d" Text="Discover" />
                     <asp:ListItem Value="mc" Text="MasterCard" />
                     <asp:ListItem Value="v" Text="Visa" />
                  </asp:RadioButtonList>
                  <asp:RequiredFieldValidator ID="rfCreditCard" runat="server"
                     ErrorMessage="Credit Card type is missing."
                     ControlToValidate="rblCardType" Display="Dynamic"
                        InitialValue="
                        CssClass="ValidationError">*</asp:RequiredFieldValidator>
               </td>
            </tr>
            <tr>
               <td>
                  CC #
               </td>
               <td style="width: 181px">
                  <asp:TextBox ID="txtCCNumber" runat="server" />
                  <asp:RequiredFieldValidator ID="rfCCNumber" runat="server"
                     ControlToValidate="txtCCNumber"
                     Display="Dynamic"
                     ErrorMessage="Credit Card Number is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
                  <asp:RegularExpressionValidator ID="reCCNumber" runat="server"
                     ErrorMessage="Invalid Credit Card Number"
                     ControlToValidate="txtCCNumber" Display="Dynamic"
                     ValidationExpression="^(\d{4}-){3}\d{4}$|^(\d{4} ){3}\d{4}$|^\d{16}$"
                     CssClass="ValidationError">*</asp:RegularExpressionValidator>
               </td>
               <td align="right">
                  Security Code
               </td>
               <td>
                  <asp:TextBox ID="txtSecurityCode" runat="server" />
                  <asp:RequiredFieldValidator ID="rfSecurityCode" runat="server"
                     ControlToValidate="txtSecurityCode"
                     Display="Dynamic" ErrorMessage="Security Code is a required field."
                     CssClass="ValidationError">*</asp:RequiredFieldValidator>
                  <asp:RegularExpressionValidator ID="reSecurityCode" runat="server"
                     ErrorMessage="Invalid Security Code"
                     ControlToValidate="txtSecurityCode" Display="Dynamic"
                     ValidationExpression="^\d{3}$"
                     CssClass="ValidationError">*</asp:RegularExpressionValidator>
               </td>
            </tr>
            <tr>
               <td colspan="2">
                  Shipping Information
               </td>
               <td colspan="2">
                  <asp:RadioButtonList ID="rblShippingAddress" runat="server"
                     AutoPostBack="true" RepeatDirection="Horizontal">
                     <asp:ListItem Value="billing" Text="Ship to Billing Address"
                        Selected="True" />
                     <asp:ListItem Value="different" Text="Ship to Different Address" />
                  </asp:RadioButtonList>
               </td>
            </tr>
            <tr>
               <td colspan="4">
                  <asp:Panel ID="pnlShippingAddress" runat="server" Visible="false">
                     <table border="0">
                        <tr>
                           <td>
                              Address
                           </td>
                           <td colspan="3">
                              <asp:TextBox ID="txtShippingAddress" runat="server"
                                 Width="250" />
                           </td>
                        </tr>
                        <tr>
                           <td>
                              City
                           </td>
                           <td>
                              <asp:TextBox ID="txtShippingCity" runat="server" />
                           </td>
                           <td>
                              <asp:DropDownList ID="ddlShippingStates" runat="server"
                                 Data SourceID="sqlStates"
                                  DataTextField="Name"
                                 DataValueField="StateProvinceCode" />
                           </td>
                           <td>
                              Zip
                           </td>
                           <td>
                              <asp:TextBox ID="txtShippingZip" runat="server" />
                              <asp:RegularExpressionValidator ID="reShippingZip"
                              runat="server"
                                 ErrorMessage="Invalid Zip format"
                                 ControlToValidate="txtShippingZip" Display="Dynamic"
                                 ValidationExpression="^\d{5}$|^\d{5}-\d{4}$"
                                 CssClass="ValidationError">*
                              </asp:RegularExpressionValidator>
                            </td>
                        </tr>
                     </table>

                  </asp:Panel>
               </td>
            </tr>
            <tr>
               <td colspan="4">
                  <asp:ValidationSummary ID="ValidationSummary1" runat="server"
                     CssClass="ValidationError" />
               </td>
            </tr>
            <tr>
               <td colspan="4">
                  <asp:Button ID="btnBuy" runat="server" Text="Buy Now"
                     CssClass="ButtonText" />
               </td>
            </tr>
         </table>

      </ContentTemplate>
   </asp:UpdatePanel>
</asp:Content>

Example 11.24. Purchase.aspx.vb

Partial Class Purchase
    Inherits System.Web.UI.Page

   Protected Sub Page_Load(ByVal sender As Object, _
         ByVal e As System.EventArgs) Handles Me.Load
      If User.Identity.IsAuthenticated = False Then
         Response.Redirect("login.aspx")
      End If

      Me.Master.PageSubTitle.Text = "Purchase"

   End Sub

Protected Sub rblShippingAddress_SelectedIndexChanged(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles rblShippingAddress.SelectedIndexChanged
    If rblShippingAddress.SelectedValue = "billing" Then
        pnlShippingAddress.Visible = False
    Else
        pnlShippingAddress.Visible = True
    End If
End Sub

Protected Sub btnBuy_Click(ByVal sender As Object, _
        ByVal e As System.EventArgs) _
        Handles btnBuy.Click
    ' stash all the info in a dictionary object going to Session
    Dim dictBuy As Dictionary(Of String, String) = _
        New Dictionary(Of String, String)
    dictBuy.Add("Name", txtName.Text)
    dictBuy.Add("Address", txtAddress.Text)
    dictBuy.Add("City", txtCity.Text)
    dictBuy.Add("State", ddlStates.SelectedValue)
    dictBuy.Add("Zip", txtZip.Text)
    dictBuy.Add("Card", rblCardType.SelectedValue)
    dictBuy.Add("CardNumber", txtCCNumber.Text)
    dictBuy.Add("SecurityCode", txtSecurityCode.Text)

    If rblShippingAddress.SelectedValue = "billing" Then
        dictBuy.Add("ShippingAddress", txtAddress.Text)
        dictBuy.Add("ShippingCity", txtCity.Text)
        dictBuy.Add("ShippingState", ddlStates.SelectedValue)
        dictBuy.Add("ShippingZip", txtZip.Text)
    Else
        dictBuy.Add("ShippingAddress", txtShippingAddress.Text)
        dictBuy.Add("ShippingCity", txtShippingCity.Text)
        dictBuy.Add("ShippingState", ddlShippingStates.SelectedValue)
        dictBuy.Add("ShippingZip", txtShippingZip.Text)
    End If

    Session("BuyerInfo") = dictBuy

    Response.Redirect("Confirm.aspx")
End Sub

End Class

Web.config

Example 11.25. web.config

<?xml version="1.0"?>
<configuration>
   <configSections>
      <sectionGroup name="system.web.extensions"
type="System.Web.Configuration.SystemWebExtensionsSectionGroup, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
         <sectionGroup name="scripting" type="System.Web.Configuration.
ScriptingSectionGroup,
System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
            <section name="scriptRe sourceHandler"
type="System.Web.Configuration.ScriptingScriptResourceHandlerSection, System.Web.
Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
requirePermission="false"
allowDefinition="MachineToApplication"/>
            <sectionGroup name="webServices"
type="System.Web.Configuration.ScriptingWebServicesSectionGroup, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
               <section name="jsonSerialization"
type="System.Web.Configuration.ScriptingJsonSerializationSection, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
requirePermission="false"
allowDefinition="Everywhere"/>
               <section name="profileService"
type="System.Web.Configuration.ScriptingProfileServiceSection, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
requirePermission="false"
allowDefinition="MachineToApplication"/>
               <section name="authenticationService"
type="System.Web.Configuration.ScriptingAuthenticationServiceSection, System.Web.
Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
requirePermission="false"
allowDefinition="MachineToApplication"/>
               <section name="roleService"
type="System.Web.Configuration.ScriptingRoleServiceSection, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
requirePermission="false"
allowDefinition="MachineToApplication"/>
            </sectionGroup>
         </sectionGroup>
      </sectionGroup>
   </configSections>
   <appSettings/>
   <connectionStrings>
  <add name="AdventureWorksConnectionString" connectionString="Data Source=.\
sqlexpress;Initial
Catalog=AdventureWorks;Integrated Security=True"
   providerName="System.Data.SqlClient" />
 </connectionStrings>
   <system.web>
      <!--
            Set compilation debug="true" to insert debugging
            symbols into the compiled page. Because this
            affects performance, set this value to true only
            during development.

            Visual Basic options:
            Set strict="true" to disallow all data type conversions
            where data loss can occur.
            Set explicit="true" to force declaration of all variables.
        -->
  <roleManager enabled="true" />
  <compilation debug="true" strict="false" explicit="true">
         <assemblies>
            <add assembly="System.Core, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>
            <add assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>
            <add assembly="System.Data.DataSetExtensions, Version=3.5.0.0,
Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>
            <add assembly="System.Xml.Linq, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=B77A5C561934E089"/>
         </assemblies>
      </compilation>
      <pages>
         <namespaces>
            <clear/>
            <add namespace="System"/>
            <add namespace="System.Collections"/>
            <add namespace="System.Collections.Generic"/>
            <add namespace="System.Collections.Specialized"/>
            <add namespace="System.Configuration"/>
            <add namespace="System.Text"/>
            <add namespace="System.Text.RegularExpressions"/>
            <add namespace="System.Linq"/>
            <add namespace="System.Xml.Linq"/>
            <add namespace="System.Web"/>
            <add namespace="System.Web.Caching"/>
            <add namespace="System.Web.SessionState"/>
            <add namespace="System.Web.Security"/>
            <add namespace="System.Web.Profile"/>
            <add namespace="System.Web.UI"/>
            <add namespace="System.Web.UI.WebControls"/>
            <add namespace="System.Web.UI.WebControls.WebParts"/>
            <add namespace="System.Web.UI.HtmlControls"/>
         </namespaces>
         <controls>
            <add tagPrefix="asp" namespace="System.Web.UI" assembly="System.Web.
Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
            <add tagPrefix="asp" namespace="System.Web.UI.WebControls"
assembly="System.Web.Extensions, Version=3.5.0.0, Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>
         </controls>
      </pages>
      <!--
            The <authentication> section enables configuration
            of the security authentication mode used by
            ASP.NET to identify an incoming user.
        -->
      <authentication mode="Forms" />
      <!--
            The <customErrors> section enables configuration
            of what to do if/when an unhandled error occurs
            during the execution of a request. Specifically,
            it enables developers to configure html error pages
            to be displayed in place of a error stack trace.

        <customErrors mode="RemoteOnly" defaultRedirect="GenericErrorPage.htm">
            <error status Code="403" redirect="NoAccess.htm" />
            <error statusCode="404" redirect="FileNotFound.htm" />
        </customErrors>
        -->
      <!-- Valid values of customErrors mode: On, Off, RemoteOnly -->
      <customErrors mode="RemoteOnly" defaultRedirect="CustomErrorPage.aspx">
         <error statusCode="400" redirect="CustomErrorPage400.aspx"/>
         <error statusCode="404" redirect="CustomErrorPage404.aspx"/>
         <error statusCode="500" redirect="CustomErrorPage500.aspx"/>
      </customErrors>

      <httpHandlers>
         <remove verb="*" path="*.asmx"/>
         <add verb="*" path="*.asmx" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.
0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
         <add verb="*" path="*_AppService.axd" validate="false"
type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.
0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
         <add verb="GET,HEAD" path="ScriptRe source.axd"
type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=3.5.0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false"/>
      </httpHandlers>
      <httpModules>
         <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.
Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </httpModules>
   </system.web>
   <system.codedom>
      <compilers>
         <compiler language="c#;cs;csharp" extension=".cs" warningLevel="4"
type="Microsoft.CSharp.CSharpCodeProvider, System, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
            <providerOption name="CompilerVersion" value="v3.5"/>
            <providerOption name="WarnAsError" value="false"/>
         </compiler>
         <compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" warningLevel="4"
type="Microsoft.VisualBasic.VBCodeProvider, System, Version=2.0.0.0, Culture=neutral,
PublicKeyToken=b77a5c561934e089">
            <providerOption name="CompilerVersion" value="v3.5"/>
            <providerOption name="OptionInfer" value="true"/>
            <providerOption name="WarnAsError" value="false"/>
         </compiler>
      </compilers>
   </system.codedom>
   <!--
        The system.webServer section is required for running ASP.NET AJAX under Internet
        Information Services 7.0. It is not necessary for previous version of IIS.
    -->
   <system.webServer>
      <validation validateIntegratedModeConfiguration="false"/>
      <modules>
         <remove name="ScriptModule"/>
         <add name="ScriptModule" preCondition="managedHandler"
type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0,
Culture=neutral,
PublicKeyToken=31BF3856AD364E35"/>
      </modules>
      <handlers>
         <remove name="WebServiceHandlerFactory-Integrated"/>
         <remove name="ScriptHandlerFactory"/>
         <remove name="ScriptHandlerFactoryAppServices"/>
         <remove name="ScriptResource"/>
         <add name="ScriptHandlerFactory" verb="*" path="*.asmx"
preCondition="integratedMode"
type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=3.5.
0.0,
Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
         <add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd"
preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory,
System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/
>
         <add name="ScriptResource" preCondition="integratedMode" verb="GET,HEAD"
path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.
Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      </handlers>
   </system.webServer>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="System.Web.Extensions"
publicKeyToken="31bf3856ad364e35"/>
            <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
         </dependentAssembly>
         <dependentAssembly>
            <assemblyIdentity name="System.Web.Extensions.Design"
publicKeyToken="31bf3856ad364e35"/>
            <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="3.5.0.0"/>
         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>