top
Loading...
ASP.NET2.0打造購物車和支付系統之二
在本篇中,我們將經由一個簡單的網上商店演示程序來探討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:

DataKeyNames="id"

這樣,當后來點擊"Add to basket"按鈕以標識我們想添加的產品時使用它。在購物籃中,當更新數量時也使用它。你可以有多個鍵名,盡管大多數情況下你僅需要一個。

在填充GridView前,你能夠把一個空DataTable綁定到它上面。這將迫使它顯示一個空行(你可以使用一個字符串來預填充)。在該演示中,這是使用兩個GridView實現的,盡管你僅能看到其中的一個對應于購物籃,因為即使在你的商店中不存在產品也并不重要。你可以象下面這樣使用"EmptyDataText"GridView屬性來設置它:

EmptyDataText="'Basket is empty'"

然后,它會象下圖1這樣被生成:


圖1.購物籃為空時的提示。

三、購物籃


圖2.示例程序中實現的購物籃。

購物籃(參考圖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。


圖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 += "&[email protected]";
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外觀的一些方法。
作者:http://www.zhujiangroad.com
來源:http://www.zhujiangroad.com
北斗有巢氏 有巢氏北斗