HomeC# Data Handling with a Shopping Cart/PayPal ...
Data Handling with a Shopping Cart/PayPal System
In this second of a three-part series exploring the GridView via a simple shop demo, we start by looking at one method of generating the GridView's DataSource and then proceed to use that data to create a fully functional shopping interface. The DataSource in this demonstration will be created on the fly.
Contributed by Tann San Rating: / 13 June 20, 2006
A downloadable zip with the source and images is available here.
Introduction
I've used the DataGrid in almost every commercial C# .net project to date, so when I started hearing about the GridView and how it would take out a chunk of the work I kept doing I leaped at the chance to use it. Both the DataGrid and the GridView are classes that allow you to easily and quickly display tabular data. They both get rendered to HTML tables when viewed online.
This is the second article in a series exploring the GridView and demonstrating some of its usages via a simple shop demo. Each article is going to use the same source files which you can grab at the link shown above. To view the demo just unzip everything into a new directory on your web server and browse to the directory name. For example, if you unzipped it all into a directory called "gridviewshop" in the root of your web server you would navigate to:
http://www.yourserver.com/gridviewshop
If everything worked okay you should see a site that looks like this:
In the previous segment we covered what a GridView is and how it can be used, including how the actual data is bound to it. In this article we're going to look more closely at where that data comes from and how it can be used along with the GridView to implement a simple shop interface.
Where does the Data come from?
This really depends on what you're doing. It could come from a static XML file, a dynamic XML feed, a database, or maybe it's just created on the fly. It pretty much comes down to this: if there is data, you can be sure it can be pumped into a GridView. For this article the data is created on the fly each time the application is restarted.
The DataSource used to populate both GridViews is a DataTable. This is constructed using DataColumns and DataRows. Inside the main class file there is a function called "createProductDT." This shows how the DataTable is initially created. Here's the function in its entirety:
private DataTable createProductDT() { DataTable dtProducts = new DataTable(); DataColumn productColumn = new DataColumn();
// Make "id" the primary key DataColumn[] pkColumns = new DataColumn[1]; pkColumns[0] = dtProducts.Columns["id"]; dtProducts.PrimaryKey = pkColumns;
return dtProducts; }
First we create the DataTable object and then a DataColumn. For most columns we just set the DataType and the column name, although for the first column, "id," we also set it to be Unique. That's because we want this to be our primary key, which also requires a little addition near the end of the function to configure this. The reason we want the id to always be unique is that we'll be using this to reference the various products we will add to the DataSource later on, so we can pull specific data out, such as just that product's price and just that product's name. This function will return an empty DataTable and is only used in a couple of places, namely getBasket() and populateProducts().
We start adding actual rows to the DataSource within populateProducts(). This is shown below. Each row is a different product. Adding a new row to a DataTable requires you to create a new DataRow and then call the DataTable's NewRow() function. This will make room in the DataTable for the new row, but it will not actually add the row.
// Re-use the row to add a new product aProduct = dtProducts.NewRow(); aProduct["id"] = 22; aProduct["thumb"] = "images/widget1.jpg"; aProduct["name"] = "Green Widget"; aProduct["price"] = 50.99; dtProducts.Rows.Add(aProduct);
// Bind the DataTable to the Products GridView gvProducts.DataSource = dtProducts; gvProducts.DataBind();
// Store the products in Session Session["dtProducts"] = dtProducts; }
First we need to add some data to the row such as the id, thumbnail image path, name and price. Once those have been added we call the Add() function to actually add our new row to the DataTable. In the demo we add six products, although in the snippet above we just add two. You can see where I cheated and just reused the same column and the same row. Once this is done we bind the DataTable to our GridView with:
Remember the RowDataBound event we talked about earlier in the first article? Well once we call DataBind(), that function gets fired off and our data starts getting created on the page. You should be aware that under the hood there might be other things happening between these two events, but for the sake of clarity it's easier to just think of it this way.
After that we also store a copy of the DataTable in Session state so we can retrieve it later on instead of having to recreate it each time we want access to the product data. It should be noted that, while this is fine for a small amount of data on a small scale project, you shouldn't really do this for large scale applications as using the Session state eats away at your server's memory, so even using just a small amount of data can use tremendous amounts of memory if there are several thousand users accessing it simultaneously. You'll see the data being pulled out of the Session several times in this demonstration, but in reality you'd probably be performing numerous database calls to pull a more specific subset of data as and when it is needed.
One thing to note is that you can set "DataKeyNames." These can be used to index items in the GridView. The product listing and the basket each implement a single DataKeyName:
DataKeyNames="id"
This is then used later on when the "Add to basket" button is clicked to identify which product we want to add. In the basket it's used when the quantities are getting updated. You can have multiple key names, although a lot of the time you will only ever need one.
Before populating the GridView you can bind an empty DataTable to it. This will force it to show an empty row that you can pre-populate with a string. This is done with both GridViews in the demo, although the only one you'll ever really see is the baskets since it wouldn't make much sense to have no products in your shop. You set it using the "EmptyDataText" GridView property like this:
The shopping basket is used to store products that the customer selects from the product listing by clicking the "Add to basket" button next to each product. It's fine to store the basket in Session state as in a full shop this data may be discarded at any moment by the customer deciding to leave your site, or maybe just from them emptying their basket. You could choose to store their basket choices in a database for several reasons, such as for market research purposes to identify who's looking at what, and to identify trends in buying habits. Another reason may be to present them with "Last time you were here you looked at these items ..." type displays. That would require you to have a method to distinguish between customers. Two commonly used techniques involve storing a cookie on their system with a unique ID to identify them on future visits, or basing it off their login ID if you implement customer logins later.
The basket also uses the createProductDT() function to create its initial empty DataTable. For this demonstration we're going to use the same table structure, but you may wish to refine your basket by removing some of the columns. In most situations you will only need to store the ID and quantity of each product, since you could easily look up the actual product details based on its ID.
Each time a product is added to the basket via the product listing, its "Add to basket" button fires an OnServerClick event:
protected void shopBuy_OnServerClick(object source, EventArgs e) { int index = ((GridViewRow)((HtmlInputButton) source).Parent.NamingContainer).RowIndex; addToBasket(Convert.ToInt32(gvProducts.DataKeys [index].Value)); }
// Loop through the basket and check if this item already exists bool found = false; for(int i = 0; i < dtBasket.Rows.Count; i++) { if(Convert.ToInt32(dtBasket.Rows[i]["id"]) == productID) { // increment the quantity and mark as found dtBasket.Rows[i]["quantity"] = Convert.ToInt32(dtBasket.Rows[i]["quantity"]) + 1; found = true;
// break out of the for loop as we have already found the item at this point break; } }
// If the item is not found then add it as a new row if(!found) { DataTable dtProducts = getProductsDt (); DataRow drProduct = dtProducts.Rows.Find (productID);
// Now we've got the data we need from our datasource we can add a new row to the basket DataRow newRow = dtBasket.NewRow(); newRow["id"] = drProduct["id"]; newRow["name"] = drProduct["name"]; newRow["price"] = drProduct["price"]; newRow["quantity"] = 1;
dtBasket.Rows.Add(newRow); }
// Store the newly updated basket back in the Session Session["dtBasket"] = dtBasket;
// Update the basket i.e. re-bind it updateShopBasket(); }
We catch this using the shopBuy_OnServerClick() function, which identities which row the button belongs to, gets the associated product ID and then uses that to call addToBasket(). Inside that we check the basket for the product ID. If it already exists in the basket, we just increment its quantity, and if it doesn't, we add it as a new row. Finally we re-bind the basket to its updated DataSource.
The basket, like the product GridView, uses TemplateColumns, so we can have a quantity text box in each row. This gives the customer an easy way to update the number of each item that they desire. Once they've changed the value, they click the "Update Quantities" button below the basket. This fires an OnServerClick event which gets caught by shopUpdateBasketQuantities _OnServerClick(). This is similar to the addToBasket() function in that we have to locate the product in the basket and then update its quantity. It differs in that we have to be careful when checking the data that has been retrieved from the text box, as you never know what someone might stick in there to confuse the system. Here's a snippet of the function that handles this checking:
// Read the data from the Quantity Text Box HtmlInputText itQuant = (HtmlInputText)row.FindControl("itProductQuantity");
// Try and convert the value to an int try
{
int quant = Convert.ToInt32(itQuant.Value);
/* * If the value converted to an Int successfully we still need to check * that its not a minus number, otherwise we might end up owing the customer money! */ if(quant > 0) { drProduct["quantity"] = quant; } else { drProduct.Delete(); } } catch { // If we cant convert it to an Int then just leave it as it was i.e. make no changes }
Say for example someone sticks -100 in as the quantity, you may end up owing them money! Okay so you probably wouldn't have to actually pay them, but depending on how your payment system is set up, it may pose a problem. For this reason we wrap the integer parsing in a try/catch clause, so that if it can't be parsed we just leave the value as it originally was. After that we check the quantity to make sure that it is greater than zero. If it's less than or equal to zero then we remove the row. Finally, after all the products in the basket have been checked and had their quantities amended, we store the basket and update the display.
The final key component of the basket is the updateShopBasket() function:
This grabs a copy of the basket out of the Session state, which in turn will create the Session basket if it doesn't already exist, and then binds the GridView. Its final purpose is to hide or show the three basket buttons, as there is no need to show them if the basket is empty.
Any point within your system where a user has the opportunity to enter data should be strictly checked to ensure that they have not entered anything that could be used for an unintended purpose. A common problem is SQL injection. This is where somebody enters SQL code into an area of a site which then gets used inside the original SQL statement you intended to use. So say for the quantity field you had:
"UPDATE tbl_basket SET quantity = " + quantity.Text + " WHERE user_id = " + user_id;
If the customer entered 6 into the "quantity" text box and their login id was 230 then the above would look like:
UPDATE tbl_basket SET quantity = 6 WHERE user_id = 230;
If instead the customer entered:
" 1 WHERE 1 = 1; DROP tbl_users; --"
then the original statement would now look like:
UPDATE tbl_basket SET quantity = 1 WHERE 1 = 1; DROP tbl_users; -- WHERE user_id =;
so they complete the original statement with "1 WHERE 1 = 1;" and then proceeded to "Drop tbl_users;" which wouldn't be good. Finally they commented out the remainder of the original statement. This was just an extremely simple example. Use your favorite search engine and look up "SQL Injection" and "String Sanitization" to learn more about methods to defend your sites.
There are many methods for receiving payment with an ecommerce solution. Here are a few:
The online shop is actually more of an online catalog, and customers have to telephone you to place orders.
Similar to the above, except you travel to the customer to complete the transaction. This might make sense if it's for some building work such as a patio or a kitchen, and you need to give them a quote on the spot after viewing the location.
A built-in secure payment section in which the customers can enter their card details, and the transaction is automatically processed by the system.
An external payment method such as PayPal, Worldpay or DebiTech.
The demo shop uses one of the old style methods of receiving payments with PayPal. It should work with the other external payment systems such as WorldPay with some minor modifications. The reason it's termed "old-style" is because PayPal now provides their own .net kit that implements their own system of connecting to their site here.
The entire system of collecting the basket data and transferring it to PayPal is handled inside the shopBasketCheckout_OnServerClick() function:
// Receive :¬( HttpWebResponse ppResponse = (HttpWebResponse) ppRequest.GetResponse(); StreamReader sr = new StreamReader (ppResponse.GetResponseStream()); string strResult = sr.ReadToEnd(); sr.Close();
// Write to screen Response.Clear(); Response.Write(strResult); Response.End(); }
Because there doesn't seem to be a way to make a C# application post and redirect to another site as you would normally do with a <forms> action attribute, we have to take a slightly different approach. We build a long string which contains multiple name/value pairs and then use the HttpWebRequest and HttpWebResponse objects to send and receive the data to and from the payment service.
The first section in the function specifies the PayPal account details such as the currency to use, the account name and the page PayPal should return the customer to should they decide to cancel the transaction.
The next step is to loop through the basket and retrieve all the product information that we want to pass to PayPal. This includes the product name, quantity and price. Due to the nature of the demonstration, we cheat a little bit with the delivery costs and add the entire delivery cost to the last product in the basket instead of adding each individually. This is because we only work out the delivery based on the total price of the basket and not on any sort of per product basis.
Now comes the interesting part. First though, let me point out that I have not invented this method myself; it's the result of some hard Googling over the problem. First we create a Request object which will be used when we contact PayPal via a Stream. We receive the response with a Response object and simply Response.Write() that to the screen. This gets the basket information to the PayPal site and directs it to the correct account.
The problem is that the first page the customer lands on will still have your shop's address in the address bar. If they click any of the links on the PayPal site, such as to view the basket contents or to login, then the address will change to reflect that it's really PayPal. As you may be aware, some people may be put off by the fact that they can still see your shop's address in the address bar and may even believe you're going to try scamming their PayPal or bank account details off them. If you are planning on implementing payments via an external system such as PayPal or WorldPay, you should check their developer sites to see what their recommended .net solution is.
Conclusion
In this installment we started by looking at one method of generating the GridView's DataSource and then proceeded to use that data to create a fully functional shopping interface. Although the DataSource in the demonstration was created on the fly you should really think about using a database for storing your product information if you either have a large number of products or just carry a frequently changing product line. Of course adding a database into the equation opens its own barrel of worms, so it's not a step to be taken lightly.
The other area that needs special attention is the payment system. The demo shop uses a very simple method to collect the required basket information and send it to an external payment system. You may want more control over payment handling, such as taking the customer's payment details and storing them in a database, or writing your own Electronic Point Of Sale functionality. Regardless of the method you choose, you should always be aware of the legalities involved with receiving and making payments in your country.
The next installment in this series will guide you through some of the methods you can use to alter the appearance of the GridView. This will then leave you with the knowledge of how to use the GridView to produce a fully functional and pretty nice looking shop.