IBuySpy Store Design

I l @ ve RuBoard

The IBS store is designed to provide a learning tool for ASP.NET developers. As such, the code is short, sweet, and to-the-point. Everything is well documented, and in general the simple approach is taken toward most tasks . This makes the site very easy to understand for developers who are new to ASP.NET. However, it also means that for some parts of the application, only a foundation is provided, not a complete solution. When the IBS method differs from how a real-world application should be designed, I will note the difference and why Microsoft chose to implement it the way they did.

Sources

In this chapter, I will occasionally quote members of the IBuySpy development team. The IBuySpy team includes Scott Guthrie and Susan Warren of Microsoft and Mike Amundsen and Bob Lair of Vertigo Software. This group designed and built the first version of the IBuySpy store in just four weeks. All four of these individuals are very helpful, and in particular Scott and Susan were able to answer many of my questions as I prepared this case study. Rob Howard, of Microsoft, was also very helpful.

The IBuySpy.com Web site and all of its source code is available for free to the public. You are welcome to use this code as a base for your own applications and to extend and modify it however you see fit. The only restriction placed upon the IBuySpy applications is that the installation files not be redistributed, to ensure that the latest builds are always available from the IBuySpy.com Web site.

Design Considerations

Let's examine some of the design considerations that Microsoft had when preparing to build the IBS store. One of the things that makes developing with Microsoft tools so much easier than with many other vendors ' offerings is the wealth of documentation and sample material they provide to developers. IBuySpy.com is not the first sample application Microsoft has used to demonstrate its technology. For example, the Duwamish bookstore (http://duwamishonline.com), originally devised as a sample desktop application, has been around for years and continues to evolve today into a distributed .NET application. Similarly, the FMStocks sample (http://fmstocks.com) application provides a great example of how to build a scalable Windows DNA application on Windows NT or 2000, and is also being revised to support .NET. Both of these sample applications are available at Microsoft's MSDN site, http://msdn.microsoft.com. Susan Warren notes: "The IBuySpy apps are intentionally implemented as simply as possible to facilitate learning. FMStocks, on the other hand, was intended to be a more general architecture prototype and specifically to show off the benefits/implementation of COM+ services."

The primary goal in designing IBuySpy was that it be simple and easy to understand. To this end, the application was designed in a modular fashion using a small number of subsystems. Because many people would use the application as a basis for developing more advanced applications, IBS was designed to be extensible and followed good coding and architectural guidelines. In the overview of the application, three design goals are cited: minimal code, high performance, and scalability. We will see in this chapter how all three of these goals were met by the application.

If IBuySpy had been designed to meet some transactions-per-second benchmark (as FMStocks was), it would have been developed somewhat differently. For one thing, it would probably have used COM+ for transaction management. The existing IBuySpy store doesn't even use transactions or deal with the payment process at all. This is left as an exercise for the reader. But remember, this was not IBuySpy's goal.

Architecturally, IBuySpy's application logic was divided into several well-known layers . Unlike previous sample applications, however, IBuySpy did not include separate data access layer (DAL) and business logic layer (BLL) components. This helped keep the number of components and total code required for the application to a minimum.

Component Architecture

In software application design, it helps to isolate the logical parts of the program into separate layers. For example, one might group all data access rules in one layer (the Data Access Layer, or DAL), and all business logic in another layer (the Business Logic Layer, or BLL). This would allow for easier modification at a later date. Thus, if (or more likely, when) business rules change, only the business layer objects need to be modified, or if the data store changes, only the data access layer objects need to be updated.

Figure 14.1 describes how the different layers of an application are frequently broken up across different physical or logical boundaries. Desktop applications, the classical example, were traditionally built with everything on the same machine and in the same logical space. Client/server applications tend to follow the two-tier design displayed. Most Windows DNA applications are built using something like the three-tier design (FMStocks 2000 follows this architecture). Finally, it is possible to distribute every layer across multiple machines to provide the greatest scalability and reliability, and the N- tier design demonstrates this.

Figure 14.1. Tiered application architectures.

At first, the IBuySpy team designed the application with separate layers of components for data access and for business rules. However, they found that the BLL was merely serving as a pass-through layer, and wasn't actually adding any benefits to the application. Experience from FMStocks had taught Vertigo and Microsoft that "having a 'pluggable' data layer (which would also justify the Biz layer) is also only 'sometimes' worthwhile. Most of the time [it] is just more code, and doesn't improve scalability or maintainability." (Susan Warren)

Currently, IBuySpy's architecture, as shown in Figure 14.2, is most closely related to the two-tier design. As you can see, its architecture is compressed into basically two layers ”the pages themselves ”which include presentation, context, and business logic ”and a set of data access layer objects.

Figure 14.2. IBuySpy architecture.

Figure 14.3 displays a tree view of the classes used in the IBuySpy application. If you look at the IBuySpy namespace, the distinction between pages and data access layer components is easy to see. The Components namespace lists all the data access classes, and the Pages namespace lists all the ASP.NET pages used in the application.

Figure 14.3. IBuySpy class hierarchy.

Note that each main object in the application, such as a Customer or an Order, has its own DAL component associated with it. This is similar to other Microsoft sample sites, such as FMStocks, and is a good way to organize your components. This system allows all related functionality for a particular part of your application to be encapsulated into one class. Meanwhile, general functions or tools that are needed by the entire application should be placed in a "library" component, or an application class. This is basically what the IBuySpyDB class does. IBuySpyDB simply exposes the connection string used by the application to access the back-end database

Application-Level Settings

The IBuySpy web.config file makes an excellent template for new developers to use when building their own ASP.NET applications. It includes a number of best practices for storing application-wide variables like connection strings, securing certain pages with a login screen, and configuring customer error handling. For reference, the entire web.config file is listed in Listing 14.1, but you can also view the most current version online at the IBuySpy Web site (http://www.ibuyspy.com).

Listing 14.1 IBuySpy store web.config.
 <?xml version="1.0" encoding="utf-8" ?> <configuration>     <! application specific settings >     <appSettings>         <add key="ConnectionString"                value="server=localhost;uid=sa;pwd=;database=store" />     </appSettings>     <! forms based authentication >     <system.web>         <! enable Forms authentication >         <authentication mode="Forms">             <forms name="IBuySpyStoreAuth" loginUrl="login.aspx"                protection="All" path="/" />         </authentication>         <! enable custom errors for the application >         <customErrors mode="RemoteOnly" defaultRedirect="ErrorPage.aspx" />         <! disable session state for application >         <sessionState mode="Off" />     </system.web>     <! set secure paths >     <location path="Checkout.aspx">         <system.web>             <authorization>                 <deny users="?" />             </authorization>         </system.web>     </location>     <location path="OrderList.aspx">         <system.web>             <authorization>                 <deny users="?" />             </authorization>         </system.web>     </location>     <location path="OrderDetails.aspx">         <system.web>             <authorization>                 <deny users="?" />             </authorization>         </system.web>     </location> </configuration> 

You learned about the web.config file in Chapter 10, "ASP.NET Applications." In ASP, there were many different ways to store connection information, and many different myths about which one was the best. Most experienced ASP developers stored the connection information in an application variable that was set in Application_OnStart . Although this is still an option with ASP.NET, I do not recommend it for several reasons. The first reason is that, logically, database connection information is an application configuration piece of data, and so it should reside in the web.config with the other configuration data. More importantly, though, there is the question of security. If you store your connection string in an application variable, and you ever decide to use Trace on a public Web page, your database connection information will be exposed to the world within the Trace dump, because it lists the contents of all application and session variables. I first ran into this on ASPAlliance.com, because I was preparing a tutorial on tracing. I quickly learned that storing database connection information in the application scope was not a good idea if you are planning to use Trace as part of your application, for the reasons I already mentioned.

In Chapter 3, we discussed some different ways to migrate ASP-based login pages to ASP.NET. The simplest and most effective way to accomplish this is through the technique used by IBuySpy. The <location> attributes allow you to manage your file security in one central location for your application. However, there is one limitation to using the web.config file: Only the root web.config file has authority to set permission configuration (or, for that matter, debug configuration) information for the application. If, for some reason, you absolutely do not have access to the root web's web.config file, you will need to find an alternative way to secure your pages, such as the user control discussed in Chapter 3.

Finally, there are two other application-wide settings that IBuySpy uses to enhance its performance. I consider these to be application settings because they are done on every page, but the actual implementation of these two best practices requires code on each individual ASP.NET page that uses them. The first item is the use of session state, and this is disabled by using the EnableSessionState="False" Page directive. You should use this on every page in your application that does not require session information, because it reduces the amount of resources the page will require on the server. The second directive is similar, and determines whether or not controls on a page should maintain their viewstate between postbacks. This should only be used for pages that post back to themselves ”any other page should disable this functionality by using the EnableViewState="False" attribute in the Page directive. Tracking viewstate requires resources both when the page is rendered and also in the form of network bandwidth, because the (often large) viewstate data must be passed to and from the client with each request. Obviously, this should only be done when necessary.

NOTE

You can also control viewstate on individual controls by setting the EnableViewState property. You should disable viewstate for any control that doesn't use it to guarantee optimum performance.


Database Design

The IBuySpy store uses just seven small database tables to store all the data required for the site. These tables and their relationships (as installed) are listed in Figure 14.4. Note that there are a few omissions to the database that should be corrected in a production application, such as adding a foreign key from OrderDetails to Products on ProductID, and also adding a primary key on Reviews. However, because the application currently doesn't allow any way for users to violate data integrity on these tables, the lack of these constraints is not terribly important to IBuySpy as a sample application. I mention them here only for the sake of absolute correctness. Of course, if you were to build on this design and use it for a production application, you would want to ensure that your database integrity was guaranteed through the proper use of keys and constraints.

Figure 14.4. IBS database schema.

One thing that is worth noting in this design is that the ShoppingCart table does not have any relationship with the Customers table. Anonymous users are thus allowed to create shopping carts without having to first log in and register themselves. This is a good practice in an e-commerce site because it lowers the barriers between the customer and the buying process. Only when they commit to the purchase by clicking on Check Out are the users required to register or log in.

Another best practice to note in the database design for IBuySpy is the use of stored procedures for all of the site's data access. All data access in the IBuySpy application is done through stored procedures, which are called by separate DB components. This is done because stored procedures perform better and are easier to maintain than hard-coded SQL statements. As you build new applications and rebuild existing applications using ASP.NET, you should follow IBuySpy's example: Do not succumb to the temptation to place hard-coded SQL in your applications.

Functional Overview

As a mock e-commerce site, IBuySpy.com must provide a number of functions in order to allow the user to browse the site's products and make a purchase decision. Because it is only a sample site, it doesn't need to worry about details like credit card validation, secure access to ordering pages that transmit credit card numbers , inventory tracking, or various shipping methods . Nonetheless, it does provide an excellent starting point for a real Web-based retail store, providing functions like the ability to browse products, add items to a shopping cart, manage that cart across separate sessions, and check out. Registration and login are also handled neatly by the application.

List Products

Of course, no catalog store would be complete without a way to list the products available. IBuySpy does this using several different methods. On the home page, a featured product is displayed, as well as a listing of the most popular items. Clicking on any of the categories on the left menu brings up a list of products in that category, complete with small image, price, and links to obtain more information or to add the item to the shopping cart. The home page for the IBuySpy store is shown in Figure 14.5.

Figure 14.5. The IBuySpy home page.

The popular items listing uses a user control to encapsulate the functionality. This is an excellent way to design your pages, because it provides better opportunities for code re-use and makes it easier to construct a template page and then just drag and drop the controls you want onto it. The featured item is hard-coded onto the page, so that it is always the Pocket Protector Rocket Pack. Obviously, this isn't ideal, because it requires the developer to edit the actual Web page in order to change the featured item, introducing potential for error. We'll see how to convert this into a user control as well later in this chapter.

Of course, the main product listing pages are the category listings and the search results page. These two pages both use the same DataList code to display the listing of products, but again, they are not user controls. Because of this, although the two pieces of code are meant to be identical, it is very easy for a developer to make a change in one and not the other, introducing unwanted behavior. For example, in one early release of the site, there was a bug on the SearchResults.aspx page that resulted in the word "ProductID" appearing next to each search result. The same code was used by both the ProductsList.aspx page and the SearchResults.aspx page to list the products, and most likely a change was made to one of these files without updating the other file, resulting in the bug.

This minor mistake made it past the testing phase because the same code was used in two places using two different copies of the code. An update to one was not reflected in the other. This is one of the primary advantages of code reuse, and this is one reason why user controls are so powerful. They provide a very easy way to encapsulate reusable pieces of code in your applications. Later in this chapter, we will redesign the SearchResults.aspx and ProductsList.aspx pages to use a User Control.

The last thing we should look at on these pages is how they get to the data for the products. The data is all stored in a SQL Server database and accessed using stored procedures. If the data for each search or category were being cached so that it would be available for other pages, it would make sense to use a DataSet to extract the data because DataSets can be easily serialized and cached. However, DataSets are bigger and slower than DataReader objects, so the first hit to the page would suffer. Further, IBuySpy is using output caching to cache the contents of the rendered page, so there is no need to cache the data itself. Thus, the DataReader provides better performance in this scenario.

In response to the question of why IBS chose to use a custom data type as the return type for their DAL components that return a single record, instead of a standard DataReader or DataSet , Susan Warren replied:

We used the Details class type ("struct") for those queries that return a single record. This makes for much more grokkable [understandable] databinding in the UI layer. (I can bind to CustomerDetails.FullName instead of DataSet. Tables["Customers"][0][" FullName "]. It's also strongly typed, so you get statement completion and design-time syntax checking from VS.NET. Because there's no biz logic reason for disconnected data, we used DataReaders to render the UI, for performance reasons. In several places we used DataSets in order to have a disconnected object we could cache.

Add Items to Cart

Using ASP.NET, developers are encouraged to create their pages so that they submit back to themselves using a "postback." One of the lessons we can learn from IBuySpy is in what instances this technique is not a good idea. A good example of this involves the AddToCart functionality.

Although the product listing and search result pages could each have implemented the AddToCart function as a postback method on each of these pages, this was not done for several reasons. Postbacks require maintaining viewstate, which is expensive especially on pages that have a lot of server controls, because data for each control must be rendered and sent over the wire. Also, because the functionality of adding an item to the cart could be called from other pages apart from the list pages (such as the home page, for instance), segregating this functionality into its own page helps keep the code logically separated and easier to reuse.

The actual AddToCart.aspx page doesn't really have any presentation logic to it at all. It simply uses its Page_Load event to add an item to the user's shopping cart before redirecting the page to the ShoppingCart.aspx page. This is a good example of how to implement general functions in an application when the functions do not require much user interaction and can be called from many different pages.

One last note for developers who are building international e-commerce sites: most countries don't use shopping carts; they use shopping baskets . If you want your customers to intuitively understand your store, and your audience extends beyond North America, you may want to use a " basket " instead of a "cart" to hold your wares.

Manage Cart

The ShoppingCart.aspx page is probably the most complicated page in the whole IBuySpy application. It uses a DataGrid to render the contents of the user's shopping cart, providing the user with controls to use to update quantities or delete items. DataGrids , described in Chapter 4, offer developers a great deal of flexibility and power, but of course with all this flexibility comes the requirement to write a decent amount of code to implement a particular behavior. This is the main reason why the ShoppingCart.aspx and ShoppingCart.cs (or .vb) files are each a few pages long ”there is a lot going on with the DataGrid . For this reason, this page makes an excellent study example of how to use DataGrids .

The way anonymous and registered users' carts are handled by this page is interesting. The CartID is used to identify the cart, and consists of a dynamically generated GUID for anonymous users and the CustomerID for known users. Whenever a user logs in or registers with the store, part of the process of authenticating the user includes migrating her cart information from anonymous to registered. This step uses a method called migrateCart in the ShoppingCartDB component, and basically all it does is update the CartID of the cart in question to be the CustomerID of the current customer.

The rest of the implementation of the shopping cart and the DataGrid is well documented within the IBuySpy source code, so I won't spend any more time on it here.

Check Out Cart

Of course, before you can finalize a transaction on an e-commerce site, you have to go through the obligatory checkout process. IBuySpy.com is no different, and offers users the option of "Final Check Out" from the Shopping Cart page. There are really only two things to note about this page. First, it requires a registered user, and will send an anonymous user to the Login.aspx page before allowing her to proceed. This is all controlled in the web.config file, which we looked at earlier. Second, the page calls a component that calls a stored procedure that creates the order, and returns an order ID to give to the customer. Apart from repeating the mantra of "use stored procedures for your data access," there isn't much else to say about this bit of functionality. Notice that by encapsulating security and data access in separate parts of the application, the CheckOut.aspx page is kept very small. The actual programming code takes less than 30 lines of code, and the whole page including HTML is less than two pages long.

Register Users

With the IBuySpy application, it doesn't take much to register yourself as a customer. A name, an e-mail address, and a password are all you need. However, although this page is simple, it does demonstrate an important concept that will come into play when we look at the Login.aspx page. User registration is one of the best places to use validation controls (discussed in Chapter 7), because this will help ensure that your database holds valid customers and not garbage data. Of course, validation can only do so much, but at the very least you should protect your customers from themselves by ensuring that their e-mail address is valid and that their password matches what was typed in the Confirm Password box. Furthermore, if you are certain to validate your user's username and password during registration, you can perform the same validation checks on your login page, and avoid the need to hit the database for login requests that are invalid according to your validation rules. Note that if you choose to use validation as the first stage to authenticating users, you must make sure that your validation logic for the registration page matches the validation logic of your login page. The easiest way to do this is to make the fields (for example, username and password and their validation controls) of the registration page into one or two User Controls.

Login Users

Although the complete login code for the IBuySpy store takes several pages, the real work is all done in one method that is called when the login button is clicked. Because this is an important piece of code and only a few lines long, it is included here as Listing 14.2. The comments provide most of the information we need to follow the code (and a VB version is available on the IBuySpy.com Web site).

Listing 14.2 IBuySpy's login code, Login.cs, C#.
 void LoginBtn_Click(Object sender, ImageClickEventArgs e) {     // Only attempt a login if all form fields on the page are valid     if (Page.IsValid == true) {         // Save old ShoppingCartID         IBuySpy.ShoppingCartDB shoppingCart =             new IBuySpy.ShoppingCartDB();         String tempCartID = shoppingCart.GetShoppingCartId();         // Attempt to Validate User Credentials using CustomersDB         IBuySpy.CustomersDB accountSystem =             new IBuySpy.CustomersDB();         String customerId =             accountSystem.Login(email.Text, password.Text);         if (customerId != null) {             // Migrate any existing shopping cart items             // into the permanent shopping cart             shoppingCart.MigrateCart(tempCartID, customerId);             // Lookup the customer's full account details             IBuySpy.CustomerDetails customerDetails =                 accountSystem.GetCustomerDetails(customerId);             // Store the user's fullname in a cookie             // for personalization purposes             Response.Cookies["IBuySpy_FullName"].Value =                 customerDetails.FullName;             // Make the cookie persistent only if the user             // selects "persistent" login checkbox             if (RememberLogin.Checked == true) {                 Response.Cookies["IBuySpy_FullName"].Expires =                    DateTime.Now.AddMonths(1);             }             // Redirect browser back to originating page             FormsAuthentication.RedirectFromLoginPage(customerId,                 RememberLogin.Checked);         }         else {             Message.Text = "Login Failed!";         }     } } 

There really is a lot more going on here than just your typical login functionality of looking up a user and password in the database and returning true or false. The first thing we do here is ensure that the login form was valid ”no sense wasting resources hitting the database if the form isn't even filled out properly, right? When we know the form is valid, we take the current shopping cart and store its ID so that we don't lose it when we log in the customer.

The actual authentication is performed with the following block of code:

 // Attempt to Validate User Credentials using CustomersDB IBuySpy.CustomersDB accountSystem =     new IBuySpy.CustomersDB(); String customerId =     accountSystem.Login(email.Text, password.Text); 

Here, the IBuySpy component accountSystem is instantiated and its Login method is called on to perform the login, which returns a customerID if successful and null otherwise . In the case of a null return, we simply return with a "Login Failed" message.

However, if the login was successful, our work is not yet done. This page does a few more things that are good tricks to know for your own applications. First, we migrate the shopping cart so that the CartID is now the CustomerID instead of an anonymous GUID. Next, we grab the customer's details and use them to store personalized data in a cookie (in this case, the customer's name). This cookie will expire when the user closes the browser, unless we set its Expires property, which is the next step. If the user stated that we should remember his login information, we update the cookie's expiration date to be a month from now, making it so that the user will only have to log in once per month as long as he checks the box to remember his login. Finally, we use the built-in FormsAuthentication class's RedirectFromLoginPage method to send the user to the page he was originally attempting to access, passing along his CustomerID and also whether or not he wants to persist his login cookie.

If you are building a site that will require users to log in ”and most sites do have at least a few pages that are restricted from public access ”the IBuySpy login page is an excellent place to start.

Expose Web Service

Web services are just cool. We covered them in Chapter 11, "ASP.NET and Web Services," and IBuySpy exposes two methods through its InstantOrder Web service: OrderItem and CheckStatus . Both of these services require the user to send her username and password as para- meters , and then perform their function using the other parameters that are passed. The sheer beauty of ASP.NET is that implementing Web services is almost trivial. Listing 14.3 shows the OrderItem Web method (in C# ”the VB version is available at http://www.ibuyspy.com). Note that the whole thing is only ten lines of actual code, about half of which deals with validating the user's credentials.

Listing 14.3 The OrderItem Web method, InstantOrder.cs.
 [WebMethod(Description="The OrderItem method enables a remote client to programmatically  place an order using a WebService.", EnableSession=false)] public OrderDetails OrderItem(string userName, string password,     int productID, int quantity) {     // Login client using provided username and password     IBuySpy.CustomersDB accountSystem = new IBuySpy.CustomersDB();     String customerId = accountSystem.Login(userName, password);     if (customerId == null) {         throw new Exception("Error: Invalid Login!");     }     // Add Item to Shopping Cart     IBuySpy.ShoppingCartDB myShoppingCart = new IBuySpy.ShoppingCartDB();     myShoppingCart.AddItem(customerId, productID, quantity);     // Place Order     IBuySpy.OrdersDB orderSystem = new IBuySpy.OrdersDB();     int orderID = orderSystem.PlaceOrder(customerId, customerId);     // Return OrderDetails     return orderSystem.GetOrderDetails(orderID); } 

That is all there is to it! Notice that in this case we are actually returning an OrderDetails object as the return type for this method. This is done by serializing the object into XML, as we covered in Chapter 11, ASP.NET and Web Services. You can definitely see that Web services are going to be extremely popular in the next few years, because exposing them to the world is such an easy task, thanks to the Microsoft .NET Framework.

Performance ”Caching

Although not really a store function, no study of IBuySpy would be complete without a look at some of its performance-enhancing features, particularly caching. The initial version of the store relied on output caching and custom use of the .NET caching API. Although this yielded very good performance, the release version of the store now includes fragment caching, which allows portions of pages to be cached. This is much easier to work with than the caching API, especially for new developers, and makes it very easy to cache individual portions of ASP.NET pages.

Some Recommended Improvements

There are some things we can do to improve and extend the existing IBS store, and provide a great learning exercise. To begin, let's go ahead and make a few of the improvements we discussed as we went over the site. The first thing we could do to improve the site would be to make the Featured Item section of the home page a user control, so that we could add featured items to any page we wanted in the future.

Featured Item User Control

Instead of hard-coding the umbrella rocket on the home page of IBuySpy.com, we would prefer to use a user control to display the day's featured item, which we could then specify using a Web-based edit page (much like the IBuySpy Portal's user controls), or in the web.config file, so that we can manage which product is featured without having to edit the actual page.

Listing 14.4 describes the user control, FeaturedItem , that we use to accomplish this task. This particular implementation uses data stored in the web.config file, but you could easily create a file or database table to store the featured item or items, or have it randomly select an item from all of those available.

Listing 14.4 FeaturedItem user control _FeaturedItem.ascx (C#).
 <%@ Control Language="C#" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ OutputCache Duration="3600" VaryByParam="None" %> <script language="C#" runat="server">     public int ProductID = 0;     public String Product_Ad_Phrase;     protected IBuySpy.ProductDetails featuredItem;     void Page_Load(Object Src, EventArgs E) {             // If no ProductID was specified, look it up from the Config.Web file            if (ProductID == 0){                        try{                                  ProductID = Int32.Parse(ConfigurationSettings.AppSettings["FeaturedItem"]);                               Product_Ad_Phrase = (string) ConfigurationSettings.AppSettings["FeaturedItemPhrase"];                        }                        catch{                               // If none set, default to Pocket Protector Rocket Pack                               ProductID =373;                               Product_Ad_Phrase = "Blast off in a ";                        }             }             try{                IBuySpy.ProductsDB productCatalog = new IBuySpy.ProductsDB();                featuredItem = productCatalog.GetProductDetails(ProductID);        }        catch{                FeaturedItemSpan.Visible = false;         }     } </script> <span id="FeaturedItemSpan" runat="server">            <img src='ProductImages/<%=featuredItem.ProductImage %>'                 width="309" border="0">            <br/>             &nbsp;&nbsp;&nbsp;&nbsp;             <span class="NormalDouble"><i><%=Product_Ad_Phrase%>&nbsp;             <a href='ProductDetails.aspx?productID=<%=ProductID %>'>             <span class="ProductListHeader"><b>             <%=featuredItem.ModelName %></b></i></span></a></span> </span> 

Now, in order to use this control on the home page (Default.aspx), we need to add the following line to the top of the page:

 <%@ Register TagPrefix="IBuySpy" TagName="FeaturedItem" src=" _FeaturedItem.ascx" %> 

and then replace the existing HTML that displays the Umbrella Rocket with this tag:

 <IBuySpy:FeaturedItem ID="FeaturedItem" runat="server" /> 

Now, this control exposes public properties for the product ID and description, so you could manually specify the featured product from this page if you wanted to. However, we would like to manage this from the web.config file, so we will leave these properties blank, and the control will default to the web.config values.

To set up the values in the web.config file, we need to add two nodes to the appsettings node, FeaturedItem and FeaturedItem_Ad_Phrase . The entire appsettings element is shown following:

 <add key="ConnectionString"     value="server=localhost;uid=sa;pwd=;database=store" /> <add key="FeaturedItem" value="374" /> <add key="FeaturedItemPhrase" value="Protect your stuff with a" /> 

These two nodes set the product ID for the featured product, and a brief ad blurb to describe it, which replaces the original store's "Blast off with a" blurb. Incidentally, if our Products database had had a field for this, we wouldn't have needed to store it here. That would make for another enhancement to add to this application.

That's it! Now, if we view the home page after making the changes just described, it will look like the page in Figure 14.6.

Figure 14.6. Revised IBuySpy.com home page.

ListItems User Control

As we saw earlier in the chapter, copying functionality between pages instead of encapsulating it in custom controls or user controls is a bad practice and can lead to errors. Listing 14.5 shows what a user control to replace the existing DataList controls on the SearchResults and ProductsList pages would look like. Using this control greatly simplifies these two pages, and enhances the ProductsList page by adding a message for the case where no items are found.

Listing 14.5 ListItems user control _ListItems.ascx (C#).
 <%@ Control Language="C#" %> <%@ Import Namespace="System.Data.SqlClient" %> <%@ OutputCache Duration="3600" VaryByParam="*" %> <script runat="server">     public SqlDataReader DataSource;     void Page_Load(Object sender, EventArgs e) {         MyList.DataSource = DataSource;         MyList.DataBind();         // Display a message if no results are found         if (MyList.Items.Count == 0) {             ErrorMsg.Text = "No items matched your query.";         }     } </script> <asp:DataList id="MyList" RepeatColumns="2" runat="server">     <ItemTemplate>         <table border="0" width="300">             <tr>                 <td width="25">                     &nbsp;                 </td>                 <td width="100" valign="middle" align="right">                     <a href='ProductDetails.aspx?productID=<%# DataBinder.Eval (Container.  DataItem, "ProductID") %>'>                     <img src='ProductImages/thumbs/<%# DataBinder.Eval (Container.  DataItem, "ProductImage") %>'                                             width="100" height="75" border="0">                     </a>                 </td>                 <td width="200" valign="middle">                     <a href='ProductDetails.aspx?productID=<%# DataBinder.Eval (Container.  DataItem, "ProductID") %>'>                         <span class="ProductListHead">                             <%# DataBinder.Eval(Container.DataItem, "ModelName") %>                         </span>                         <br>                     </a><span class="ProductListItem"><b>Special Price: </b>                         <%# DataBinder.Eval(Container.DataItem, "UnitCost", " { 0:c} ") %>                     </span>                     <br>                     <a href='AddToCart.aspx?productID=<%# DataBinder.Eval (Container.  DataItem, "ProductID") %>'>                         <span class="ProductListItem">                         <font color="#9D0000"><b>Add To Cart<b></font>                         </span>                     </a>                 </td>             </tr>         </table>     </ItemTemplate> </asp:DataList> <img height="1" width="30" src="Images/1x1.gif"> <asp:Label id="ErrorMsg" class="ErrorText" runat="server" /> 

Implementing the list on the ProductList.aspx and SearchResults.aspx pages simply requires adding a Register directive to the top of each of these pages, and replacing the existing data list code with the user control call. The Register directive would look like this:

 <%@ Register TagPrefix="IBuySpy" TagName="ItemList" src="_ListItems.ascx" %> 

The call to the user control would then be:

 <IBuySpy:ItemList ID="ItemList1" runat="server" /> 

Finally, the DataList code in the Page_Load event needs to be updated to refer to the ItemList control, like so (this example is from the SearchResults.aspx page):

 ItemList1.DataSource =     productCatalogue.SearchProductDescriptions(Request.Params["txtSearch"]); ItemList1.DataBind(); 

One more feature that we might add to the site to improve it would be the ability for users to update their profile and change their name or password. We'll conclude the chapter with this example.

Let Users Update Their Profile

The Register.aspx page provides a form for users to sign up for the IBuySpy Web site, but once they have done so, they cannot modify their settings afterward. Adding this functionality is fairly straightforward ”we are just going to copy the Register.aspx file to EditUser.aspx and change the action to update the user's settings rather than inserting a new user. The main part of the form can remain the same, but we've changed the button's name to UpdateBtn. Listing 14.6 shows the script block for the new EditUser.aspx page (the complete page is available online at http://aspauthors.com/aspnetbyexample/ch14/).

Listing 14.6 Script block for EditUser.aspx (C#)
 <script runat="server"> void Page_Load(){     if(!Page.IsPostBack){     // Calculate end-user's shopping cart ID     IBuySpy.ShoppingCartDB cart = new IBuySpy.ShoppingCartDB();     String cartId = cart.GetShoppingCartId();     // cartId is also the customer ID     IBuySpy.CustomersDB custDB = new IBuySpy.CustomersDB();     IBuySpy.CustomerDetails custDetails = custDB.GetCustomerDetails(cartId);     // Populate the form with the customer's current values     Name.Text = custDetails.FullName;     Email.Text = custDetails.Email;     Password.Text = custDetails.Password;     ConfirmPassword.Text = Password.Text;     } } void UpdateBtn_Click(Object sender, ImageClickEventArgs e){     // Only attempt a login if all form fields on the page are valid     if (Page.IsValid == true) {         // Calculate end-user's shopping cart ID         IBuySpy.ShoppingCartDB cart = new IBuySpy.ShoppingCartDB();         String cartId = cart.GetShoppingCartId();         // Update Customer Information In CustomersDB database         IBuySpy.CustomersDB accountSystem = new IBuySpy.CustomersDB();         accountSystem.UpdateCustomer(cartId, Name.Text, Email.Text, Password.Text);         Response.Redirect("ShoppingCart.aspx",true);     } } </script> 

In the Page_Load method we retrieve the customer's ID by using the GetShoppingCartId, since this ID is the same as the customer's ID. To ensure that this has a value, we will set up the EditUser.aspx page in the web.config file so that it requires the user to login to access the page. Once we have the ID, we use the CustomersDB object to retrieve a CustomerDetails struct, which has all of the information we need. Finally, we set the values of our four form fields with the appropriate values from the database.

When the user clicks the submit image, it will launch the UpdateBtn_Click event handler (because there is an OnClick="UpdateBtn_Click" attribute on the ImageButton). This method looks identical to the click handler in the Register.aspx page, except instead of adding a customer, we are calling a new method of the CustomersDB object, UpdateCustomer(). This method did not exist in the original IBuySpy application ”we had to add it, along with a new stored procedure. The UpdateCustomer() method is shown in Listing 14.7, and must be added to the CustomersDB.cs file in the Components folder. Before the change will take effect, the mk.bat file must be executed from within the components folder as well.

Listing 14.7 UpdateCustomer() method (CustomersDB.cs)
 public void UpdateCustomer(string customerId, string fullName,     string email, string password) {     // Create Instance of Connection and Command Object     SqlConnection myConnection = new         SqlConnection(ConfigurationSettings.AppSettings["ConnectionString"]);     SqlCommand myCommand = new SqlCommand("CustomerUpdate", myConnection);     // Mark the Command as a SPROC     myCommand.CommandType = CommandType.StoredProcedure;     // Add Parameters to SPROC     SqlParameter parameterCustomerID =         new SqlParameter("@CustomerID", SqlDbType.Int, 4);     parameterCustomerID.Value = Int32.Parse(customerId);     myCommand.Parameters.Add(parameterCustomerID);     SqlParameter parameterFullName =         new SqlParameter("@FullName", SqlDbType.NVarChar, 50);     parameterFullName.Value = fullName;     myCommand.Parameters.Add(parameterFullName);     SqlParameter parameterEmail =         new SqlParameter("@Email", SqlDbType.NVarChar, 50);     parameterEmail.Value = email;     myCommand.Parameters.Add(parameterEmail);     SqlParameter parameterPassword =         new SqlParameter("@Password", SqlDbType.NVarChar, 50);     parameterPassword.Value = password;     myCommand.Parameters.Add(parameterPassword);     myConnection.Open();     myCommand.ExecuteNonQuery();     myConnection.Close(); } 

Comparing this method to the AddCustomer() method, we see that it is almost identical, except that it doesn't return a value, and it takes customerId as a parameter. These methods are very well documented on the IBuySpy Web site, so won't go into any more detail on what this page is doing. Assuming you've already read the ADO.NET chapter, you should have no trouble following this code anyway. The UpdateCustomer() method calls the CustomerUpdate stored procedure, which we have added to the store database. The source for this procedure is displayed in Listing 14.8, below, and can be added to your sql server database by simply typing the entire listing into the query analyzer and running it.

Listing 14.8 CustomerUpdate stored procedure.
 CREATE PROCEDURE CustomerUpdate (@CustomerID int,     @FullName   nvarchar(50),     @Email      nvarchar(50),     @Password   nvarchar(50)) AS UPDATE Customers SET     FullName = @FullName,     EMailAddress = @Email,     Password = @Password WHERE CustomerID = @CustomerID 

Once this is done, the last change is to add the EditUser.aspx page to the list of pages that require the user to log in, which is done in the web.config file. Listing 14.9 shows the node that should be added to the main configuration node (with the other location nodes).

Listing 14.9 Securing the EditUser.aspx page (web.config)
 <location path="EditUser.aspx">     <system.web>         <authorization>             <deny users="?" />         </authorization>     </system.web> </location> 

Now your users will be able to update their name and password and email for their accounts by using this page. Add a link somewhere on the site to the EditUser.aspx page, and this feature will be active for everyone who uses your site. Adding additional features that require data access that isn't already in the DB components will generally require changes like the ones we have just covered here, including new stored procedures and updates to the appropriate DB component. Using these techniques you can easily adapt the IBuySpy store archtitecture to support your own commercial Internet storefront.

I l @ ve RuBoard


Asp. Net. By Example
ASP.NET by Example
ISBN: 0789725622
EAN: 2147483647
Year: 2001
Pages: 154

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net