The Congo.com Application

The application pictured in Figure 9-13 breathes life into many of the concepts presented in this chapter. Called Congo.com, it鈥檚 a virtual storefront for a fictitious online bookseller. Congo.com鈥檚 catalog consists of titles obtained from the SQL Server Pubs database. The main page, Congo.aspx, fetches the titles from the database and displays them in a DataGrid. Each row in the DataGrid contains an Add to Cart button that, when clicked, adds the corresponding book to a virtual shopping cart. Clicking the View Cart button at the top of the page shows the shopping cart鈥檚 contents, again using a DataGrid. This DataGrid has Remove buttons that delete items from the shopping cart.

Figure 9-13
Congo.com.

Here鈥檚 how to install Congo.com on your Web server:

Once deployment is complete, call up Congo.aspx in your browser and click a few Add to Cart buttons. Then click View Cart to view your shopping cart鈥檚 contents. Now do the same using a second instance of your browser. You鈥檒l find that the two browser instances track items added to the shopping cart independently. Why? Because each represents a separate session and is therefore assigned its own session (and own session state) on the server.

Inside Congo.com

Congo.com鈥檚 source code, shown in Figure 9-14, is remarkably compact considering the amount of functionality it provides. If you don鈥檛 believe me, try coding the application as an ISAPI DLL. You鈥檒l see what I mean.

The action begins in Global.asax. Each time a new user requests a page from the site, ASP.NET creates a session for that user and calls Global.asax鈥檚 Session_Start handler. Session_Start creates a new ShoppingCart object to serve as a container for the user鈥檚 selections and stores a reference to it in session state, keying it with the name 鈥淢yShoppingCart鈥?

Session["MyShoppingCart"]聽=聽new聽ShoppingCart聽();

When the user calls up Congo.aspx and clicks an Add to Cart button, Congo.aspx鈥檚 OnItemCommand method is called on the server. OnItemCommand retrieves the product ID, title, and price of the corresponding book from the DataGrid and encapsulates them in a BookOrder object:

BookOrder聽order聽=聽new聽BookOrder聽(e.Item.Cells[0].Text,
聽聽聽聽e.Item.Cells[1].Text,聽Convert.ToDecimal
聽聽聽聽(e.Item.Cells[2].Text.Substring聽(1)),聽1);

OnItemCommand then retrieves the reference to the user鈥檚 ShoppingCart from session state and adds the BookOrder to the ShoppingCart:

ShoppingCart聽cart聽=聽(ShoppingCart)聽Session["MyShoppingCart"];
if聽(cart聽!=聽null)
聽聽聽聽cart.AddOrder聽(order);

Congo.aspx鈥檚 Page_Load handler populates the DataGrid by binding it to a DataSet holding the results of a database query.

When the user clicks the View Cart button at the top of the page, Congo.com redirects the user to ViewCart.aspx with Response.Redirect:

Response.Redirect聽("ViewCart.aspx");

ViewCart.aspx declares a DataGrid that鈥檚 similar to the one declared in Congo.aspx. But ViewCart.aspx鈥檚 DataGrid control doesn鈥檛 bind to a DataSet encapsulating the results of a database query; it binds to the ShoppingCart object in session state. Here鈥檚 the code that does the binding:

ShoppingCart聽cart聽=聽(ShoppingCart)聽Session["MyShoppingCart"];
聽聽.
聽聽.
聽聽.
MyDataGrid.DataSource聽=聽cart.Orders;
MyDataGrid.DataBind聽();

Clearly, ShoppingCart plays a huge role in Congo.com鈥檚 operation. Not only does it keep a record of the items the user selected, but it implements an Orders property that supports data binding. Where does ShoppingCart come from, and what鈥檚 the magic that enables it to work with data-binding controls?

ShoppingCart is a custom data type defined in Congo.cs. It鈥檚 accompanied by BookOrder, which is also defined in Congo.cs. The ShoppingCart class is basically a wrapper around a Hashtable. It implements a private field named _Orders that holds a Hashtable reference, and public methods that enable BookOrder objects to be added to the Hashtable and removed. It also implements a public property named Orders that exposes the Hashtable鈥檚 ICollection interface:

public聽ICollection聽Orders聽
{
聽聽聽聽get聽{聽return聽_Orders.Values;聽}
}

That鈥檚 why a DataGrid can bind to a ShoppingCart: because its Orders property exposes the underlying Hashtable鈥檚 ICollection interface. The statement

MyDataGrid.DataSource聽=聽cart.Orders;

does nothing more than put the Hashtable鈥檚 ICollection interface into the hands of the DataGrid.

Both ShoppingCart and BookOrder are tagged with Serializable attributes. That鈥檚 so they can be stored in session state regardless of the session state process model selected. As I said earlier, it鈥檚 wise to mark types that you intend to store in session state as serializable so that your source code doesn鈥檛 have to change if the process model changes.

What role does Web.config play in Congo.com鈥檚 operation? It stores the connection string that Congo.aspx uses to connect to the Pubs database. Storing the connection string in Web.config rather than hardcoding it into Congo.aspx enables it to be changed without modifying any C# code.

Web.config
<configuration>
聽聽<appSettings>
聽聽聽聽<add聽key="connectString"
聽聽聽聽聽聽value="server=localhost;database=pubs;uid=sa;pwd=" />
聽聽</appSettings>
</configuration>
Figure 9-14
Congo.com source code.
Global.asax
<script聽language="C#" runat="server">
聽聽void聽Session_Start聽()
聽聽{
聽聽聽聽聽聽Session["MyShoppingCart"]聽=聽new聽ShoppingCart聽();
聽聽}
</script>
Congo.aspx
<%@聽Import聽Namespace="System.Data" %>
<%@聽Import聽Namespace="System.Data.SqlClient" %>

<html>
聽聽<body>
聽聽聽聽<h1>Congo.com</h1>
聽聽聽聽<form聽runat="server">
聽聽聽聽聽聽<table聽width="100%" bgcolor="teal">
聽聽聽聽聽聽聽聽<tr>
聽聽聽聽聽聽聽聽聽聽<td>
聽聽聽聽聽聽聽聽聽聽聽聽<asp:Button聽Text="View聽Cart" OnClick="OnViewCart"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽RunAt="server" />
聽聽聽聽聽聽聽聽聽聽</td>
聽聽聽聽聽聽聽聽</tr>
聽聽聽聽聽聽</table>
聽聽聽聽聽聽<br>
聽聽聽聽聽聽<center>
聽聽聽聽聽聽聽聽<asp:DataGrid聽ID="MyDataGrid"
聽聽聽聽聽聽聽聽聽聽AutoGenerateColumns="false" CellPadding="2"
聽聽聽聽聽聽聽聽聽聽BorderWidth="1" BorderColor="lightgray"
聽聽聽聽聽聽聽聽聽聽Font-Name="Verdana" Font-Size="8pt"
聽聽聽聽聽聽聽聽聽聽GridLines="vertical" Width="90%"
聽聽聽聽聽聽聽聽聽聽OnItemCommand="OnItemCommand" RunAt="server">
聽聽聽聽聽聽聽聽聽聽<Columns>
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Item聽ID"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="title_id" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Title"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="title" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Price"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="price" DataFormatString="{0:c}"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽HeaderStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ItemStyle-HorizontalAlign="right" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:ButtonColumn聽HeaderText="Action" Text="Add聽to聽Cart"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽HeaderStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ItemStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽CommandName="AddToCart" />
聽聽聽聽聽聽聽聽聽聽</Columns>
聽聽聽聽聽聽聽聽聽聽<HeaderStyle聽BackColor="teal" ForeColor="white"
聽聽聽聽聽聽聽聽聽聽聽聽Font-Bold="true" />
聽聽聽聽聽聽聽聽聽聽<ItemStyle聽BackColor="white" ForeColor="darkblue" />
聽聽聽聽聽聽聽聽聽聽<AlternatingItemStyle聽BackColor="beige"
聽聽聽聽聽聽聽聽聽聽聽聽ForeColor="darkblue" />
聽聽聽聽聽聽聽聽</asp:DataGrid>
聽聽聽聽</center>
聽聽聽聽</form>
聽聽</body>
</html>

<script聽language="C#" runat="server">
聽聽void聽Page_Load聽(Object聽sender,聽EventArgs聽e)
聽聽{
聽聽聽聽聽聽if聽(!IsPostBack)聽{
聽聽聽聽聽聽聽聽聽聽string聽ConnectString聽=
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ConfigurationSettings.AppSettings["connectString"];
聽聽聽聽聽聽聽聽聽聽SqlDataAdapter聽adapter聽=聽new聽SqlDataAdapter
聽聽聽聽聽聽聽聽聽聽聽聽聽聽("select聽*聽from聽titles聽where聽price聽!=聽0",聽ConnectString);
聽聽聽聽聽聽聽聽聽聽DataSet聽ds聽=聽new聽DataSet聽();
聽聽聽聽聽聽聽聽聽聽adapter.Fill聽(ds);
聽聽聽聽聽聽聽聽聽聽MyDataGrid.DataSource聽=聽ds;
聽聽聽聽聽聽聽聽聽聽MyDataGrid.DataBind聽();
聽聽聽聽聽聽}
聽聽}

聽聽void聽OnItemCommand聽(Object聽sender,聽DataGridCommandEventArgs聽e)
聽聽{
聽聽聽聽聽聽if聽(e.CommandName聽== "AddToCart")聽{
聽聽聽聽聽聽聽聽聽聽BookOrder聽order聽=聽new聽BookOrder聽(e.Item.Cells[0].Text,
聽聽聽聽聽聽聽聽聽聽聽聽聽聽e.Item.Cells[1].Text,聽Convert.ToDecimal
聽聽聽聽聽聽聽聽聽聽聽聽聽聽(e.Item.Cells[2].Text.Substring聽(1)),聽1);
聽聽聽聽聽聽聽聽聽聽ShoppingCart聽cart聽=聽(ShoppingCart)聽Session["MyShoppingCart"];
聽聽聽聽聽聽聽聽聽聽if聽(cart聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽聽聽cart.AddOrder聽(order);
聽聽聽聽聽聽}
聽聽}

聽聽void聽OnViewCart聽(Object聽sender,聽EventArgs聽e)
聽聽{
聽聽聽聽聽聽Response.Redirect聽("ViewCart.aspx");
聽聽}
</script>
ViewCart.aspx
<html>
聽聽<body>
聽聽聽聽<h1>Shopping聽Cart</h1>
聽聽聽聽<form聽runat="server">
聽聽聽聽聽聽<table聽width="100%" bgcolor="teal">
聽聽聽聽聽聽聽聽<tr>
聽聽聽聽聽聽聽聽聽聽<td>
聽聽聽聽聽聽聽聽聽聽聽聽<asp:Button聽Text="Return聽to聽Shopping" OnClick="OnShop"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽RunAt="server" />
聽聽聽聽聽聽聽聽聽聽</td>
聽聽聽聽聽聽聽聽</tr>
聽聽聽聽聽聽</table>
聽聽聽聽聽聽<br>
聽聽聽聽聽聽<center>
聽聽聽聽聽聽聽聽<asp:DataGrid聽ID="MyDataGrid"
聽聽聽聽聽聽聽聽聽聽AutoGenerateColumns="false" CellPadding="2"
聽聽聽聽聽聽聽聽聽聽BorderWidth="1" BorderColor="lightgray"
聽聽聽聽聽聽聽聽聽聽Font-Name="Verdana" Font-Size="8pt"
聽聽聽聽聽聽聽聽聽聽GridLines="vertical" Width="90%"
聽聽聽聽聽聽聽聽聽聽OnItemCommand="OnItemCommand" RunAt="server">
聽聽聽聽聽聽聽聽聽聽<Columns>
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Item聽ID"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="ItemID" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Title"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="Title" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Price"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="Price" DataFormatString="{0:c}"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽HeaderStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ItemStyle-HorizontalAlign="right" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:BoundColumn聽HeaderText="Quantity"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽DataField="Quantity" 
聽聽聽聽聽聽聽聽聽聽聽聽聽聽HeaderStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ItemStyle-HorizontalAlign="center" />
聽聽聽聽聽聽聽聽聽聽聽聽<asp:ButtonColumn聽HeaderText="Action" Text="Remove"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽HeaderStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽ItemStyle-HorizontalAlign="center"
聽聽聽聽聽聽聽聽聽聽聽聽聽聽CommandName="RemoveFromCart" />
聽聽聽聽聽聽聽聽聽聽</Columns>
聽聽聽聽聽聽聽聽聽聽<HeaderStyle聽BackColor="teal" ForeColor="white"
聽聽聽聽聽聽聽聽聽聽聽聽Font-Bold="true" />
聽聽聽聽聽聽聽聽聽聽<ItemStyle聽BackColor="white" ForeColor="darkblue" />
聽聽聽聽聽聽聽聽聽聽<AlternatingItemStyle聽BackColor="beige"
聽聽聽聽聽聽聽聽聽聽聽聽ForeColor="darkblue" />
聽聽聽聽聽聽聽聽</asp:DataGrid>
聽聽聽聽聽聽</center>
聽聽聽聽聽聽<h3><asp:Label聽ID= "Total" RunAt="server" /></h3>
聽聽聽聽</form>
聽聽</body>
</html>

<script聽language="C#" runat="server">
聽聽void聽Page_Load聽(Object聽sender,聽EventArgs聽e)
聽聽{
聽聽聽聽聽聽ShoppingCart聽cart聽=聽(ShoppingCart)聽Session["MyShoppingCart"];
聽聽聽聽聽聽if聽(cart聽!=聽null)聽{
聽聽聽聽聽聽聽聽聽聽MyDataGrid.DataSource聽=聽cart.Orders;
聽聽聽聽聽聽聽聽聽聽MyDataGrid.DataBind聽();
聽聽聽聽聽聽聽聽聽聽Total.Text聽=聽String.Format聽("Total聽Cost:聽{0:c}",
聽聽聽聽聽聽聽聽聽聽聽聽聽聽cart.TotalCost);
聽聽聽聽聽聽}
聽聽}

聽聽void聽OnItemCommand聽(Object聽sender,聽DataGridCommandEventArgs聽e)
聽聽{
聽聽聽聽聽聽if聽(e.CommandName聽== "RemoveFromCart")聽{
聽聽聽聽聽聽聽聽聽聽ShoppingCart聽cart聽=聽(ShoppingCart)聽Session["MyShoppingCart"];
聽聽聽聽聽聽聽聽聽聽if聽(cart聽!=聽null)聽{
聽聽聽聽聽聽聽聽聽聽聽聽聽聽cart.RemoveOrder聽(e.Item.Cells[0].Text);
聽聽聽聽聽聽聽聽聽聽聽聽聽聽MyDataGrid.DataBind聽();
聽聽聽聽聽聽聽聽聽聽聽聽聽聽Total.Text聽=聽String.Format聽("Total聽Cost:聽{0:c}",
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽cart.TotalCost);
聽聽聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽}
聽聽}

聽聽public聽void聽OnShop聽(Object聽sender,聽EventArgs聽e)
聽聽{
聽聽聽聽聽聽Response.Redirect聽("Congo.aspx");
聽聽}
</script>
Congo.cs
using聽System;
using聽System.Collections;

[Serializable]
public聽class聽BookOrder
{
聽聽聽聽string聽_ItemID;
聽聽聽聽string聽_Title;
聽聽聽聽decimal聽_Price;
聽聽聽聽int聽_Quantity;

聽聽聽聽public聽string聽ItemID聽
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽_ItemID;聽}
聽聽聽聽聽聽聽聽set聽{聽_ItemID聽=聽value;聽}
聽聽聽聽}

聽聽聽聽public聽string聽Title聽
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽_Title;聽}
聽聽聽聽聽聽聽聽set聽{聽_Title聽=聽value;聽}
聽聽聽聽}

聽聽聽聽public聽decimal聽Price
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽_Price;聽}
聽聽聽聽聽聽聽聽set聽{聽_Price聽=聽value;聽}
聽聽聽聽}

聽聽聽聽public聽int聽Quantity聽
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽_Quantity;聽}
聽聽聽聽聽聽聽聽set聽{聽_Quantity聽=聽value;聽}
聽聽聽聽}

聽聽聽聽public聽BookOrder聽(string聽ItemID,聽string聽Title,聽decimal聽Price,
聽聽聽聽聽聽聽聽int聽Quantity)
聽聽聽聽{
聽聽聽聽聽聽聽聽_ItemID聽=聽ItemID;
聽聽聽聽聽聽聽聽_Title聽=聽Title;
聽聽聽聽聽聽聽聽_Price聽=聽Price;
聽聽聽聽聽聽聽聽_Quantity聽=聽Quantity;
聽聽聽聽}
}

[Serializable]
public聽class聽ShoppingCart
{
聽聽聽聽Hashtable聽_Orders聽=聽new聽Hashtable聽();

聽聽聽聽public聽ICollection聽Orders聽
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽{聽return聽_Orders.Values;聽}
聽聽聽聽}
	
聽聽聽聽public聽decimal聽TotalCost
聽聽聽聽{
聽聽聽聽聽聽聽聽get聽
聽聽聽聽聽聽聽聽{
聽聽聽聽聽聽聽聽聽聽聽聽decimal聽total聽=聽0;
聽聽聽聽聽聽聽聽聽聽聽聽foreach聽(DictionaryEntry聽entry聽in聽_Orders)聽{
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽BookOrder聽order聽=聽(BookOrder)聽entry.Value;
聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽total聽+=聽(order.Price聽*聽order.Quantity);
聽聽聽聽聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽聽聽聽聽return聽total;
聽聽聽聽聽聽聽聽}
聽聽聽聽}

聽聽聽聽public聽void聽AddOrder聽(BookOrder聽Order)
聽聽聽聽{
聽聽聽聽聽聽聽聽BookOrder聽order聽=聽(BookOrder)聽_Orders[Order.ItemID];
聽聽聽聽聽聽聽聽if聽(order聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽order.Quantity聽+=聽Order.Quantity;
聽聽聽聽聽聽聽聽else
聽聽聽聽聽聽聽聽聽聽聽聽_Orders.Add聽(Order.ItemID,聽Order);
聽聽聽聽}

聽聽聽聽public聽void聽RemoveOrder聽(string聽ItemID)
聽聽聽聽{
聽聽聽聽聽聽聽聽if聽(_Orders[ItemID]聽!=聽null)
聽聽聽聽聽聽聽聽聽聽聽聽_Orders.Remove聽(ItemID);
聽聽聽聽}
}
On Your Own

Congo.com uses the default session time-out, which normally equals 20 minutes. To experience the impact of shortened time-out intervals firsthand, add the following statement to Web.config:

<sessionState聽timeout="1" />

Call up Congo.aspx, click a few Add to Cart buttons, verify that the items were added to the shopping cart, and return to Congo.aspx. Now wait a couple of minutes and check the shopping cart again. Because the session time-out is a mere 1 minute, the cart should be empty. Finish up by deleting the sessionState element from Web.config in order to reset the time-out interval to 20 minutes.

Because it lacks a Web.config file specifying otherwise, Congo.com settles for the default session state process model. To demonstrate the effect of moving session state out of Aspnet_wp.exe, try this simple experiment:

  1. Open Congo.aspx in your browser.

  2. Add a few items to the shopping cart.

  3. Open a command prompt window and restart IIS by typing iisreset.

  4. View the shopping cart. How many items does it contain?

The answer should be zero because restarting IIS restarts ASP.NET, and restarting ASP.NET shuts down Aspnet_wp.exe. Since that鈥檚 where session is stored in the in-proc model, restarting IIS destroys all active session state, too. Now do the following:

  1. In a command prompt window, type

    net聽start聽aspnet_state

    to start the ASP.NET state server process running.

  2. Add the following statement to the system.web section of Web.config:

    <sessionState
    聽聽mode="StateServer"
    聽聽stateConnectionString="tcpip=localhost:42424"
    />
  3. Bring up Congo.aspx in your browser and add a few items to your shopping cart.

  4. Type iisreset again to restart IIS.

  5. Check your shopping cart.

This time, the shopping cart鈥檚 contents should still be there because session state is no longer stored in Aspnet_wp.exe. It鈥檚 in Aspnet_state.exe, which isn鈥檛 restarted when ASP.NET is restarted. If you go the extra mile and move the state server process to another machine (or use a SQL Server database on another machine to store session state), you can reboot your entire server without losing session state.

As a final learning exercise, try modifying Congo.aspx to store the DataSet that it binds to the DataGrid in the application cache. As it stands now, a physical database access is performed every time Congo.aspx is requested. Assuming the contents of the database don鈥檛 change very often, it would be far more efficient to query the database periodically, store the results in the application cache, and populate the DataGrid from the cache. Here鈥檚 a blueprint for making the change:

  1. Add an Application_Start method to Global.asax.

  2. In Application_Start, populate a DataSet with a database query and add the DataSet to the application cache. Specify that the DataSet expires 5 minutes after it鈥檚 added to the cache, and provide a reference to a callback method that鈥檚 called when the DataSet expires.

  3. Code the callback method to reexecute the database query and place a new DataSet in the application cache.

  4. Modify Congo.aspx鈥檚 Page_Load handler to bind to the DataSet stored in the application cache rather than populate a new DataSet with the results of a database query.

Once these changes are made, a physical database access will occur only every 5 minutes, no matter how often the page is requested. The performance difference will be negligible if you have only a few users, but as the load on the server increases, the improvement will be more and more noticeable. Caching frequently used data in memory is a tried-and-true means of increasing performance, and ASP.NET鈥檚 application cache is the perfect tool for the job.