asp.net|購物車
在本篇中,我們將經由一個簡單的網上商店示範程式來探討GridView,並開始分析一種產生GridView的DataSource的方法,然後繼續使用該資料來建立一個完全功能的購物介面。注意,在這個示範程式中的DataSource是可以自由建立的。
一、 簡介
在第一篇中,我們討論了什麼是GridView以及如何使用它,包括實際資料如何綁定到其上。在本文中,我們將更密切地分析這些資料的來源以及如何與GridView一起使用它來實現一個簡單的購物介面。
二、 資料來自於何處?
從根本上講,這個問題要依賴於你要幹什麼。它可以來自於一個靜態XML檔案,一個動態XML饋送,一個資料庫,或許它是自由建立的。但是,無論如何,應該確保滿足:如果存在資料,你能夠確保它能夠"匯入"到一個GridView中。在本文中,在每次重啟動應用程式時,這部分資料都是自由建立的。
用於填充兩個GridView的DataSource是一個DataTable。它是使用DataColumns和DataRows構建的。在這個主類檔案記憶體在一個稱為"createProductDT"的函數,它說明了DataTable的初始建立方式。下面是該函數的完整實現:
private DataTable createProductDT() { DataTable dtProducts = new DataTable(); DataColumn productColumn = new DataColumn(); productColumn.DataType = System.Type.GetType("System.Int32"); productColumn.ColumnName = "id"; productColumn.Unique = true; dtProducts.Columns.Add(productColumn); productColumn = new DataColumn(); productColumn.DataType = System.Type.GetType("System.String"); productColumn.ColumnName = "thumb"; dtProducts.Columns.Add(productColumn); productColumn = new DataColumn(); productColumn.DataType = System.Type.GetType("System.String"); productColumn.ColumnName = "name"; dtProducts.Columns.Add(productColumn); productColumn = new DataColumn(); productColumn.DataType = System.Type.GetType("System.Double"); productColumn.ColumnName = "price"; dtProducts.Columns.Add(productColumn); productColumn = new DataColumn(); productColumn.DataType = System.Type.GetType("System.Int32"); productColumn.ColumnName = "quantity"; dtProducts.Columns.Add(productColumn);
//使"id"成為主鍵 DataColumn[] pkColumns = new DataColumn[1]; pkColumns[0] = dtProducts.Columns["id"]; dtProducts.PrimaryKey = pkColumns; return dtProducts; } |
首先,我們建立了一個DataTable對象,然後建立一個DataColumn。對於大多數表格列來說,我們僅需要設定資料類型和列名,儘管對於第一列("id")來說,我們還要把它設定為唯一的。這是因為我們要把它作為我們的主鍵;另外,在函數最後處還要求對之進行配置。之所以我們要使id總是唯一的,是因為我們要使用它來引用我們將在後面添加到DataSource上的各種產品;這樣以來,我們能夠從中選擇特定的資料,例如只使用產品的價格與產品名。這個函數將返回一個空的DataTable,並因此僅被使用於getBasket()和populateProducts()中。
現在,我們開始把實際的行資料添加到populateProducts()內的DataSource,詳見下面的代碼。每行對應一個不同的產品。添加一個新行到一個DataTable要求你建立一個新的DataRow,然後調用該DataTable的NewRow()函數。這將在DataTable內為新行留出位置,但是它不會實際地添加該行。
private void populateProducts() { //建立基本結構 DataTable dtProducts = createProductDT(); //把產品添加到其上 //建立初始的行 DataRow aProduct = dtProducts.NewRow(); aProduct["id"] = 11; aProduct["thumb"] = "images/widget0.jpg"; aProduct["name"] = "Red Widget"; aProduct["price"] = 19.99; dtProducts.Rows.Add(aProduct); //重用該行以添加新產品 aProduct = dtProducts.NewRow(); aProduct["id"] = 22; aProduct["thumb"] = "images/widget1.jpg"; aProduct["name"] = "Green Widget"; aProduct["price"] = 50.99; dtProducts.Rows.Add(aProduct); //把DataTable綁定到產品GridView gvProducts.DataSource = dtProducts; gvProducts.DataBind();
//把產品儲存到Session Session["dtProducts"] = dtProducts; } |
首先,我們需要把一些資料添加到該行(例如id,縮圖像的路徑,名稱和價格)。一旦添加上這些內容,我們即可以調用Add()函數來實際地把我們的新行添加到DataTable。在該示範程式中,我們添加了六種產品,儘管在上面的片斷中我們僅添加了兩種。你能夠看出我是如何?"欺騙"的並且僅重用了相同的列和相同的行。一旦實現這一點,我們即把DataTable綁定到我們的GridView。詳見下面的代碼:
gvProducts.DataSource = dtProducts; gvProducts.DataBind(); |
還記得我們在第一篇中所討論的RowDataBound事件嗎?好,一旦我們調用了DataBind(),該函數被啟用,即開始在頁面上建立我們的資料。你應該清楚,在底層實現上,在這兩個事件之間可能還會有其它事件發生;但是,為了更易於理解起見,我們僅考慮這兩個事件。
此後,我們還在工作階段狀態中儲存該DataTable的一個副本;這樣以來,在以後我們每次想存取產品資料時,我們可以直接檢索它而不必重新建立它。值得注意的是,儘管這種情況比較適合於針對一個小規模工程的一少部分資料;但是,當針對大型的應用程式時,你不應該象本例中這樣使用工作階段狀態-它會很容易地"吞掉"你的伺服器記憶體;因此,即使使用一部分資料也有可能使用大量的記憶體,如果存在上千的使用者同時訪問它的話。在這個示範程式中,你將看到資料被從會話中多次提取;但是,在實際中,你可能實現眾多資料庫調用以便提取特定的資料子集(當需要它時)。
值得注意的一個事情是,你能夠設定"DataKeyNames",這些內容能夠用來索引GridView中的項。產品列表和購物籃都分別實現單個DataKeyName:
這樣,當後來點擊"Add to basket"按鈕以標識我們想添加的產品時使用它。在購物籃中,當更新數量時也使用它。你可以有多個鍵名,儘管大多數情況下你僅需要一個。
在填充GridView前,你能夠把一個空DataTable綁定到它上面。這將迫使它顯示一個空行(你可以使用一個字串來預填充)。在該示範中,這是使用兩個GridView實現的,儘管你僅能看到其中的一個對應於購物籃,因為即使在你的商店中不存在產品也並不重要。你可以象下面這樣使用"EmptyDataText"GridView屬性來設定它:
EmptyDataText="~Basket is empty~" |
然後,它會象下圖1這樣被產生:
三、購物籃
購物籃(參考圖2)用於儲存產品(顧客通過點擊緊鄰每一種產品的"Add to basket"按鈕從產品列表中選擇)。把購物籃儲存在工作階段狀態中的實現是不錯的技術,因為在一次完整的購物中,在任何時候顧客決定離開你的網站,或可能倒空他們的購物籃時,所有這些資料都有可能被丟棄。當然,由於若干原因,例如為了市場調查目的以標識誰在分析什麼以及判斷購物潮流等時,你還可以選擇把顧客的購物籃內容儲存在一個資料庫中。另一個理由可能是,向他們展示"Last time you were here you looked at these items ..."類型顯示。這要求你有一個方法來區分顧客。兩種通常使用的技術是,把一個cookie儲存在使用者自己的系統中-通過使用一個唯一的ID來標識他們的未來訪問,或使用他們的登入ID來加以區別(如果你已經實現顧客登入的話)。
更新的購物籃還使用createProductDT()函數來建立它的初始的空DataTable。在本示範程式中,我們將使用相同的表格結構,但是你可以通過刪除一些資料列來進一步"提煉"你的購物籃。在大多數情況下,你僅需要儲存每種產品的ID和數量,由於你能夠容易地基於它的ID尋找實際的產品細節。
每次經由產品列表把一個產品添加到籃中時,它的"Add to basket"按鈕都會啟用一個OnServerClick事件:
protected void shopBuy_OnServerClick(object source, EventArgs e) { int index = ((GridViewRow)((HtmlInputButton)source).Parent.NamingContainer).RowIndex; addToBasket(Convert.ToInt32(gvProducts.DataKeys[index].Value)); } protected void addToBasket(int productID) { DataTable dtBasket = getBasketDt(); //迴圈遍曆購物籃並檢查是否該項已經存在 bool found = false; for(int i = 0; i < dtBasket.Rows.Count; i++) { if(Convert.ToInt32(dtBasket.Rows[i]["id"]) == productID) { //增加數量並且標記為已發現 dtBasket.Rows[i]["quantity"] = Convert.ToInt32(dtBasket.Rows[i]["quantity"]) + 1; found = true; //當我們已經找到一項時跳出迴圈 break; } } //如果該項沒有找到,則把它添加為一個新行 if(!found) { DataTable dtProducts = getProductsDt(); DataRow drProduct = dtProducts.Rows.Find (productID); //現在,我們已經從資料來源中得到了需要的資料,那麼我們將把一個新行添加到購物籃中 DataRow newRow = dtBasket.NewRow(); newRow["id"] = drProduct["id"]; newRow["name"] = drProduct["name"]; newRow["price"] = drProduct["price"]; newRow["quantity"] = 1; dtBasket.Rows.Add(newRow); } //把新更新的購物籃儲存回會話中 Session["dtBasket"] = dtBasket; //更新購物籃,也即是"重新綁定它" updateShopBasket(); } |
我們是使用shopBuy_OnServerClick()函數來"捕獲"這一點的(這個函數能標識按鈕屬於哪一行),得到相關產品的ID並用它來調用addToBasket()。在該函數內,我們可以使用給定的產品ID來檢查購物籃。如果它已經存在於購物籃中,那麼我們需增加它的數量;而如果它不存在,那麼我們把它添加為一個新行。最後,我們把購物籃重新綁定到它的更新的DataSource上。參考圖3。
該購物籃,就象產品GridView一樣,也使用TemplateColumns;因此,我們可以在每一行上建立一個數量文字框。這為顧客提供一種容易的方式來更新他們要求的每一種商品的數目。一旦他們改變了這些值,他們點擊在購物籃下面的"Update Quantities"按鈕。這將啟用一個為shopUpdateBasketQuantities_OnServerClick()所捕獲的OnServerClick事件。這類似於addToBasket()函數:我們必須定位購物籃中的產品,然後更新它的數量。區別在於:當檢查從文字框中檢索的資料時,我們必須小心,因為你根本不會知道什麼人能夠進入到其中致使弄亂你的系統。下面是處理這一檢查的函數的部分代碼片斷:
//從Quantity文字框中讀取資料 HtmlInputText itQuant = (HtmlInputText)row.FindControl("itProductQuantity"); //把該值轉換成一個整數 try { int quant = Convert.ToInt32(itQuant.Value);
/*如果該值成功轉換成一個整數,那麼我們還 需要檢查它不是一個負數;否則的話,我們可能欠 顧客錢!*/ if(quant > 0) { drProduct["quantity"] = quant; } else { drProduct.Delete(); } } catch { //如果我們不能把它轉換成整數,那麼我們不作什麼改變。 } |
例如,如果有人在quantity域中輸入-100,你可能還會欠他們的錢!不過,一般地,你可能不會把錢支付給他們,但是這要依賴於你的支付系統是如何建立的。由於這個原因,我們把這個整數分析封裝到一個try/catch語句塊內,以便在不能分析的情況下,我們保留原來的值不變。此後,我們檢查這個quantity以確保它大於零。如果它小於或等於零,那麼我們刪除這一行。最後,在檢查完購物籃中所有的產品並且修改它們各自相應的數量後,我們即儲存購物籃並更新顯示。
購物籃的最後一個關鍵組成是updateShopBasket()函數:
private void updateShopBasket() { gvBasket.DataSource = getBasketDt(); gvBasket.DataBind(); ibEmptyBasket.Visible = ibUpdateBasketQuantities.Visible = ibBasketCheckout.Visible = gvBasket.Rows.Count > 0; } |
這個函數能夠從工作階段狀態中提取購物籃的一個副本,這反過來將建立會話購物籃,如果它已經不存在的話,然後綁定到GridView。其最終目的是隱藏或顯示三個購物籃按鈕;因為如果購物籃為空白的話,不需要顯示它們。 四、一個值得注意的安全問題
在你的系統的使用者有機會輸入資料的任何地方都應該嚴格控制以確保他們沒有輸入任何不想實現的內容。一個普通問題就是SQL注入。在這種位置,有些人可以把SQL代碼輸入到一個網站的某個部分,然後你可以在你想使用的原始SQL語句內使用它。所以,比方說相應於quantity域,你可以使用:
"UPDATE tbl_basket SET quantity = " + quantity.Text + " WHERE user_id = " + user_id; |
如果顧客在"quantity"文字框內輸入6並且他們的登入id是230,那麼上面的代碼將看起來象:
UPDATE tbl_basket SET quantity = 6 WHERE user_id = 230; |
而如果顧客輸入:
" 1 WHERE 1 = 1; DROP tbl_users; --" |
那麼,原始語句現在看起來象:
UPDATE tbl_basket SET quantity = 1 WHERE 1 = 1; DROP tbl_users; -- WHERE user_id =; |
這樣以來,他們可以使用"1 WHERE 1 = 1;"來完成原始語句,然後繼續"Drop tbl_ users;"操作,這很不妙!最後,他們可以注釋掉原始語句的其它部分。其實,這僅是一個極其簡單的樣本。有關於SQL注入的問題,你可以在網站上搜到許多資訊。
五、 支付
存在許多種使用電子業務方式接收支付的方法。下面列出幾種:
· 線上商店實際上並不僅僅是一個線上目錄,顧客往往還必須能夠電話聯絡到你以便進行訂購。
· 類似上面這種情形,除非你親自找到顧客來完成整個交易。如果這是有關一些建築方面的工作(例如一個院子或一個廚房),並且在實地考察之後你需要當場向他們提出一個報價,那麼這可能很重要。
· 使用一種內建安全的支付方法。通過這種方法,顧客能夠輸入他們的信用卡細節並且可以由系統自動處理交易。
· 使用例如PayPal、Worldpay或DebiTech等一種外部支付方法。
本文中的示範商店基於一種舊式風格的使用PayPal接收支付的方法。它應該與其它外部支付系統(例如稍經修改的WorldPay)結合在一起工作。我們之所以說是"舊式風格"是因為,現在的PayPal一般都提供其自己的.net工具包-實現它們自己的串連到它們的網站的系統。
整個收集購物籃資料並把它轉移到PayPal的系統都是在shopBasketCheckout_OnServerClick()函數內實現的:
protected void shopBasketCheckout_OnServerClick(object source,EventArgs e) { string postData = ""; postData += "currency_code=GBP"; postData += "&cmd=_cart"; postData += "&business=youremailaddress@yourdomain.net"; postData += "&upload=1"; postData += "&cancel_return=www.davidmillington.net"; DataTable dtBasket = getBasketDt(); double total = 0.00; for(int i = 0; i < dtBasket.Rows.Count; i++) { postData += "&item_name_" + (i + 1) + "=" + dtBasket.Rows[i]["name"]; postData += "&quantity_" + (i + 1) + "=" + dtBasket.Rows[i]["quantity"]; postData += "&amount_" + (i + 1) + "=" + Convert.ToDouble(dtBasket.Rows[i]["price"]); total += (Convert.ToDouble(dtBasket.Rows[i] ["price"]) * Convert.ToInt32(dtBasket.Rows[i]["quantity"])); if(i == dtBasket.Rows.Count - 1) { postData += "&shipping_" + (i + 1) + "=" + calcDeliveryCost(total); } else { postData += "&shipping_" + (i + 1) + "=0.00"; } postData += "&shipping2_" + (i + 1) + "=0.00"; postData += "&handling_" + (i + 1) + "=0.00"; } postData += "&handling=" + calcDeliveryCost(total); byte[] data = Encoding.ASCII.GetBytes(postData); HttpWebRequest ppRequest = (HttpWebRequest) WebRequest.Create("https://www.paypal.com/cgi-bin/webscr");; ppRequest.Method = "POST"; ppRequest.ContentType = "application/x-www-form- urlencoded"; ppRequest.ContentLength = data.Length; //發送 Stream ppStream = ppRequest.GetRequestStream(); ppStream.Write(data, 0, data.Length); ppStream.Close(); //接收 HttpWebResponse ppResponse = (HttpWebResponse)ppRequest.GetResponse(); StreamReader sr = new StreamReader(ppResponse.GetResponseStream()); string strResult = sr.ReadToEnd(); sr.Close(); //輸出到螢幕 Response.Clear(); Response.Write(strResult); Response.End(); } |
因為看起來沒有一種辦法使一個C#應用程式實現寄送並重新導向到另一個網站(就象你通常使用一個<form > action屬性所實現的那樣),所以我們必須採用一種稍微不同的方法。我們構建了一個long型字串,它包含多個名/值對,然後使用HttpWebRequest和HttpWebResponse對象來從支付服務中來回傳送與接收資料。
該函數的第一部分指定PayPal帳戶細節,例如使用的貨幣,帳戶名稱以及PayPal應該把顧客返回的頁面(如果他們決定取消該交易的話)。
下一步是遍曆購物籃並且檢索所有我們想傳遞到PayPal的產品資訊。這包括產品名稱、數量和價格。由於該示範程式的特點,我們在運送費用方面稍微施加了一點技巧並且把整個運送費用添加到購物籃中最後一件產品上而不是添加到每一件產品。這是因為我們僅基於購物籃的總價求出總的運送費用,而不是基於任何產品種類。
現在,我們來討論有趣的部分。我承認,這是我通過Google引擎查詢的結果。首先我們建立一個Request對象,當我們經由一個Stream聯絡到PayPal時使用它。我們使用一個Response對象來接收該響應並簡單地把它通過Response.Write()輸出到螢幕。這可以把整個購物籃資訊輸送到PayPal網站並把它導向正確的帳戶。
現在的問題是,顧客到達的第一個頁面在相應的地址欄內仍然有你的商店地址。如果他們點擊該PayPal網站的任何連結,例如觀看購物籃內容或進行登入的話,那麼該地址應該相應地改變以反映它確實是PayPal。你可能意識到,有些人可能會被誤解,因為這樣的事實-他們仍然能夠在地址欄中看到你的商店的地址並且甚至可能認為你在試圖騙取他們的PayPal或銀行帳戶細節。如果你正在計劃經由一個外部系統例如PayPal或WorldPay來實現支付,那麼你應該檢查它們的開發人員網站來看一下他們推薦的.net方案是什麼。
六、結論
在本節中,我們首先分析一種產生GridView的DataSource的方法,然後繼續使用該資料來建立一個全功能的購物介面。儘管該示範程式中的DataSource可以自由建立;但是,如果你或者有大量的產品或僅擁有一個經常改變的產品線的話,你確實應該需要考慮使用一個資料庫來儲存你的產品資訊。當然,把一個資料庫添加到系統中等於開啟了它自己的病毒庫;因此,這是一種不應該輕易採取的措施。
需要特別注意的地方是支付系統。該示範商店使用一個很簡單的方法來收集要求的購物籃資訊並把它發送到一個外部支付系統。你可能想使用更多的控制項來實現支付處理,例如提取顧客的支付細節並把它們儲存到一個資料庫,或編寫你自己的電子銷售點功能。不管你選擇什麼方法,你應該清醒地認識到在你的國家實現接收和支付的合法性。
在本系列下一篇中,我們將討論更改GridView外觀的一些方法。
|