Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

ASP.NET 2.0 Instant Results

.pdf
Скачиваний:
67
Добавлен:
17.08.2013
Размер:
11.03 Mб
Скачать

Wrox WebShop

The data in the database is not accessed by the code in the data access layer directly using inline SQL statements. Instead, for each relevant database action (insert, update, and so on), the database has a stored procedure that carries out the requested action. This makes it easier to reuse the SQL code in other parts of the application. It also makes it easier to make radical changes to the structure of the database; instead of examining many methods in the data access layer, all you need to do is change the stored procedures. The stored procedures for the WebShop are all pretty straightforward. Most of them do nothing but insert a single record, or retrieve one or more items.

ShopManagerDB

Because of the limited size of the WebShop application, all data access code is centralized in a single class, the ShopManagerDB. In larger applications it’s a good idea to give most of the business classes their own counterpart in the business layer. That way it’s easy to see how classes in both layers are related. For the WebShop that would likely result in numerous very small classes like the ProductManagerDB, Category ManagerDB, CustomerManagerDB, and so on, each with only one or two methods. To keep things simple and straightforward, the WebShop uses the ShopManager class in the business layer and the Shop ManagerDB class (Figure 9-11 shows its structure) in the data access layer. Just like a shop manager in the real world, these classes are responsible for showing the customers around (displaying products from the product catalog), stocking the shelves (maintaining the product catalog in the maintenance section), and completing the customer’s order.

Figure 9-11

The ShopManagerDB class has only shared methods and no properties. Therefore, its constructor has been hidden by marking it as Private. This is indicated by the little lock icon you can see in Figure 9-11. The other methods in this class can be divided in two groups: methods that are used at the front end of the site to order products, and those that are used in the maintenance area to manage the products in the product catalog. The following table lists the six important methods of the ShopManagerDB class:

Method Name

Return

Description

 

Type

 

 

 

 

Public Shared

n/a

Marks a product as deleted by setting the Deleted

Sub DeleteProduct

 

column to 1 (true).

(ByVal theProduct

 

 

As Product)

 

 

 

 

 

 

 

Table continued on following page

287

Chapter 9

Method Name

 

Return

Description

 

 

Type

 

 

 

 

 

Public Shared

 

Integer

Inserts an order in the OrderBase table and adds an

Function FinalizeOrder

 

associated record for each ordered product in the

(ByVal theShoppingCart

 

OrderDetail table. It returns the new orderId.

As ShoppingCart, ByVal

 

 

theCustomer As Customer)

 

 

Public Shared

 

Product

Returns a single instance of a Product, indicated by

Function GetProduct

 

the productId parameter. Returns Nothing when

(ByVal productId

 

the product couldn’t be found.

As Integer)

 

 

 

Public Shared

Function

DataSet

Returns a list with the available Categories as a

GetProductCategories ()

 

DataSet with an ID and a Description column.

Public Shared

Function

List

Returns a strongly typed list of products. The

GetProducts (ByVal

(Of Product)

categoryId parameter is used to limit the product

categoryId As

Integer)

 

list to products in one category.

Public Shared

Sub

n/a

Creates a new product in the Product table.

InsertProduct

(ByVal

 

 

theProduct As

Product)

 

 

Helper Classes

The App_Code folder for the WebShop also contains two utility classes called Helpers and App Configuration. The Helpers class has one method called ResizeImage. This method takes a path to an image and a maximum and then resizes the image. The inner workings of this method are discussed in Chapter 11, which deals with manipulating images.

The AppConfiguration class has a single read-only and shared property called ConnectionString that reads the WebShop’s connection string from the Web.config file and returns it to the calling code. This makes it very easy to change the connection string later; for example, when you switch from the development to the production environment. All you need to do is change the connection string in the Web.config file and the changes will be picked up by the ASP.NET run time automatically.

Another benefit of using a shared property like this is that you get IntelliSense on the Helpers class to help you remember the name of the connection. With this property, you don’t have to remember the actual name of the key of the connection string or the code required to get the string from the Web.config file.

Now that you understand the design of the business and data access layers, it’s time to turn your attention to the actual implementation. The following section explains most of the pages in the WebShop application and how they interact with the code in the business layer.

288

Wrox WebShop

Code and Code Explanation

The files in the WebShop are logically grouped together in folders. The Css folder contains a single file called Core.css that contains the CSS used throughout the site. The Images folder contains the few images that are used throughout the site, such as the logo. Its subfolder Products contains the automatically generated thumbnails, three for each of the products.

In addition to these two folders the site contains three other important folders: Controls, Shop, and Management. The Controls folder contains three user controls that are used in various other pages in the site. The controls are explained as part of the pages that contain them in the next few sections. The Shop folder contains all the pages that make up the shopping section of the site, such as the Product Detail page and the Check Out page. These files are examined in great detail later. The Management folder contains the files that are required to maintain the products in the product catalog.

Root Files

A few files located in the root are also used by many other pages in the site, so it’s important to look at them first.

Global.asax

The Global.asax for the WebShop contains code for one event only: Application_Error. In this event handler, which fires whenever an error occurs in the site, an e-mail is sent with the error details. Refer to Chapter 6 for an explanation of this code.

Web.config

As is common in any ASP.NET application, the WebShop has a Web.config file that contains settings that are critical to the application. This section shows you the most important keys only; many of the other settings in the file are placed there by default whenever you create a new web application in Visual Web Developer.

Under the <appSettings> node you find a single key/value pair called MailFromAddress. This key is used to set the From: address in e-mails that are sent by the application. This setting is used in the PasswordRecovery control in the Login page:

<appSettings>

<add key=”MailFromAddress” value=”You@YourProvider.Com”/> </appSettings>

Below the <appSettings> you find the connectionStrings node that has a single connection string defined called WebShop. The connection string uses the local SQL Express Edition of SQL Server and instructs it to automatically attach the WebShop.mdf file in the App_Data folder:

<connectionStrings>

<add name=”WebShop” connectionString=”server=(local)\SqlExpress; AttachDbFileName=|DataDirectory|WebShop.mdf;Integrated Security=true; User Instance=true”/>

</connectionStrings>

289

Chapter 9

The <profile> node sets up the Profile provider. Profiles are a great way to quickly save user-specific information such as name, addresses, and preferences. Storing and retrieving the information is transparently done by the ASP.NET Framework — no custom code is required. All you need to do is set up the <profile> node in the Web.config file. The <profile> element has a few sub-elements that require some explanation. Following is the entire <profile> element:

<profile>

<providers> <clear />

<add name=”AspNetSqlProfileProvider” connectionStringName=”WebShop” applicationName=”/”

type=”System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”

/>

</providers>

<properties>

<add name=”FirstName” /> <add name=”LastName” />

<add name=”ProfileCompleted” type=”System.Boolean” /> <group name=”Address”>

<add name=”Street” /> <add name=”ZipCode” /> <add name=”City” /> <add name=”Country” />

</group>

</properties>

</profile>

The <providers> element tells the ASP.NET run time what Profile provider to use. In the Machine.config file, the root of all configuration files on your server, there is already a default provider configured that uses a local connection string and a database called aspnetdb.mdf. Because the WebShop uses its own database, the <clear /> element is used to remove the inherited profile settings. The <add> element then adds a new provider that is very similar to the original one, except that the connectionStringName property now points to the WebShop database.

The <properties> node is where you add the actual <profile> properties you want to use. Properties come in two flavors: simple properties and grouped properties. An example of a simple property is the FirstName. Because no additional information is supplied, the type of the FirstName property defaults to a System.String, which is fine for a first name. The property ProfileCompleted is set to a Boolean by specifying type=”System.Boolean” in the <add> element.

The Street, ZipCode, City, and Country properties are part of a properties group called Address. All these properties are of type String, because of the missing type attribute. Grouping properties is a very convenient way to provide a clean programming model to the developers using this profile. Figure 9-12 shows you how you can retrieve the ZipCode property from the Address group.

As you can see, the ZipCode property is now a property of the Address group, which in turn is a property of the Profile class. Behind the scenes, the information in the <properties> node is compiled into a class that is made available through the Profile class. This happens both at development time, so you get IntelliSense support, and at compile time, so the information is available at run time.

290

Wrox WebShop

Figure 9-12

The <profiles> element has a few other settings that allow you to tweak the behavior of the profile feature. For example, the allowAnonymous attribute of a property controls whether the setting can be used by anonymous users, whereas the automaticSaveEnabled attribute of the <profile> element determines whether changes in the profile are saved automatically, or if you need to explicitly call the Save method. If you want to find out more about the Profiles feature of ASP.NET 2.0, pick up a copy of Wrox’s Professional ASP.NET 2.0.

Next up in the Web.config are the settings for the Membership and Role providers. It’s quite a bit of code and very similar to the code you have seen in previous chapters, so it’s not repeated here but you’re encouraged to take a look at it in the Web.config file. The most important elements are the <membership> and <roleManager>. They control the way users are authenticated in the site and to what roles they are assigned. The <roleManager> is used for only one role, the Administrator that needs to access the maintenance section.

At the bottom of the Web.config file you find two <location> nodes that override the authentication for the Management folder and the CheckOut.aspx page. Only users in the Administrator role can access the Management folder, whereas the Check Out page is blocked for all non-authenticated users.

MasterPage.master

The master page of the WebShop defines the general look and feel of all the pages in the site, as you saw in the introduction of this chapter. The master page contains a reference to the global CSS file called Core.css in the Css folder. It also includes a user control called MainMenu that you find in the Controls folder. This MainMenu control in turn contains a number of <a> tags that link to entry point pages of each main section. Some of the links are hidden from unauthenticated users by using a LoginView control. The display of the Login or Logout button and the Admin button is controlled as follows:

<asp:LoginView runat=”server” ID=”lv1”> <AnonymousTemplate>

<a href=”~/Login.aspx” runat=”Server”>Login</a>

</AnonymousTemplate>

<LoggedInTemplate>

<asp:LinkButton ID=”lnkLogout” runat=”server”

291

Chapter 9

OnClick=”lnkLogout_Click”>Logout</asp:LinkButton> </LoggedInTemplate>

<RoleGroups>

<asp:RoleGroup Roles=”Administrator”> <ContentTemplate>

<asp:LinkButton ID=”lnkLogout” runat=”server” OnClick=”lnkLogout_Click”>Logout</asp:LinkButton>

<span class=”Separator”>|</span>

<a href=”~/Management/” runat=”Server”>Admin</a> </ContentTemplate>

</asp:RoleGroup>

</RoleGroups>

</asp:LoginView>

All anonymous users get to see the Login button defined in the <AnonymousTemplate> element. Authenticated users see the Logout button instead. Users that are in the Administrator role see the Logout button and the Admin button. The Logout link is repeated because the <RoleGroups> element takes precedence over the <LoggedInTemplate>. That is, you can’t show content to a user from both the

<LoggedInTemplate> and a <ContentTemplate> of a <RoleGroup> at the same time.

In addition to the LoginView control, you can also use a LoginStatus control for the Login and Logout links. This control displays either a Login or a Logout link, depending in the current status.

The final important piece of the master page is the ContentPlaceHolder. This defines the region that pages implementing the master page can override with their own custom content.

Default.aspx

Default.aspx is the homepage for the WebShop and contains only introduction text and a link to the product catalog.

Login.aspx

The Login.aspx page deals with anything related to a user logging in to the system. It contains three of the new ASP.NET 2.0 login controls, each placed in its own HTML <fieldset> control to provide some visual separation. The following table lists each of the controls and their usage:

Control Type

Description

 

 

CreateUserWizard

This control allows a user to sign up for an account at the WebShop.

 

The control is used “out of the box” without any impacting

 

changes.

Login

Allows an existing user to log in to the WebShop. Just as the Create

 

UserWizard control, there isn’t much configured on this control

 

except for some textual changes.

PasswordRecovery

Allows a user to get a new password by answering the secret

 

question.

 

 

292

Wrox WebShop

The PasswordRecovery control needs a bit more explanation. By default, the control retrieves a user’s password based on his or her username. However, many users won’t remember their username. They do, however, remember their e-mail address. That’s why a little trick is deployed to get the user’s name based on his or her e-mail address automatically. First the UserNameInstructionText and UserName LabelText attributes of the control are changed to instruct the user to type an e-mail address instead of a username. Then in the VerifyingUser method of the PasswordRecovery control the user’s name is retrieved with the following code:

Protected Sub PasswordRecovery1_VerifyingUser(ByVal sender As Object, _ ByVal e As System.Web.UI.WebControls.LoginCancelEventArgs) _ Handles PasswordRecovery1.VerifyingUser

PasswordRecovery1.UserName = _

Membership.GetUserNameByEmail(PasswordRecovery1.UserName)

End Sub

This way, the user can supply an e-mail address, while under the hood the user’s name is used to assign the new password.

UserDetails.aspx

The user details page allows a user to supply information such as a name and shipping address. The page contains six text boxes, one for each of the properties from the profile. At load time, the controls are filled with the user’s details from the profile if the information is available. When the Save button is clicked, the values from the controls are saved in the profile. The page uses validation controls to make sure each field is filled in.

The Shop Folder

The Shop folder contains all files required for the front end of the shop, like the product catalog, the shopping cart, and the checkout page. Before each page in the front end is discussed, take a look at Figure 9-13, which shows a typical work flow a user follows while making purchases in the WebShop.

You enter the homepage of the site (1) and then proceed to the product catalog (2). When you see a product you want to purchase you can add it to the shopping cart (3). From there you can browse back to the product catalog and add more items. Once you’re ready to make the purchase, you move on to the checkout page (4). To finalize the order, you need to log in (5). If you don’t have an account yet, you should sign up for one first (6). After you log in, the system asks you for additional details like name and shipping address (7). Once all details have been filled in and validated, the order is finalized (8) and you get a confirmation message, showing the order details and order number (9).

The next section of this chapter digs deeper into each of the pages involved in this process.

293

Chapter 9

 

 

 

 

 

 

 

 

 

 

Proceed to

 

 

 

 

 

 

 

 

 

 

Enter Site

 

 

Visit Product

 

 

Add Product

 

 

 

Catalog

 

 

to Cart

 

Check Out

1

 

 

 

 

 

 

2

 

3

4

 

 

 

 

 

 

 

 

 

 

 

 

Display

 

 

Finalize

 

 

 

 

 

User Details

 

 

 

 

 

 

 

 

 

Yes

 

 

Yes

 

Signed In?

Confirmation

 

 

Order

8

 

 

 

Known

 

 

 

 

 

 

 

 

 

 

 

 

9

 

 

 

 

 

 

 

 

 

 

 

 

No

 

 

 

 

 

 

 

Sign In

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

5

 

 

Yes

 

 

Known User?

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

No

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

No

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Gather User

 

 

 

 

 

 

 

Sign up for an

 

Details

 

 

 

 

 

 

 

 

account

 

7

 

 

 

 

 

 

 

 

6

Figure 9-13

Displaying Products

When you click the Shop menu item on the homepage you’re taken to Default.aspx in the shop folder (number 2 in Figure 9-13). This page is split into two parts using an HTML table. The left column contains an ASP.NET Repeater control that displays the available product categories. The categories are added to the Repeater through an ObjectDataSource control that gets the items as a DataSet from the GetProductCategories method in the ShopManager class. When you click one of the categories, the product list on the right is reloaded to show only the products that belong to that category.

The products are displayed using a DataList with its RepeatColumns set to 2, so it displays two products next to each other. Right below the DataList are two HyperLink controls to move forward and backward in the list. The .aspx portion of this page doesn’t feature much news so it isn’t discussed any further. What’s more interesting to look at is the code-behind for the file.

Because the DataList doesn’t support paging out of the box, the page uses a custom paging solution with a PagedDataSource. The PagedDataSource control is the underlying mechanism for controls that support paging natively like the GridView and the DetailsView. In the LoadData method of the code-behind file a new PagedDataSource control is created, and then its DataSource is filled with the results of ShopManager.GetProductList. In addition to the data source you can set properties like

AllowPaging, PageSize, and CurrentPageIndex the same way as you would for a GridView. The

CurrentPageIndex is calculated by looking at a query string variable called Page. In the end, the PagedDataSource control is assigned to the GridView, which is then responsible for displaying the data held in the PagedDataSource.

294

Wrox WebShop

The GetProductList method to fill the data source is simply a wrapper around the GetProducts method of the ShopManagerDB class. If scalability and performance is of the highest importance, you could use the method in the Business Layer class to cache the data in the ASP.NET cache. However, the current implementation always returns fresh data from the database. In Chapter 12 you learn how to cache data using the new SqlCacheDependency, which removes items from the cache whenever the underlying table in the database changes.

The GetProducts method in the ShopManagerDB class executes a stored procedure in the database to get a list of products for the requested category. That list is then returned to the calling code as a generic List with the new syntax (Of Product), which means a strongly typed list of individual products is returned. To see how that all works, take a look at the code for the method:

Public Shared Function GetProducts(ByVal categoryId As Integer) As List(Of Product) Dim productList As List(Of Product) = New List(Of Product)

Try

Using myConnection As New SqlConnection(AppConfiguration.ConnectionString)

Dim myCommand As SqlCommand = New _ SqlCommand(“sprocProductSelectListByProductCategory”, myConnection)

myCommand.CommandType = CommandType.StoredProcedure myCommand.Parameters.AddWithValue(“@categoryId”, categoryId)

Dim theProduct As Product ‘Temp Product to add to our ProductList

myConnection.Open()

Using myReader As SqlDataReader = _ myCommand.ExecuteReader(CommandBehavior.CloseConnection)

While myReader.Read() theProduct = New Product( _

myReader.GetInt32(myReader.GetOrdinal(“Id”)), _ categoryId)

theProduct.Title = myReader.GetString(myReader.GetOrdinal(“Title”)) ‘ Other properties are set here

productList.Add(theProduct) End While

myReader.Close() End Using

End Using

Catch ex As Exception Throw

End Try

Return productList End Function

The method starts by declaring a string with the name of the stored procedure and a List of type Product. With the special syntax of Of Product, the variable productList is capable of performing all the actions defined in the List class (located in the System.Collections.Generic namespace), while at the same time it can only work with Product instances. This gives you a very flexible yet type safe way to work with collections of objects. Generics are a powerful but complex concept to understand. Professional .NET 2.0 Generics by Tod Golding provides you with all the information you need to know to successfully implement Generics in your code.

295

Chapter 9

Next, a connection and a command object are constructed. The categoryId is passed up to the stored procedure so it only returns products that are in the requested category. If the stored procedure returns any results, the code inside the While loop creates a new instance of the Product class, sets its public variables, and then adds the product to the product list. In the end, the list of products, possibly holding zero, one, or many products, is passed back to the calling code.

You may have noticed the use of a Try Catch block to intercept any errors. Instead of logging the error to a log file, or dealing with it, it is simply passed up using the Throw keyword. Because there isn’t much to be done about a SQL error, for example, inside this method, the error is passed up to the calling code. Eventually, the error will be caught by code in the Global.asax that was set up to handle these errors and log them. If you want to log errors at the place where they occur, you can create a custom method like LogError in the Helpers class that you call in each of the Catch clauses in addition to the Throw keyword. This method could accept an object of type Exception, which it then logs to the event log, a database, a text file, or an e-mail.

If you don’t like this almost-empty Catch block, you can remove the Try Catch altogether. In that case, the error is passed up automatically. However, you then lose the ability to add a Finally block that you could use to clean up resources, such as open connections. That’s why you’ll see the Try Catch block with a Throw statement a lot in this and other chapters; that way, there’s already a code template in place that you can extend later if required.

Instead of just Throw, other code examples you may have seen use Throw Ex to forward the caught exception to the next layer. However, this is not recommended. By using Throw Ex, you effectively destroy the call stack of the current execution. That way, final code that catches the exception (for example, the code in the Global.asax that sends error messages by e-mail) has no way to find out where the exception came from originally.

Adding a Product to the Cart

The GridView in the Product List page contains HyperLink controls that link to the ProductDetail.aspx where a user can view more information about a product and add it to the shopping cart (number 3 in Figure 9-13). The code in this page is very similar to that in the product list. But instead of a GridView, a DetailsView control is used that is bound to the return value of the GetProduct method using an ObjectDataSource. When the page loads, the ObjectDataSource calls into the business layer and gets a single product specified by the ID in the query string. This product is then presented on the page with the DetailsView control.

When a user clicks the Add to Cart button, a new instance of the product is created and added to the cart:

Dim productId As Integer

productId = Convert.ToInt32(Request.QueryString.Get(“Id”)) Dim myProduct As Product = ShopManager.GetProduct(productId) ShopManager.AddProductToCart(myProduct) Response.Redirect(“ShoppingCart.aspx”)

The new instance of the product is created using the GetProduct method. This is the same method the ObjectDataSource control uses to display the product on the page. This instance is then passed to the AddProductToCart method that in turn calls the Add method of the ShoppingCart:

Public Shared Sub AddProductToCart(ByVal theProduct As Product)

ShopManager.ShoppingCart.Add(theProduct)

End Sub

296