Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 70 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
70
Dung lượng
2,24 MB
Nội dung
CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 469 Figure 12-11. Blank user details page 26. Click Edit and enter some details, as shown in Figure 12-12. Darie-Watson_4681C12.fm Page 469 Monday, September 12, 2005 6:50 AM 470 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS Figure 12-12. User details page in edit mode 27. Click Update and note how the credit card number is displayed as XXXX-XXXX-XXXX-1234. 28. Log out (you should be redirected to the log in page), and then log back in again as a different user. When you look at the user details for this user, you should see that the details are blank—they are unique to users. How It Works: Implementing User Profiles for BalloonShop That was a long exercise! Still, at no point have you seen any particularly complicated code. In fact, most of it was to make the user details edit form look good. Still, there’s plenty to analyze, starting with the way that user profile data is exposed to the FormView control via an ObjectDataSource control. Sadly, there is no direct way to bind user profile data to controls. Many methods are available for doing this (for example, a fellow author and friend, Dave Sussman, created a generic way to do this, see http://blogs. ipona.com/davids/archive/2004/10/29/414.aspx). You could even take the simple option—ignore data binding and build a form yourself, setting Text properties of TextBox or Label controls to appropriate values in the code behind. Because you have encrypted credit card details available, you needed to take a slightly oblique approach to keep the data in the database secure; going with the data-bound approach is also a good test for your ASP.NET development muscles. To start with, let’s look at ProfileWrapper. The code for this class starts with a reference to the SecurityLib library and a bunch of private fields. These fields cover all the fields defined in web.config, along with credit card fields obtained from the SecureCard class in the SecurityLib namespace: Darie-Watson_4681C12.fm Page 470 Monday, September 12, 2005 6:50 AM CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 471 using System; using System.Web; using System.Web.Security; using SecurityLib; /// <summary> /// A wrapper around profile information, including /// credit card encryption functionality. /// </summary> public class ProfileWrapper { private string address1; private string address2; private string city; private string region; private string postalCode; private string country; private string shippingRegion; private string dayPhone; private string evePhone; private string mobPhone; private string email; private string creditCard; private string creditCardHolder; private string creditCardNumber; private string creditCardIssueDate; private string creditCardIssueNumber; private string creditCardExpiryDate; private string creditCardType; These fields all have associated public properties, which weren’t all listed to save space. Next, the constructor for the ProfileWrapper class obtains the profile information for the currently logged-in user and populates the preceding fields. Because this class isn’t the code behind for a Web Form, you can’t use the Page.Profile property to access this information, so instead you used the static HttpContext.Current property to obtain the current context. From this, you get the ProfileCommon instance that you’re interested in: public ProfileWrapper() { ProfileCommon profile = HttpContext.Current.Profile as ProfileCommon; From this object, you extract all the data you want. Most of this is simply a case of examining properties of the ProfileCommon instance, but in some cases more code is required. For instance, for shippingRegion, we wanted to use a drop-down list rather than a text box (because limited options are available), so we initialized the field accordingly—if profile.ShippingRegion is empty, you instead use the text "1", which matches the ShippingRegionID for “Please Select” in the ShippingRegion table. You could do this for some of the other properties, notably Country, but for simplicity (and brevity) we’ve kept things simple. Darie-Watson_4681C12.fm Page 471 Monday, September 12, 2005 6:50 AM 472 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS You also extracted the email address of the user by obtaining a MembershipUser object via Membership. GetUser() and passing the username obtained from the profile as a parameter. You then used the Email property of this object to obtain the email address. Strictly speaking, the user’s email address isn’t part of the user’s profile, but it makes sense to expose it here for easy editing. creditCard also needs more work. You needed to decrypt any information stored and use the decrypted data to fill the appropriate fields: creditCardHolder, creditCardNumber, and so on. Because a decryption failure results in an exception, this decryption is performed in a try catch block. address1 = profile.Address1; address2 = profile.Address2; city = profile.City; region = profile.Region; postalCode = profile.PostalCode; country = profile.Country; shippingRegion = (profile.ShippingRegion == null || profile.ShippingRegion == "" ? "1" : profile.ShippingRegion); dayPhone = profile.DayPhone; evePhone = profile.EvePhone; mobPhone = profile.MobPhone; email = Membership.GetUser(profile.UserName).Email; try { SecureCard secureCard = new SecureCard(profile.CreditCard); creditCard = secureCard.CardNumberX; creditCardHolder = secureCard.CardHolder; creditCardNumber = secureCard.CardNumber; creditCardIssueDate = secureCard.IssueDate; creditCardIssueNumber = secureCard.IssueNumber; creditCardExpiryDate = secureCard.ExpiryDate; creditCardType = secureCard.CardType; } catch { creditCard = "Not entered."; } } Next the UpdateProfile method sets profile data, email data, and credit card details from the data contained in the object instance from which it is called. Again, the code here is simple, with only some minor trickery required to obtain the encrypted form of the credit card data. There’s nothing here you haven’t seen elsewhere, so there’s no need to repeat the code here. To use object data with the ObjectDataSource control, you needed to pass an object supporting IEnumerable as the return result of a SELECT method. This is because ObjectDataSource is designed to work with data lists as well as single data items. ProfileDataSource acts as an interface between ObjectDataSource and Darie-Watson_4681C12.fm Page 472 Monday, September 12, 2005 6:50 AM CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 473 ProfileWrapper, simply using the IEnumerable that is supporting generic list class List<T> to pass data to ObjectDataSource. The code instantiates an instance of List<ProfileWrapper> and adds a single item, the user’s profile data, to this list and then returns it. public List<ProfileWrapper> GetData() { List<ProfileWrapper> data = new List<ProfileWrapper>(); data.Add(new ProfileWrapper()); return data; } Because List<T> actually supports IEnumerable<T>, this is a strongly typed binding, meaning that the UPDATE method is passed an argument of type T when ObjectDataSource calls it. In this case, T is ProfileWrapper, so to update the profile information, you just called the UpdateProfile() method: public void UpdateData(ProfileWrapper newData) { newData.UpdateProfile(); } Next, you used these classes to populate a FormView control via the aforementioned ObjectDataSource control. The templates created needed a bit of modification, because we didn’t want to display all the credit card fields on the initial item view. We also wanted to use a drop-down list for the shippingRegion property, and bound that drop-down list to the ShippingRegion table using simple data-binding syntax. This customization required a lot of code, but most of this was for general display purposes, so there’s no real need to go through it in any depth here. Suffice to say that the credit card details get fully displayed for the editing template. ■Note We haven’t done it here, but it would be relatively easy to modify this code to enable customers to store multiple credit cards, with one selected as a default to use for purchases. You could, for example, store an array of strings for credit card details, each containing one encrypted card, along with a default card prop- erty. Alternatively, you could extend SecureCard to provide a single encrypted string for multiple cards. The only reason this hasn’t been done here is to keep things moving—there’s no reason to get bogged down in lengthy, uninteresting code at this point. Another feature that’s lacking here is the inclusion of validation controls to ensure that required fields are filled in. Again, this is easy to add, but would have filled up another page or so if included here. You used a user control to store the customer details editing form, CustomerDetailsEdit.ascx. There’s a good reason for this—later you’ll want to display the same information to customers when they place their orders, giving them a last chance to modify details. To facilitate this reuse, CustomerDetails.ascx.cs includes two public properties, Editable and Title, which can be used to hide the EditButton button and set the title for the FormView control, respectively. This customization happens in the OnPreRender event handler for the control, to cater for the fact that these properties may be set late on in the life cycle of the control, and we still want them to work if this happens. For the Edit Details page, you use the default values for these properties; later you’ll supply nondefault values for them. Darie-Watson_4681C12.fm Page 473 Monday, September 12, 2005 6:50 AM 474 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS The page displaying the CustomerDetailsEdit.ascx user control (CustomerDetails.aspx) needed to have its access limited to users in the Customers role, so you added the required security code to web.config. Note that the code in web.config prevents users in the Administrators role from editing profiles. This isn’t a problem, however, because administrators don’t need to store this information. Finally, you tested things out by entering some details for a customer and verified that the information added applied only to that customer. Now that you have this information available, you can move on to the next step—providing a new checkout page. The Checkout Page The new checkout page will display an order summary and customer details, which can be reviewed before the customer places an order. This page appears when a customer clicks the Proceed to Checkout button after viewing his shopping cart. In the next exercise, you’ll implement and secure this page. Exercise: Implementing a new Checkout Page 1. Add a new page to the BalloonShop application called Checkout.aspx and modify the code as follows: <%@ Page Language="C#" MasterPageFile="~/BalloonShop.master" AutoEventWireup="true" CodeFile="Checkout.aspx.cs" Inherits="Checkout" %> <%@ Register TagPrefix="uc1" TagName="CustomerDetailsEdit" Src="UserControls/CustomerDetailsEdit.ascx" %> <asp:Content ID="Content1" ContentPlaceHolderID="contentPlaceHolder" runat="Server"> <asp:Label ID="titleLabel" runat="server" CssClass="ShoppingCartTitle" Text="Your Shopping Cart" /> <br /> <br /> <asp:GridView ID="grid" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" BorderWidth="1px" Width="100%"> <Columns> <asp:BoundField DataField="Name" HeaderText="Product Name" ReadOnly="True" SortExpression="Name" /> <asp:BoundField DataField="Price" DataFormatString="{0:c}" HeaderText="Price" ReadOnly="True" SortExpression="Price" /> <asp:BoundField DataField="Quantity" HeaderText="Quantity" ReadOnly="True" SortExpression="Quantity" /> Darie-Watson_4681C12.fm Page 474 Monday, September 12, 2005 6:50 AM CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 475 <asp:BoundField DataField="Subtotal" DataFormatString="{0:c}" HeaderText="Subtotal" ReadOnly="True" SortExpression="Subtotal" /> </Columns> </asp:GridView> <asp:Label ID="Label2" runat="server" Text="Total amount: " CssClass="ProductDescription" /> <asp:Label ID="totalAmountLabel" runat="server" Text="Label" CssClass="ProductPrice" /> <br /> <br /> <uc1:CustomerDetailsEdit ID="CustomerDetailsEdit1" runat="server" Editable="false" Title="User Details" /> <br /> <asp:Label ID="InfoLabel" runat="server" CssClass="InfoText" /> <br /> <br /> <asp:Button ID="placeOrderButton" runat="server" CssClass="ButtonText" Text="Place order" OnClick="placeOrderButton_Click" /> </asp:Content> 2. Modify Checkout.aspx.cs as follows: public partial class Checkout : System.Web.UI.Page { protected void Page_Load(object sender, EventArgs e) { // Set the title of the page this.Title = BalloonShopConfiguration.SiteName + " : Checkout"; if (!IsPostBack) PopulateControls(); } // fill controls with data private void PopulateControls() { // get the items in the shopping cart DataTable dt = ShoppingCartAccess.GetItems(); // populate the list with the shopping cart contents grid.DataSource = dt; grid.DataBind(); // setup controls titleLabel.Text = "These are the products in your shopping cart:"; grid.Visible = true; Darie-Watson_4681C12.fm Page 475 Monday, September 12, 2005 6:50 AM 8213592a117456a340854d18cee57603 476 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS // display the total amount decimal amount = ShoppingCartAccess.GetTotalAmount(); totalAmountLabel.Text = String.Format("{0:c}", amount); // check customer details bool addressOK = true; bool cardOK = true; if (Profile.Address1 + Profile.Address2 == "" || Profile.ShippingRegion == "" || Profile.ShippingRegion == "Please Select" || Profile.Country == "") { addressOK = false; } if (Profile.CreditCard == "") { cardOK = false; } // report / hide place order button if (!addressOK) { if (!cardOK) { InfoLabel.Text = "You must provide a valid address and credit card " + "before placing your order."; } else { InfoLabel.Text = "You must provide a valid address before placing your " + "order."; } } else if (!cardOK) { InfoLabel.Text = "You must provide a credit card before " + "placing your order."; } else { InfoLabel.Text = "Please confirm that the above details are " + "correct before proceeding."; } placeOrderButton.Visible = addressOK && cardOK; } Darie-Watson_4681C12.fm Page 476 Monday, September 12, 2005 6:50 AM CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 477 protected void placeOrderButton_Click(object sender, EventArgs e) { // Store the total amount because the cart // is emptied when creating the order decimal amount = ShoppingCartAccess.GetTotalAmount(); // Create the order and store the order ID string orderId = ShoppingCartAccess.CreateOrder(); // Create the PayPal redirect location string redirect = ""; redirect += "https://www.paypal.com/xclick/business=youremail@server.com"; redirect += "&item_name=BalloonShopOrder " + orderId; redirect += "&item_number=" + orderId; redirect += "&amount=" + String.Format("{0:c} ", amount); redirect += "&return=http://www.YourWebSite.com"; redirect += "&cancel_return=http://www.YourWebSite.com"; // Redirect to the payment page Response.Redirect(redirect); } } 3. Add the following class definition to BalloonShop.css: .InfoText { font-family: Verdana, Helvetica, sans-serif; font-size: 12px; } 4. Modify web.config as follows: <! Only existing customers can access Checkout.aspx > <location path="Checkout.aspx"> <system.web> <authorization> <allow roles="Customers" /> <deny users="*" /> </authorization> </system.web> </location> 5. Modify ShoppingCart.aspx.cs as follows: // redirect to the checkout page protected void checkoutButton_Click(object sender, EventArgs e) { string redirect = "Checkout.aspx"; // Redirect to the checkout page Response.Redirect("Checkout.aspx"); } Darie-Watson_4681C12.fm Page 477 Monday, September 12, 2005 6:50 AM 478 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 6. Log in, edit your customer details, and place an order via the shopping cart page. If your details are correct, you should be able to click Proceed to Checkout; otherwise, you’ll have to add valid customer details before proceeding. How It Works: Implementing a New Checkout Page We haven’t really done much that is particularly difficult here—most of the work was already done. All we’ve really done is reorganize existing code to prepare for a new order backend. The new checkout page, Checkout.aspx, now appears when customers click Proceed to Checkout from the shopping cart view. It displays the current shopping cart using code very similar to—but not identical to—code in ShoppingCart.aspx. The code is different in that editing is no longer possible—quantities and order items are fixed. Checkout.aspx also includes a noneditable version of the CustomerUserDetails.ascx control, customized using the Editable and Title properties you added earlier. As with ShoppingCart.aspx, you use a code-behind method called PopulateControls() to get and bind to data. The major differences here are that you don’t need to check for existing shopping cart items (we know there will be some at this stage), and that you also check for valid address and credit card details before allowing the user to proceed: // check customer details bool addressOK = true; bool cardOK = true; if (Profile.Address1 + Profile.Address2 == "" || Profile.ShippingRegion == "" || Profile.ShippingRegion == "1" || Profile.Country == "") { addressOK = false; } if (Profile.CreditCard == "") { cardOK = false; } This code, which checks the validity of the address and credit card, merits a little extra discussion. First, notice the validation of the address. The address is validated by checking a few of the fields for data (Address1 and Address2 combined must contain data for a valid order, and a country and shipping region must be set). This may look overly simple, but it’s fine here—if address problems occur further down the line, you can deal with problems as they arise. The shipping region is also interesting because you check for a value of “1”, which corresponds to “Please Select” in the database—hence the importance of this record having an ID field value of 1, as noted earlier. As far as credit card details go, you just check that some data is stored, not what that data is. Again, problems here can be dealt with later. Assuming that the data is okay, the placeOrder button allows users to actually place an order. Notice that the code here is the same code you used in the earlier incarnation of the ShoppingCart.aspx page. In fact, none of the extra details are used. This isn’t a problem because you now have everything you need to hook into a proper, fleshed-out order pipeline, as you’ll see in subsequent chapters. Darie-Watson_4681C12.fm Page 478 Monday, September 12, 2005 6:50 AM [...]... orders: In this section, you’ll enable customers to place orders • Accessing customer orders: In this section, you’ll enable the order-processing system in later chapters to access customer orders Placing Customer Orders To enable customers to place orders using ASP.NET membership, you need to make several modifications You’ll modify the database and business tier to enable customer orders to be placed... orderRow["OrderID"].ToString()); // set info properties Refresh(); } public void Refresh() { // calculate total cost and set data StringBuilder sb = new StringBuilder(); TotalCost = 0.0; foreach (CommerceLibOrderDetailInfo item in OrderDetails) { sb.AppendLine(item.ItemAsString); TotalCost += item.Subtotal; } sb.AppendLine(); sb.Append("Total order cost: $"); sb.Append(TotalCost.ToString()); OrderAsString = sb.ToString();... time-saving operation Finally, a Refresh method similar to the one in CommerceLibOrderDetailInfo is used to initialize some utility fields: TotalCost, OrderAsString, and CustomerAddressAsString You’ll use all of these for more speedy access to order details later The GetOrder Method The last thing to add to the CommerceLibAccess class is a method to obtain an order, in the form of a CommerceLibOrderInfo... Darie-Watson_4 681 C13.fm Page 495 Wednesday, September 14, 2005 9:27 AM CHAPTER 13 ■ ADVANCED CUSTOMER ORDERS // get customer address string sb = new StringBuilder(); sb.AppendLine(Customer.UserName); sb.AppendLine(CustomerProfile.Address1); if (CustomerProfile.Address2 != "") { sb.AppendLine(CustomerProfile.Address2); } sb.AppendLine(CustomerProfile.City); sb.AppendLine(CustomerProfile.Region); sb.AppendLine(CustomerProfile.PostalCode);... also need to modify CreateCustomerOrder so that a tax and a shipping option are added when an order is added The modifications are as follows: ALTER PROCEDURE CreateCustomerOrder (@CartID char(36), @CustomerID uniqueidentifier, @ShippingID int, @TaxID int) AS /* Insert a new record into Orders */ DECLARE @OrderID int INSERT INTO Orders (CustomerID, ShippingID, TaxID) VALUES (@CustomerID, @ShippingID,... you’ve implemented a customer account system that customers can use to store their details for use during order processing You’ve looked at many aspects of the customer account system, including encrypting sensitive data, and securing web connections for obtaining it You started by creating a set of classes in a new namespace called SecurityLib for hashing and encrypting strings, and a secure credit... 12, 2005 6:50 AM 482 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS attempt to access Login.aspx without SSL, it’s better to detect unsecure connections in code and redirect accordingly This means that users trying to access Login.aspx without SSL are automatically redirected to the same page, but with SSL Similarly, we want users attempting to use SSL to access a page such as Default.aspx—which doesn’t need to. .. need to access the current instance from the current context to do this After you’ve obtained a ProfileCommon instance for the customer, you simply store it in a publicly accessible field, just like the other order information This will make it easy for you later, because you’ll be able to access customer profile information with very simple syntax From the information stored in the ProfileCommon instance,... sb.Append(UnitCost.ToString()); sb.Append(" each, total cost $"); sb.Append(Subtotal.ToString()); ItemAsString = sb.ToString(); } } This class wraps a row from the OrderDetail table Note that we aren’t using a struct for this functionality This is because structs can’t have constructors, and to make initialization easier, this class uses a constructor that takes a DataRow object to initialize itself This constructor... double TotalCost; public string OrderAsString; public string CustomerAddressAsString; public List OrderDetails; public CommerceLibOrderInfo(DataRow orderRow) { OrderID = Int32.Parse(orderRow["OrderID"].ToString()); DateCreated = orderRow["DateCreated"].ToString(); DateShipped = orderRow["DateShipped"].ToString(); Comments = orderRow["Comments"].ToString(); Status = Int32.Parse(orderRow["Status"].ToString()); . products in your shopping cart:"; grid.Visible = true; Darie-Watson_4 681 C 12. fm Page 475 Monday, September 12, 20 05 6: 50 AM 82 1 3592a117456a3 4 08 54d18cee57 603 476 CHAPTER 12 ■ ADDING CUSTOMER. result. Darie-Watson_4 681 C 12. fm Page 479 Monday, September 12, 20 05 6: 50 AM 4 80 CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS Obtaining an SSL Certificate from VeriSign Obtaining a certificate from VeriSign. shown in Figure 12- 14. Darie-Watson_4 681 C 12. fm Page 4 80 Monday, September 12, 20 05 6: 50 AM CHAPTER 12 ■ ADDING CUSTOMER ACCOUNTS 481 Figure 12- 13. File Security property page Figure 12- 14. Setting