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

ASP.NET 2.0 Instant Results

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

Wrox WebShop

The user’s shopping cart is made accessible by the ShopManager class with a shared property called

ShoppingCart:

Public Shared ReadOnly Property ShoppingCart() As ShoppingCart

Get

If HttpContext.Current.Session(“ShoppingCart”) Is Nothing Then

HttpContext.Current.Session(“ShoppingCart”) = New ShoppingCart()

End If

Return CType(HttpContext.Current.Session(“ShoppingCart”), ShoppingCart)

End Get

End Property

The first time this property is accessed, a new instance of the ShoppingCart is created, stored in a session variable, and then returned to the calling code. Subsequent calls to the property return the ShoppingCart from session state. This way the shopping cart is always available.

If there is already an OrderedProduct with the same ID in the shopping cart, its quantity is increased by one. Otherwise, a new instance of OrderedProduct is created. Its constructor expects an instance of Product that is stored in a private variable called _theProduct and the initial quantity, which is set to 1. The ordered product is then added to the _items list that represents the user’s shopping cart:

Public Sub Add(ByVal theProduct As Product)

For Each existingProduct As OrderedProduct In _items If theProduct.Id = existingProduct.ProductId Then

existingProduct.Quantity += 1 Exit Sub

End If Next

Dim myOrderedProduct As OrderedProduct = New OrderedProduct(theProduct, 1) _items.Add(myOrderedProduct)

End Sub

The ShoppingCart.aspx Page

After the product has been added the user is taken to ShoppingCart.aspx, which displays the products. This page by itself doesn’t do much. It has a few static text blocks that are displayed depending on whether the cart contains any items. The actual shopping cart is displayed by embedding a user control called ShoppingCartView:

<Wrox:ShoppingCartView ID=”ShoppingCartView1” runat=”server” />

The ShoppingCartView control, located in the Controls folder, contains a GridView that displays the products and an ObjectDataSource that’s responsible for retrieving the items from the cart. To allow deleting and updating of products, the ObjectDataSource control sets up the appropriate methods and parameters for these actions:

<asp:ObjectDataSource ID=”odsShoppingCart” runat=”server” TypeName=”ShopManager” DeleteMethod=”RemoveProductFromCart” SelectMethod=”GetShoppingCartItems” UpdateMethod=”UpdateProductInCart”> <UpdateParameters>

<asp:Parameter Name=”newQuantity” Type=”Int32” /> </UpdateParameters>

</asp:ObjectDataSource>

297

Chapter 9

The SelectMethod, UpdateMethod, and DeleteMethod all call into the ShopManager class. The

GetShoppingCartItems method simply returns the public Items list of the ShoppingCart. As you recall, this list is a strongly typed list of OrderedProduct items. This list is then bound to the GridView using a mix of BoundField, ImageField, and TemplateField columns when the page loads.

Changing Items in the Cart

The Quantity column is a bit more complicated than other columns like Price or Title. In edit mode, the GridView displays this column as a drop-down with the numbers 1 through 10 to allow a user to choose a new quantity:

<asp:DropDownList ID=”lstQuantity” runat=”server” SelectedValue=’<%# Eval(“Quantity”) %>’ AutoPostBack=”True” OnSelectedIndexChanged=”lstQuantity_SelectedIndexChanged”>

<asp:ListItem Value=”1” Selected=”True”>1</asp:ListItem>

... Other items go here </asp:DropDownList>

The SelectedValue for the drop-down list is bound to the Quantity property of the OrderedProduct with the Eval method. It also has its AutoPostBack property set to True to automatically post back when its selected value changes. When that happens, lstQuantity_SelectedIndexChanged is fired. This method then calls the UpdateRow method of the GridView. This in turn causes the ObjectDataSource to fire its Updating event, which fires right before it actually calls the Update method in the business layer. The Updating event is an excellent location to set the parameters that need to be passed to the Update ProductInCart method. In the section about the design of the WebShop, you saw that the UpdateProduct InCart method expects the ID of the OrderedProduct in the shopping cart and the new quantity. These values are passed through that method with the following code in the Updating event:

Protected Sub odsShoppingCart_Updating(ByVal sender As Object, _

ByVal e As System.Web.UI.WebControls.ObjectDataSourceMethodEventArgs) _ Handles odsShoppingCart.Updating

e.InputParameters(“Id”) = _

New Guid(GridView1.DataKeys(GridView1.EditIndex).Value.ToString()) e.InputParameters(“newQuantity”) = Convert.ToInt32( _

CType(GridView1.Rows(GridView1.EditIndex).FindControl(“lstQuantity”), _ DropDownList).SelectedValue)

End Sub

The GridView that uses the ObjectDataSource control (GridView1) has its DataKeys property set to Id, which uniquely identifies each OrderedProduct that is being displayed in the cart. The ObjectDataSource sees this relation and automatically sets up a parameter for this Id field. In the previous snippet, that Id parameter is then given a value with this code:

e.InputParameters(“Id”) = _

New Guid(GridView1.DataKeys(GridView1.EditIndex).Value.ToString())

The item in the cart that is currently being edited is retrieved with GridView1.EditIndex. This index is then passed to the GridView control’s DataKeys collection to get the unique Id for the OrderedProduct. This Id is then converted to a GUID and assigned to e.InputParameters(“Id”), the first parameter that the UpdateProductInCart method of the ShopManager class expects.

298

Wrox WebShop

The new ordered quantity is passed to that method in a similar way. Because the GridView knows nothing intrinsically about the Quantity of an OrderedProduct, an explicit parameter called newQuantity has been set up in the <UpdateParameters> node of the ObjectDataSource that you saw earlier. In the Updating event, this quantity is assigned a value with this code:

e.InputParameters(“newQuantity”) = Convert.ToInt32( _ CType(GridView1.Rows(GridView1.EditIndex).FindControl(“lstQuantity”), _ DropDownList).SelectedValue)

Again, GridView1.EditIndex is used to get the ID of the item that is being edited. However, in this case not the DataKeys but the Rows collection is queried for an item with that ID. Rows(GridView1

.EditIndex) returns a reference to the row in the cart that is being edited. Then the FindControl method is used to find a reference to the drop-down list with the new quantity. That quantity is converted to an Integer and finally passed to the e.InputParameters(“newQuantity”) parameter so it is eventually passed to UpdateProductInCart to update the ordered quantity for the OrderedProduct in the cart.

In addition to the Quantity column, the Edit column also deserves a closer examination. By default, when you add a CommandField with ShowEditButton set to True, you get a column that displays an Edit button. Once you click that Edit button, the selected row becomes editable and the Edit button is replaced with an Update and Cancel button.

For the shopping cart, the Update button is not desirable. The only item in each row that is editable is the Quantity drop-down. This drop-down posts back automatically and updates the cart. An additional Update button would confuse users. To remove the Update button, the CommandField is converted to a TemplateField using the GridView control’s Fields dialog, shown in Figure 9-14.

Figure 9-14

299

Chapter 9

To get at this dialog, right-click the GridView and choose Show Smart Tag. On the resulting GridView tasks dialog, click Edit Columns. Then locate the column you want to convert to a template in the Selected Fields list, and click the blue link with the text “Convert this field into a TemplateField” at the bottom-right of the dialog. The column is then converted into an <asp:TemplateField> with an

<ItemTemplate> and an <EditItemTemplate>.

Removing the highlighted code from the code generated by the conversion process removes the Update button from the column:

<asp:TemplateField ShowHeader=”False”> <ItemTemplate>

<asp:Button ID=”LinkButton1” runat=”server” CausesValidation=”False” CommandName=”Edit” Text=”Edit”></asp:Button>

</ItemTemplate>

<EditItemTemplate>

<asp:LinkButton ID=”LinkButton1” runat=”server” CausesValidation=”True”

CommandName=”Update” Text=”Update”></asp:LinkButton>  <asp:Button ID=”LinkButton2” runat=”server” CausesValidation=”False”

CommandName=”Cancel” Text=”Cancel”></asp:Button> </EditItemTemplate>

<ItemStyle Width=”100px” HorizontalAlign=”Center” /> </asp:TemplateField>

When you click the Edit button for a product in the cart, the GridView switches to edit mode and displays the EditItemTemplate for the quantity drop-down, as depicted in Figure 9-15.

Figure 9-15

You can now choose a new quantity by using just the drop-down; there is no need for an additional Update button. If you change your mind, you can click the Cancel button to stop editing.

Deleting an item requires no additional code; when the Delete button is clicked, RemoveProduct FromCart is called, which removes the ordered product from the shopping cart. However, to make the cart a bit more user-friendly, the Delete button was converted to a TemplateField as well, with the exact same method you just saw. With the Field converted to a TemplateField, it’s easy to ask users for confirmation when they click the Delete button with the Button’s OnClientClick event:

<ItemTemplate>

<asp:Button ID=”btnDelete” runat=”server” CausesValidation=”False” CommandName=”Delete” Text=”Delete” OnClientClick=”return confirm(‘Are you sure

you want to remove this product from your cart?’);” /> </ItemTemplate>

When users click Cancel on the confirmation dialog, nothing happens. If they click OK, the page posts back to the server and the item is removed from the cart.

300

Wrox WebShop

So far you have seen how the ObjectDataSource control is able to display, update, and delete items in the shopping cart. This solution, where the page posts back to itself, has one interesting challenge, though. The master page, on which the ShoppingCart.aspx page is based, has a ShoppingCartTotal control that displays the number of items in the cart and the total order amount. The label with the totals is filled in the Load event of the control. However, updating or removing the items from the cart happens after Page_Load. This means that the label with the totals has been set even before the cart is updated, causing the label to be out of sync with the actual shopping cart. To fix that problem, an event is implemented in the Shopping CartView control that fires when the cart is updated. The following block of code shows you how the event is declared, and how it is used in the RowUpdated method for the GridView:

Public Event CartUpdated As EventHandler

Protected Sub GridView1_RowUpdated(ByVal sender As Object, _

ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _

Handles GridView1.RowUpdated

RaiseEvent CartUpdated(sender, New System.EventArgs())

End Sub

The first line of this example sets up an Event that the control can raise when the shopping cart is updated. The next block shows the method GridView1_RowUpdated. This method fires whenever a row in the GridView is updated. This method uses RaiseEvent to raise the CartUpdated event. Calling code can subscribe to the event using the Handles syntax. This is done in the ShoppingCart.aspx page like this:

Public Sub ShoppingCartView1_CartUpdated(ByVal Sender As Object, _

ByVal e As EventArgs) Handles ShoppingCartView1.CartUpdated

Response.Redirect(“ShoppingCart.aspx”)

End Sub

This example simply redirects the user to ShoppingCart.aspx, which then loads the fresh order total from the cart into the ShoppingCartTotal control. As an alternative, you could create a method on the ShoppingCartTotal control that updates the label and then call that method instead.

Finalizing Orders

When you’re happy with the products in your shopping cart, you can click the Proceed to Check Out button to go to CheckOut.aspx (number 4 in Figure 9-13). However, this page is protected to unauthenticated users with the follow settings in the Web.config file:

<location path=”Shop/CheckOut.aspx”> <system.web>

<authorization> <deny users=”?”/>

</authorization>

</system.web>

</location>

So instead of the Check Out page, you get the Login page. On this page, you can sign up for a new user account or log in if you already have a username and password. Refer to the section called “Root Files” earlier in this chapter to see how this page works.

301

Chapter 9

Just like the Shopping Cart page, this Check Out page also contains a ShoppingCartView control to display the products in the cart. However, in Page_Load of the Check Out page, the ReadOnly property of the ShoppingCartView is set to True. When the ShoppingCartView control loads, it hides the columns that contain the Edit and Delete buttons, effectively blocking the user from making any more changes to the item in the cart:

If _isReadOnly Then

GridView1.Columns(5).Visible = False

GridView1.Columns(6).Visible = False

End If

If you click the Finalize Order button the following code checks if you have already completed your profile information:

If Profile.ProfileCompleted = True Then

... Shown later

Else

Response.Redirect(“~/UserDetails.aspx”)

End If

When the Boolean value ProfileCompleted is False, you’re taken to UserDetails.aspx. This page presents a series of text boxes where you can enter your first and last name and your address details. These details are then stored using the built-in Profile provider:

Page.Validate()

If Page.IsValid Then

Profile.FirstName = txtFirstName.Text

Profile.LastName = txtLastName.Text

Profile.Address.Street = txtStreet.Text

Profile.Address.ZipCode = txtZipCode.Text

Profile.Address.City = txtCity.Text

Profile.Address.Country = txtCountry.Text

Profile.ProfileCompleted = True

If ShopManager.ShoppingCart.Count > 0 Then

Response.Redirect(“~/Shop/CheckOut.aspx”)

Else

Response.Redirect(“~/”)

End If

End If

As you can see, the Profile feature makes it very easy to store user-specific data. All you need to do is assign the values from a control to the public profile property that’s been set up in the Web.config file.

After you complete the profile, you are either taken back to CheckOut.aspx, where you can review the order and shipping details and then click the Finalize Order button, or you’re taken back to the homepage. The code for the Finalize button retrieves the total order amount and then creates a new instance of a Customer object that it passes to the FinalizeOrder method of the ShopManager class. The total order amount must be retrieved here, because when FinalizeOrder has finished, the shopping cart is empty, and it no longer has a total order amount:

Try

Dim orderAmount As Decimal = ShopManager.ShoppingCart.Total

Dim theCustomer As Customer = _

302

Wrox WebShop

New Customer(CType(Membership.GetUser().ProviderUserKey, Guid), _ Profile.FirstName, Profile.LastName, Profile.Address.Street, _ Profile.Address.ZipCode, Profile.Address.City, Profile.Address.Country)

Dim orderId As Integer = ShopManager.FinalizeOrder(theCustomer) Response.Redirect(“ThankYou.aspx?OrderNumber=” & _

orderId.ToString() & “&Total=” & orderAmount.ToString(“c”)) Catch ex As Exception

lblFailure.Visible = True btnFinalize.Visible = False

End Try

The customer details come from two different sources — the customer ID is taken from the MembershipUser class, which exposes a ProviderUserKey property that is unique for each user in the system. All the other properties come from the user’s profile.

The FinalizeOrder method in the ShopManager class performs two actions. First it inserts the order and order details in the database by calling FinalizeOrder on the ShopManagerDB class. When the order has been saved successfully, the cart is then emptied to avoid the same order from being saved twice. The FinalizeOrder method in the ShopManagerDB class contains quite a bit of code, so the method is broken down in pieces and discussed line by line. The code begins by declaring a variable called myTransaction of type SqlClient.SqlTransaction:

Public Shared Function FinalizeOrder(ByVal theShoppingCart As ShoppingCart, _ ByVal theCustomer As Customer) As Integer

Dim myTransaction As SqlClient.SqlTransaction = Nothing

The order is saved partially in the OrderBase table and partially in the OrderDetail table. This is done with multiple INSERT statements. If any of the statements fails, you want to roll back the entire operation to avoid having incomplete orders in the database. It’s the SqlTransaction object’s responsibility to manage that process. All you need to do is wrap the code in a Try Catch block, assign the transaction object to each SqlCommand object you want to execute, and call Commit or Rollback, depending on the success of the operation. The SqlTransaction object is instantiated by calling the BeginTransaction method of a connection:

Try

Using myConnection As New SqlConnection(AppConfiguration.ConnectionString) myConnection.Open()

myTransaction = myConnection.BeginTransaction

The next block of code sets up the first SqlCommand object that inserts the order’s base data in the OrderBase table:

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

myCommand.Transaction = myTransaction myCommand.CommandType = CommandType.StoredProcedure

With the SqlCommand object instantiated, it’s time to pass the customer’s details to the stored procedure using SqlParameters and execute it. The code for the stored procedure isn’t shown here because it doesn’t do anything special. All it does is insert a new record in the OrderBase table, returning its new

303

Chapter 9

ID using the Scope_Identity() function of SQL Server. As of SQL Server 2000, Scope_Identity() is preferred over @@IDENTITY because the former returns the ID created in the current scope, like a stored procedure, whereas the latter could return an unrelated ID caused by a trigger on the table that you’re inserting the record into.

The next step is to add the parameters to the SqlCommand object using the AddWithValue method:

myCommand.Parameters.AddWithValue(“@CustomerId”, theCustomer.CustomerId)

... Other parameters are added here myCommand.Parameters.AddWithValue(“@Country”, theCustomer.Country)

Dim theReturnValue As SqlParameter = New SqlParameter() theReturnValue.Direction = ParameterDirection.ReturnValue myCommand.Parameters.Add(theReturnValue)

myCommand.ExecuteNonQuery()

The stored procedure returns the ID of the new record in the OrderBase table. That ID can be retrieved from the parameter theReturnValue. Because the return value is passed back as a generic object, it must be cast to an Integer using Convert.ToInt32:

Dim orderId As Integer = Convert.ToInt32(theReturnValue.Value)

The next block of code is responsible for inserting the order details and binding it to the OrderBase record that was created earlier. Another SqlCommand object is set up and assigned the transaction object that was created earlier (see the following code). This way this new command will participate in the same transaction:

Dim myCommand2 As SqlCommand = _

New SqlCommand(“sprocOrderDetailInsertSingleItem”, myConnection)

myCommand2.Transaction = myTransaction myCommand2.CommandType = CommandType.StoredProcedure

Just as with the first command, you need to pass parameters to the stored procedure. The code block that sets the parameters for the myCommand object used the convenient AddWithValue method that sets up the parameter automatically. However, in the case of the order details you cannot use that technique because you need to be able to use the parameters multiple times; once for each ordered product in the shopping cart. That’s why you need to declare and instantiate each parameter explicitly:

Dim orderBaseIdParam As SqlParameter = _

New SqlParameter(“OrderBaseId”, SqlDbType.Int) myCommand2.Parameters.Add(orderBaseIdParam)

Dim productIdParam As SqlParameter = _

New SqlParameter(“productId”, SqlDbType.Int) myCommand2.Parameters.Add(productIdParam)

Dim priceParam As SqlParameter = _

New SqlParameter(“price”, SqlDbType.Money) myCommand2.Parameters.Add(priceParam)

Dim quantityParam As SqlParameter = _

New SqlParameter(“quantity”, SqlDbType.Int) myCommand2.Parameters.Add(quantityParam)

304

Wrox WebShop

With the explicit parameters set up it’s now very easy to reuse them in a loop and assign them a different value that is retrieved from the ordered product being added:

For Each myOrderedProduct As OrderedProduct In theShoppingCart.Items orderBaseIdParam.Value = orderId

productIdParam.Value = myOrderedProduct.ProductId priceParam.Value = myOrderedProduct.Price quantityParam.Value = myOrderedProduct.Quantity myCommand2.ExecuteNonQuery()

Next

Just as the stored procedure that inserts the order base details, the stored procedure that inserts the order details is very simple as well. It simply inserts the product ID, the price, and the quantity of each item, and then relates the record to the OrderBase table by setting the OrderBaseId column. At this point, the entire order has been saved successfully so you call Commit to commit the transaction in the database and then return the new order ID to the calling code:

myTransaction.Commit() Return orderId

End Using

If an error occurred anywhere in this method, the code in the Catch block is executed. By calling Rollback on the transaction object you can let the database know that an error occurred and then it will undo any changes it has made so far. At the end, you call Throw to pass up the error in the call chain:

Catch ex As Exception myTransaction.Rollback() ‘ Pass up the error

Throw

End Try

End Sub

The order ID returned from the FinalizeOrder method in the data access layer is passed through the business layer to the Check Out page. That page passes it, together with the total order amount, to the Thank You page:

Response.Redirect(“ThankYou.aspx?OrderNumber=” & _

orderId.ToString() & “&Total=” & orderAmount.ToString(“c”))

The Thank You page instructs the user to transfer the money to the Wrox WebShop account before the goods will be shipped. As a reference, the order number and total order amount are displayed. Passing the order amount in the query string sounds like a security risk, but in this case it isn’t. The order has been completely finalized so there is no way to change it anymore. Also, the goods won’t be shipped until the customer has paid the full amount into the shop’s bank account.

This concludes the discussion of the front end of the web shop. With the finalization page, the whole ordering process is complete. Users can browse the product catalog, add items to their shopping cart, get a customer account and log in, and finalize their orders.

305

Chapter 9

The Management Folder

The Management folder is used to allow an administrator of the site to make changes to the products in the catalog. You have already seen most of the concepts used in this mini content management system in Chapter 5. However, there may be one thing you’re unfamiliar with. Whenever you create a new product and upload an image, three thumbnails are created automatically. In the classic ASP days, you’d need to buy a commercial third-party component or write some hefty C++ to resize images automatically. However, in the .NET era you need only a few lines of code. Take a look first at the code that fires whenever a new product is about to be inserted. You find the following code in the FormView1_ItemInserting method in the AddProduct.aspx.vb file:

First. try to save the images

Dim theFileUpload As FileUpload = CType( _ FormView1.FindControl(“FileUpload1”), FileUpload)

If theFileUpload.HasFile Then

Dim fileNameSmall As String = “~/Images/Products/” & Guid.NewGuid.ToString() Dim fileNameMedium As String = “~/Images/Products/” & Guid.NewGuid.ToString() Dim fileNameLarge As String = “~/Images/Products/” & Guid.NewGuid.ToString()

Dim theExtension As String = Path.GetExtension(theFileUpload.FileName)

fileNameSmall &= theExtension

fileNameMedium &= theExtension fileNameLarge &= theExtension

theFileUpload.SaveAs(Server.MapPath(fileNameLarge))

‘ Now resize the images Helpers.ResizeImage(Server.MapPath(fileNameLarge), _

Server.MapPath(fileNameSmall), 40)

Helpers.ResizeImage(Server.MapPath(fileNameLarge), _

Server.MapPath(fileNameMedium), 100)

Helpers.ResizeImage(Server.MapPath(fileNameLarge), _

Server.MapPath(fileNameLarge), 250)

The code first checks if an image has been uploaded. If HasFile of the Upload control returns True, three filenames are generated, one for each thumb. The extension for the files is determined by using Path.GetExtension and passing it the name of the uploaded file.

The final block of code creates the three thumbs by calling Helpers.ResizeImage and passing it the name of the image to resize, the name the thumb should be saved to, and the maximum width or height for each image (40 for the thumb used in the shopping cart, 100 for the image in the product catalog, and 250 for the image on the detail page). You see the implementation for the ResizeMethod in Chapter 11, where it’s discussed in full detail.

With this short description of the Management folder, you’ve come to the end of the “Code and Code Explanation” section. The next section describes the installation process of the WebShop application.

306