Chapter 17 購物網站 (1) - 產品目錄與購物車
本章提要 17 - 1 購物網站的架構與資料庫的規劃 17 - 2 產品列表頁 (Product.aspx) 17 - 3 購物車網頁 (Cart.aspx) 17 - 4 結帳頁面 (Checkout.aspx)
購物網站 (1) - 產品目錄與購物車 網路商店逐漸融入人們日常的購物習慣, 架設於網路上的購物網站可以突破時間、地域與店面大小的限制, 讓購物者可以隨時隨地觀看與購買商品。 本章及下一章將為您說明如何以 ASP.NET 製作購物網站。
17 - 1 購物網站的架構與資料庫的規劃 一個完整的購物網站應該包含產品、訂單、客戶、庫存等許多個管理系統, 不過限於篇幅, 所以本書將只實作產品與訂單管理兩個系統。
網站架構示意圖
網站架構示意圖 本章會先說明 Product.aspx、Cart.aspx、Checkout.aspx 這 3 個前端程式的製作方式, 而後端的管理系統則留待下一章介紹。
資料庫結構 請先建立一個名為 ShoppingCart 的資料庫, 以放置購物網站所需的資料表。 在前面的網站架構示意圖中, 可以看到購物網站內的資料庫共有 4 個資料表, 產品與產品類別資料表主要用來存放產品資訊, 而訂單與訂單明細資料表則儲存使用者的訂單。
產品與產品類別資料表 產品與產品類別資料表的結構如下:
產品與產品類別資料表 產品資料表的類別編號欄位會參照到產品類別資料表的編號欄位, 如果您不太熟悉關聯的概念, 請參閱這兩個資料表的內容如下:
產品與產品類別資料表 這兩個資料表的編號欄位都具有 Identity 屬性, 新增資料時, SQL Server 會自動以遞增的方式為每筆記錄編號, 所以程式插入資料時不需要特別指定編號。
訂單與訂單明細資料表 下面是訂 單與訂單 明細資料 表的結構:
訂單與訂單明細資料表 訂單明細資料表的訂單編號欄位參照到訂單資料表的編號欄位, 此外產品編號欄位則會參照到產品資料表的編號欄位。 下面是訂單與訂單明細資料表的內容:
訂單與訂單明細資料表 上面可以看到編號 1 的訂單共買了 3 個產品, 這 3 個分別是產品資料表中編號 1、7、8 的產品。 另外, 訂單資料表的訂購日期欄位預設值為 getdate() 函式, 這個函式會取得目前的時間, 所以程式新增訂單資料時, 不需要指定訂購日期欄位的值, SQL Server 會自動以新增資料時的時間作為該欄位的值。
17 - 2 產品列表頁 (Product.aspx) 本節將說明如何將產品列出顯示於網頁, 以及使用 Cookie 儲存使用者購物車內產品的方法。
產品列表頁的規劃 下面是我們將製作的產品列表頁:
產品列表頁的規劃 從上面可以總結我們需要製作以下功能: 隨後將分別介紹各功能的製作方式, 首先將說明如何將產品顯示於網頁上。 從資料庫中讀取產品資訊, 然後整齊地顯示於網頁。 依照使用者所選的類別, 顯示不同的產品。 分頁的功能。 使用 Cookie 儲存使用者放入購物車的產品。 隨後將分別介紹各功能的製作方式, 首先將說明如何將產品顯示於網頁上。
使用 DataList 控制項顯示產品列表 我們將使用 DataList 控制項顯示產品資訊, DataList 與第 11 章所介紹的 GridView 和 DetailsView 類似, DataList 控制項可以列示多筆資料, 不過其顯示資料的格式與版面彈性較大。 DataList 與 SqlDataSource 控制項繫結的方式與第 11、12 章介紹的完全相同, 所以下面僅就重點說明差異處與需要注意的地方。 請如下操作在 Web Form 建立並設定 DataList 控制項:
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表 請接著進行 SqlDataSource 控制項的設定, 完成後 DataList 控制項便可以將產品資料表的資料顯示於網頁上。 然後請如下設定 DataList 控制項:
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表
使用 DataList 控制項顯示產品列表 設定完成並存檔後, 在瀏覽器檢視執行結果
使用 DataList 控制項顯示產品列表 製作了產品顯示功能之後, 接著將說明如何加入分類與分頁的功能。
分類功能的示意圖 下面是分類顯示產品功能的運作示意圖:
分類功能的示意圖 分類的基礎類似 12-22 頁所介紹的 『GridView 搭配 DetailsView』, 所以隨後說明分類功能時僅列出重點, 詳細操作步驟請參閱該章節。
分頁功能的運作原理 DataList 控制項預設並沒有分頁的功能, 所以我們必須自行撰寫程式以製作此功能。 一般可能會想到使用資料的編號來分頁, 例如一頁要顯示 6 筆資料, 若目前在第 1 頁, 便顯示編號 1~6 的資料, 若在第 2 頁則顯示編號 7~12。
分頁功能的運作原理 但是若資料表有資料被刪除, 導致編號不連續, 這個方式便會出現問題:
分頁功能的運作原理 除了刪除資料會產生編號不連續的問題, 如果只需要顯示部分的資料時, 例如上面資料表若只取類別編號為 2 的書籍, 可想而知, 查詢出來的資料其編號一定不連續。 幸好 SQL Server 2005 提供一個 ROW_NUMBER() 的函式, 可以用來將資料表排序後重新編號, 所以我們可以使用 ROW_NUMBER() 所產生的編號來製作分頁的功能:
分頁功能的運作原理
分頁功能的運作原理 如果只需顯示部分資料, 一樣可以使用 ROW_NUMBER() 排序並重新編號:
修改 SqlDataSource 的資料來源 原本 DataList1 所繫結的 SqlDataSource_ProductList 控制項只能讀取所有產品, 所以首先要修改 SqlDataSource_ProductList 控制項的資料來源, 使其可以接收參數, 讀取某一個類別的產品, 並且還要加上 ROW_NUMBER() 函式, 以便製作分頁的功能 :
修改 SqlDataSource 的資料來源
修改 SqlDataSource 的資料來源
修改 SqlDataSource 的資料來源 在 WHERE 子句中指定『類別編號 LIKE @ CategoryID』, 之後只要在 @ CategoryID 參數設定指定的類別編號, 即可控制 SqlDataSource_ProductList 只讀取某一類別的產品。 @ CategoryID 參數的預設值為 %, 而 % 表示零或多個任意字元, 所以若未指定參數值時, 預設會顯示所有產品。 稍後程式即會使用 @ CategoryID 參數來指定分類, 然後使用列號欄位進行分頁。
製作分類功能 前面分類功能的示意圖中提到, 我們將使用 GridView 控制項顯示產品分類, 並且傳送類別編號給 SqlDataSource_ProductList。 所以請如下操作, 新增與設定 GridView 控制項:
製作分類功能
製作分類功能
製作分類功能
製作分類功能
製作分類功能
製作分類功能 上面設定當使用者按下 LinkButton1 鈕時, 會發出一個 SelectCategory 命令, 而且該命令帶有參數, 其值則是按鈕所繫結的類別編號。
製作分類功能 當 GridView1 內的控制項發出命令時, 便會產生 RowCommand 事件, 所以請如下建立 GridView1 的 RowCommand 事件處理程式, 以便針對命令進行處理:
製作分類功能 下面是 GridView1_RowCommand 程序的程式碼:
製作分類功能
製作分類功能 第 94 行括號內將 GridView 控制項的命令事件與參數指定為 e 物件。 第 97 行使用 e.CommandName 屬性檢查命令名稱, 以 e.CommandArgument 取得命令參數。
製作分類功能 如此即可完成分類的功能, 接著將製作分頁的功能。 第 101 行使用 SqlDataSource 控制項 SelectParameters 的 DefaultValue 屬性, 將 e.CommandArgument 傳來的類別編號, 指定給 SqlDataSource_ProductList 執行 SELECT 時的 @ CategoryID 參數。 如此即可完成分類的功能, 接著將製作分頁的功能。
分頁功能的重要變數 下表列出製作分頁功能的重要變數名稱與用途: 在程式碼的最前面, 會宣告並設定 NumPerPage 變數:
分頁功能的重要變數 因為 NumPerPage 變數宣告於各程序的外部, 所以會成為 Product.aspx 程式的全域變數, 各程序都可以存取其值。 如果想要改變每一頁顯示的產品數量, 只要更改此變數的值即可。
分頁功能的重要變數 總頁數 ViewState ("TotalPage") 的值則可以 DataListTotal 除以 NumPerPage 然後取最大值計算之。
自訂 GetTotal() 程序取得總產品數與總頁數 下面自訂了一個 GetTotal() 程序, 可以取得目前的總頁數, 放置於 ViewState ("TotalPage") 變數:
自訂 GetTotal() 程序取得總產品數與總頁數
製作分頁的功能 我們準備將分頁的功能放置於 Page_Load 與 Page_PreRender 這兩個事件處理程序內。 下面是運作的示意圖:
製作分頁的功能
製作分頁的功能
製作分頁的功能 上面可以看到, Page_Load 程序在第一次連線時會使用 GetTotal() 取得總產品數與總頁數, 而 Page_PreRender 則負責在網頁輸出之前, 計算並過濾目前頁數應該顯示的列號範圍。 下面是 Page_Load 程序的程式碼:
製作分頁的功能
製作分頁的功能
製作分頁的功能 我們的分頁功能主要是由 Page_PreRender 程序負責, 下面是 Page_PreRender 程序過濾並顯示列號範圍內資料的程式碼:
製作分頁的功能 上面程式碼使用 ViewState ("CurrentPage") 與 NumPerPage 計算目前頁數應顯示的範圍, 假設每頁應顯示 6 個產品, 目前位於第 3 頁, 那麼便應該顯示列號為 13~18 的產品, 所以只要將 "[列號] > 12 AND [列號] <= 18" 設定至 SqlDataSource_ProductList 的 FilterExpression 屬性, 便能讓控制項只取得指定的資料。 完成資料過濾後, Page_PreRender 程序內還需要處理下面事宜:
製作分頁的功能
製作分頁的功能 如此即可完成分頁功能最主要的部分。
上一頁、下一頁與切換類別時的分頁處理 前面 Page_PreRender 程序只負責過濾與顯示目前所在頁數的產品, 不過當使用者按上一頁、下一頁鈕時, 還需要將畫面切換到其他頁數, 這個工作便交給上一頁與下一頁按鈕的 Click 事件處理程序即可:
上一頁、下一頁與切換類別時的分頁處理
上一頁、下一頁與切換類別時的分頁處理 觸發 Page_PreRender 程序的時間是在網頁輸出前、所有控制項的事件之後, 所以按鈕的事件會在 Page_PreRender 之前產生, 故上下頁按鈕的 Click 事件處理程序只要將 ViewState ("CurrentPage") 加或減 1, 後續的顯示工作交給後面的 Page_PreRender 程序即可。
上一頁、下一頁與切換類別時的分頁處理 另外當使用者切換到其他分類時, 需要讀取該分類的產品, 並且重新進行分頁, 所以我們還需要在前面介紹過的 GridView1_RowCommand 程序內加入下面程式:
上一頁、下一頁與切換類別時的分頁處理
上一頁、下一頁與切換類別時的分頁處理
上一頁、下一頁與切換類別時的分頁處理 同樣的, GridView1_RowCommand 程序會在 Page_PreRender 程序之前觸發, 所以只需要重新取得總產品數與總頁數, 並且將目前頁數重設為 1, 顯示的工作留給 Page_PreRender 程序即可。
以 Cookie 儲存購物車內的產品 完成顯示、分類、分頁的功能後, 最後便是購物網站的重要功能:將產品放入購物車。 我們將使用 Cookie 來記錄購物車內的產品, Cookie 的名稱 Product_XX 表示編號為 XX 的產品, 至於 Cookie 的值則表示欲訂購的數量。 例如 Cookie ("Product_2") = 5, 代表欲訂購編號 2 的產品 5 個。
以 Cookie 儲存購物車內的產品 所以當使用者將編號 1 的產品放入購物車時, 程式只要建立一個 Cookie (“Product_1”), 並設定其值為 1 即可。 如果要計算目前購物車內所有產品的總數, 則如下進行統計:
以 Cookie 儲存購物車內的產品
以 Cookie 儲存購物車內的產品 第 58 行使用 For Each 迴圈取得 Request.Cookies 集合內所有 Cookie 物件。 第 61 行判斷 Cookie 的名稱是否以 "Product_" 開頭, 如果是, 則將總數加上 1。 第 67 行顯示目前購物車內的產品總數。
將產品放入購物車 規劃好 Cookie 儲存產品的方式, 最後就要製作將產品放入購物車的功能了。前面使用 DataList 控制項顯示產品時, 已經預先在每個產品下面放了一個放入購物車的按鈕:
將產品放入購物車
將產品放入購物車 之前製作分類功能時, 曾經設定按下 GridView1 內按鈕時會發出 SelectCategory 命令, 然後在 GridView1_RowCommand 處理該命令以切換類別。 DataList 控制項也可以使用相同的方式, 設定按下放入購物車鈕時發出特定命令, 然後在 DataList1_ItemCommand 程序內進行處理, 將產品放入購物車。
將產品放入購物車 所以將放入購物車按鈕的 CommandName 屬性設定為 AddtoCart, 並且將 CommandArgument 屬性繫結至產品資料表的編號欄位, 然後新增 DataList1_ItemCommand 程序, 輸入以下程式碼:
將產品放入購物車
將產品放入購物車 至此已經完全所有需要的功能, 產品頁面可以順利運作了。
17 - 3 購物車網頁 (Cart.aspx) 購物車網頁是讓使用者可以查看已選購產品、設定數量、計算總價格...等, 本節將說明其製作方式。
購物車網頁的架構 我們將製作具備下面功能的購物車網頁:
購物車網頁的架構 因為購物車網頁的功能比較單純, 所以架構也比較簡單, 下面是其包含的控制項:
購物車網頁的架構
購物車網頁的架構 上面 GridView1 的小計欄位內不放任何元件, 而數量與刪除欄位的樣版配置如下:
購物車網頁程式的流程 下面是購物車網頁的程式流程圖:
購物車網頁程式的流程
使用 Cookie 讀取購物車資訊 前面 17-21 頁已經說明本章程式儲存 Cookie 的方式, 假設使用者選了編號 3、8、11 的產品, 且數量都是 1, 則將存在以下 Cookie: Cookie ("Product_3") = 1 Cookie ("Product_8") = 1 Cookie ("Product_11") = 1
使用 Cookie 讀取購物車資訊 所以只要判斷 Cookie 的名稱是否為 “Product_” 開頭, 如果是, 則以 “_” 切割字串取得後面的數字, 便是產品的編號。 網頁的 Page_Init 程序便使用這個方式取得目前購物車內所有產品的編號, 然後以這些編號建立過濾條件, 傳送給 SqlDataSource1:
使用 Cookie 讀取購物車資訊
使用 Cookie 讀取購物車資訊
使用 Cookie 讀取購物車資訊
使用 Cookie 讀取購物車資訊 假設使用者選了編號 1、2、5 的產品, 程式第 19~36 行的迴圈取得這些編號之後, 會如下設定 FilterStr 的變數值: 當程式第 49 行將 FilterStr 變數附加至 SqlDataSource1 的 SELECT 指令後, SqlDataSource1 就會如下進行資料查詢:
顯示產品數量、小計與總數量、總價格 在 Page_Init 程序讀取 Cookie 並且將過濾條件送給 SqlDataSource1 之後, 繫結 SqlDataSource1 的 GridView1 只會顯示編號 1、2、5 這 3 項產品的資料:
顯示產品數量、小計與總數量、總價格 與 5-9 節介紹的 Table 控制項相同, GridView 控制項使用 Row 與 Cell 處理表格的列與欄, 而中間有資料的列稱為 DataRow, 最上面與最下面不含資料的列則稱為 HeaderRow 與 FooterRow。 FooterRow 預設不顯示, 筆者設定 GridView1 的 ShowFooter 屬性為 "True", 以便稍後撰寫程式將總價與總數量顯示於 FooterRow。
顯示產品數量、小計與總數量、總價格 GridView 控制項會從 SqlDataSource 逐筆取得資料, 每取得一筆資料便生成一個資料列, 生成資料列後會產生 RowDataBound 事件, 所以我們會如下在該事件的處理程序中顯示產品數量, 並且計算小計、總數量與總價格:
顯示產品數量、小計與總數量、總價格
顯示產品數量、小計與總數量、總價格
顯示產品數量、小計與總數量、總價格
顯示產品數量、小計與總數量、總價格
更新訂購明細 我們的購物車網頁上面有更新訂購明細的按鈕, 可以讓使用者修改數量或刪除產品。 對於這個功能, 一般直覺的作法可能是建立該按鈕的 Click 事件處理程序, 在該程序內讀取使用者輸入的值, 然後傳送給 GridView1 重新顯示表格內的資訊。
更新訂購明細 不過這樣的作法會讓程式變得複雜而且難以維護。 未來如果 GridView1 的欄位或是格式有所更動, 除了要修改更新按鈕的 Click 事件處理程序以外, 還要同時改前面的 GridView1_RowDataBound 程序;如果設計得不好, 兩邊可能還會發生衝突。
更新訂購明細 我們的作法是在更新按鈕的 Click 事件處理程序內, 依照使用者的輸入重新設定 Cookie 的值。 然後以 Response.Redirect 方法將使用者導向此頁網址 (等於重新整理網頁), 便可以讓 GridView1_RowDataBound 程序重新讀取新的 Cookie 設定, 顯示新的購物車資訊:
更新訂購明細
更新訂購明細
更新訂購明細
更新訂購明細 所以 UpdateCart_Click 程序只負責修改 Cookie 設定, 至於購物車的顯示則統一交給 GridView1_RowDataBound 程序, 才能讓每個程序做的工作單純化。
17 - 4 結帳頁面 (Checkout.aspx) 結帳頁面可以讓使用者輸入個人資料, 待使用者進一步確認訂單之後, 程式會將客戶資料與訂單明細寫入資料庫, 本節將說明其製作方式。
結帳頁面的架構 下面是結帳頁面的架構:
結帳頁面的架構 由上面架構可以看到, 程式最重要的部分在於 View2 如何將資料寫入資料庫, 至於其他 View 只要使用基本控制項與程式語法即可完成, 以下說明也會著重在 View2 控制項。
讓使用者輸入個人資料 View1 是讓使用者輸入個人資料, 其中包含的控制項如下:
讓使用者輸入個人資料 View1 主要由許多 TextBox 與相關的驗證控制項所組成, 當使用者輸入完資料, 按下一步鈕並通過驗證之後, 程式便會切換到 View2:
讓使用者輸入個人資料
確認訂購明細與個人資料 下面是 View2 所 包含的控制項:
確認訂購明細與個人資料 此處使用 GridView 控制項來顯示訂單明細, 其顯示原理與 17-3 節的購物車頁面相同, 故此處不再贅述。 中間許多 Label 控制項則會從 View1 的 TextBox 控制項取得資料, 讓使用者確認個人資料是否有誤。
確認訂購明細與個人資料 若使用者確認無誤, 按下送出訂單鈕時會觸發 CheckOut_Click 程序, 我們會在此程序透過最下方兩個 SqlDataSource 控制項:SqlDataSource_Orders、SqlDataSource_OrderDetail, 將資料分別寫入訂單與訂單明細資料表。
將訂單資料寫入資料庫 前面 17-4 頁已經看到資料庫的結構, 基於正規化的設計我們將訂單資料分別放置於訂單與訂單明細兩個資料表。 因為訂單明細資料表中的訂單編號欄位關聯到訂單資料表的編號, 所以程式寫作時會遇到的主要問題, 在於將主要資料寫入訂單資料表之後, 如何取得該筆訂單的編號, 以便將其餘明細寫入訂單明細資料表。
將訂單資料寫入資料庫 為了解決這個問題, 我們在 SQL Server 資料庫建立了一個名為 InsertOrder 的 T-SQL 預存程序。 程式會透過參數將資料傳遞給 InsertOrder, 當 InsertOrder 將其寫入訂單資料表之後, 會將自動編號欄位的最新編號透過參數回傳程式, 於是我們便可以使用這個編號繼續寫入訂單明細資料表了:
將訂單資料寫入資料庫 請如下在資料庫總管窗格建立 InsertOrder 預存程序:
將訂單資料寫入資料庫
將訂單資料寫入資料庫 建立好 InsertOrder 預存程序後, 接著設定 SqlDataSource_Orders 使用該預存程序寫入資料:
將訂單資料寫入資料庫
將訂單資料寫入資料庫 所以程式必須先將資料設定給 SqlDataSource_Orders 的參數 (除了 OrderID 以外), 才執行 Insert 命令, 而寫入資料之後即可由 OrderID 參數值, 取得預存程序傳回的訂單編號。 有了訂單編號後, 程式會使用 SqlDataSource_OrderDetail 寫入訂單明細資料表, 請如下設定 SqlDataSource_OrderDetail:
將訂單資料寫入資料庫
將訂單寫入資料庫的程式 完成整個寫入資料庫的規劃與設定後, 便可以在結帳鈕按下時觸發的 CheckOut_Click 程序, 輸入下面程式:
將訂單寫入資料庫的程式
將訂單寫入資料庫的程式
將訂單寫入資料庫的程式
將訂單寫入資料庫的程式
將訂單寫入資料庫的程式
將訂單寫入資料庫的程式 第 39 行使用 SqlDataSource_Orders 的 Insert() 方法寫入訂單資料表之後, ASP.NET 會產生 Inserted 事件, 而觸發第 82 行的 SqlDataSource_Orders_Inserted 程序, 所以程式在此程序會先取得訂單編號, 等該程序執行完成後才會繼續執行第 42~78 行的程式。
將訂單寫入資料庫的程式 第 72 行到第 75 行將藉由刪除 Cookie 的方式清空購物車, 請小心, 如果要刪除 Cookie, 請勿使用類似第 50 行讀取 Cookie 集合的迴圈, 否則刪除 Cookie 時, ASP.NET 會因為 Cookie 集合已經改變了, 而產生執行時期錯誤, 導致程式中止執行。