© Lee Naylor 2016

Lee Naylor, ASP.NET MVC with Entity Framework and CSS , 10.1007/978-1-4842-2137-2_8

8. Creating a Shopping Basket

Lee Naylor

(1)Newton-le-Willows, Merseyside, UK

This chapter covers how to allow users to add products to their shopping basket. We’re going to treat a shopping basket as a list of “basket lines” with a basket ID, a product, a quantity, and the time when the entry was added to the basket. The features of the basket will be as follows:

  • The basket will allow logged in and anonymous users to add items to their basket and store these baskets in the database.

  • For demonstration purposes, we’re going to use the session to store a key that will represent the current user. The default session timeout is set to 20 minutes for ASP.NET web applications.

  • If the user is anonymous, we'll generate a GUID to represent the user and if the user is logged in, we'll use the username to store basket entries.

  • The site will convert the GUID into a userID if a user logs in or registers after adding items to the basket.

  • If a logged in user logs out, the site will no longer display the items in the basket since anyone could then see them or edit the basket.

  • If a user has previously added items to their basket, the site will display them when they log in.

  • The site will not empty a logged in user’s basket unless they choose to do so themselves.

Note

If you want to follow along with the code in this chapter, you must either have completed Chapter 7 or download Chapter 7’s source code from www.apress.com as a starting point.

Adding a BasketLine Entity

Caution

We use the ASP.NET Session State to store the user’s current basket ID; however, in a high-traffic production web site, this is not a viable solution so you will need to consider an alternative. For example, if you are running your site in Azure, one alternative is to use Windows Azure Caching Service.

To start creating a basket, we’re going to create a BasketLine class containing a basket ID, a product, a quantity, and the time when the entry was added to the basket. The best way to think of this class is that it represents a physical line on the screen in a basket. Add a new class named BasketLine to the models folder of the project as follows:

using System;
using System.ComponentModel.DataAnnotations;


namespace BabyStore.Models
{
    public class BasketLine
    {
        public int ID { get; set; }
        public string BasketID { get; set; }
        public int ProductID { get; set; }
        [Range(0,50,ErrorMessage="Please enter a quantity between 0 and 50")]
        public int Quantity { get; set; }
        public DateTime DateCreated { get; set; }
        public virtual Product Product { get; set; }
    }
}

This is a simple class with an ID property to act as the key of the BasketLine entry in the database, a BasketID (a GUID or userID) to relate the BasketLine to a basket, a ProductID to relate it to a product, and properties reflecting the quantity of a product and the time the BasketLine was created. We've also added a navigation property to the product referenced by the BasketLine. The Quantity property has a Range attribute that checks that the range entered by a user is between 0 and 50 to prevent users from entering negative or excessive quantities to the basket.

Next, modify the DAL/StoreContext.cs file to add the BasketLines property.

using BabyStore.Models;
using System.Data.Entity;


namespace BabyStore.DAL
{
    public class StoreContext:DbContext
    {
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
        public DbSet<ProductImage> ProductImages { get; set; }
        public DbSet<ProductImageMapping> ProductImageMappings { get; set; }
        public DbSet<BasketLine> BasketLines { get; set; }
    }
}

Create a Code First Migration as follows in order to generate the BasketLines table. Run the following commands in Package Manager Console:

add-migration AddBasketLine -Configuration StoreConfiguration

This will generate a new migration file containing statements to create and drop a BasketLines database table. Now create the new BasketLines table in the database by running the following command in Package Manager Console:

update-database -Configuration StoreConfiguration

The database will now be updated and contain the new BasketLines table. Verify this by opening Server Explorer in Visual Studio and then opening the StoreContext connection. Then expand the tables node and you should be able to see the new BasketLines table, as shown in Figure 8-1.

A419071_1_En_8_Fig1_HTML.jpg
Figure 8-1. Viewing the new BasketLines table in Server Explorer

Adding Basket Logic

Now that we have a BasketLine class, we're going to create a Basket class to handle the main code for managing the shopping basket. The basket isn't like the other entities in the project in that it doesn't easily map onto CRUD operations or views and is created, updated, and deleted by adding or removing products from it. The main logic required is as follows:

  • Get a basket

  • Set a session key for a basket

  • Add a quantity of a product to a basket

  • Update the quantity of one or more products in a basket

  • Empty a basket

  • Calculate the total cost of a basket

  • Get the items in a basket

  • Get the overall number of products in a basket

  • Migrate the session key for a basket to a username when a user logs in

Start by adding a new class named Basket to the Models folder. Once the class is created, update the class to add a private string property named BasketID, a private constant named BasketSessionKey, and a new StoreContext instance as follows:

using BabyStore.DAL;

namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();
    }
}

Next add the following GetBasketID method to the Basket class, which is used to return a session entry named "BasketID". The session entry is set by the following rules:

If the session item BasketID is not null then return the current entry. If it is null then set the session's BasketID entry to the current user's username if they are logged in; otherwise, create a GUID and set the session's BasketID entry to the GUID.

GUID stands for Globally Unique Identifier and in theory they are always unique. However, if you ran the site for eternity, you would eventually get a duplicate one at some point.

using BabyStore.DAL;
using System;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }
    }
}

Ensure you also add the using statements using System.Web; and using System; to the top of the ModelsBasket.cs file. Following the GetBasketID method, add a new GetBasket method to the Basket class to create a new Basket instance, get the BasketID using the new GetBasketID method, and return the new Basket as follows:

using BabyStore.DAL;
using System;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }
    }
}

Now add a new AddToBasket method to the Basket class, using the productID and quantity parameters to add the specified quantity of the product to the basket.

using BabyStore.DAL;
using System;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }
    }
}

Add the statement using System.Linq; to the top of the file. This method attempts to find the BasketLine record where the BasketID and ProductID match the current basket and the ProductID input parameter. If there is no record found, then a new BasketLine is created because this basket does not contain the required product. Otherwise, the quantity value is updated by the provided quantity.

Next add a method named RemoveLine to the Basket class taking an integer, productID as an input parameter, to search for a BasketLine in the current user's basket containing the productID and deleting it from the database if found:

using BabyStore.DAL;
using System;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }


        public void RemoveLine(int productID)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);
            if (basketLine != null)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }
    }
}

Next update the Basket class to add a method named UpdateBasket, which takes a list of BasketLines as an input parameter. The method loops through the BasketLines and removes the BasketLine if its quantity is 0, or otherwise it sets the quantity to the input parameter value. This code also checks if the BasketLine is null to cover the case where a session has timed out. Note that if the session has expired, then a new empty basket is generated and returned to the user by the Basket class.

using BabyStore.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }


        public void RemoveLine(int productID)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);
            if (basketLine != null)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public void UpdateBasket(List<BasketLine> lines)
        {
            foreach (var line in lines)
            {
                var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                    b.ProductID == line.ProductID);
                if (basketLine != null)
                {
                    if (line.Quantity == 0)
                    {
                        RemoveLine(line.ProductID);
                    }
                    else
                    {
                        basketLine.Quantity = line.Quantity;
                    }
                }
            }
            db.SaveChanges();
        }
    }
}

Ensure you add using System.Collections.Generic; to the top of the file. Following this method, add a couple of methods to the Basket class—one named EmptyBasket to allow a user to empty the basket and one named GetBasketLines to return all the BasketLines for the current BasketID, as follows.

using BabyStore.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }


        public void RemoveLine(int productID)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);
            if (basketLine != null)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public void UpdateBasket(List<BasketLine> lines)
        {
            foreach (var line in lines)
            {
                var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                    b.ProductID == line.ProductID);
                if (basketLine != null)
                {
                    if (line.Quantity == 0)
                    {
                        RemoveLine(line.ProductID);
                    }
                    else
                    {
                        basketLine.Quantity = line.Quantity;
                    }
                }
            }
            db.SaveChanges();
        }


        public void EmptyBasket()
        {
            var basketLines = db.BasketLines.Where(b => b.BasketID == BasketID);
            foreach (var basketLine in basketLines)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public List<BasketLine> GetBasketLines()
        {
            return db.BasketLines.Where(b => b.BasketID == BasketID).ToList();
        }
    }
}

Now add two methods to the Basket class to calculate the total cost of the basket and to get the total number of products in the basket. Note that it's important to ensure you include the quantity of each item in your calculations.

using BabyStore.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }


        public void RemoveLine(int productID)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);
            if (basketLine != null)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public void UpdateBasket(List<BasketLine> lines)
        {
            foreach (var line in lines)
            {
                var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                    b.ProductID == line.ProductID);
                if (basketLine != null)
                {
                    if (line.Quantity == 0)
                    {
                        RemoveLine(line.ProductID);
                    }
                    else
                    {
                        basketLine.Quantity = line.Quantity;
                    }
                }
            }
            db.SaveChanges();
        }


        public void EmptyBasket()
        {
            var basketLines = db.BasketLines.Where(b => b.BasketID == BasketID);
            foreach (var basketLine in basketLines)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public List<BasketLine> GetBasketLines()
        {
            return db.BasketLines.Where(b => b.BasketID == BasketID).ToList();
        }


        public decimal GetTotalCost()
        {
            decimal basketTotal = decimal.Zero;


            if (GetBasketLines().Count > 0)
            {
                basketTotal = db.BasketLines.Where(b => b.BasketID == BasketID).Sum(b =>
                    b.Product.Price * b.Quantity);
            }


            return basketTotal;
        }


        public int GetNumberOfItems()
        {
            int numberOfItems = 0;
            if (GetBasketLines().Count > 0)
            {
                numberOfItems = db.BasketLines.Where(b => b.BasketID == BasketID).Sum(b =>
                    b.Quantity);
            }


            return numberOfItems;
        }
    }
}

Finally, complete the Basket class by adding a method to change the BasketID of the current Basket instance to the user's username. This method will be used to migrate the BasketID from a GUID to a username, when a user logs in or registers. The code in this method checks to see if a user already has a basket stored. If they do then it calls the AddToBasket method to add the items to the existing basket. This is done to avoid the scenario of getting two lines in the basket for the same product, which can occur if you simply change the BasketID of the BasketLines to the username. Also note that this method calls ToList() to store the baskets in memory. We do this to avoid getting an error relating to having multiple data readers open. Although this can increase memory usage, it will only be a short increase of a small amount and the number of users migrating a basket at the same time is likely to be very low. You should always be careful when using ToList() so as not to load huge lists into memory.

using BabyStore.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;


namespace BabyStore.Models
{
    public class Basket
    {
        private string BasketID { get; set; }
        private const string BasketSessionKey = "BasketID";
        private StoreContext db = new StoreContext();


        private string GetBasketID()
        {
            if (HttpContext.Current.Session[BasketSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(HttpContext.Current.User.Identity.Name))
                {
                    HttpContext.Current.Session[BasketSessionKey] =
                        HttpContext.Current.User.Identity.Name;
                }
                else
                {
                    Guid tempBasketID = Guid.NewGuid();
                    HttpContext.Current.Session[BasketSessionKey] = tempBasketID.ToString();
                }
            }
            return HttpContext.Current.Session[BasketSessionKey].ToString();
        }


        public static Basket GetBasket()
        {
            Basket basket = new Basket();
            basket.BasketID = basket.GetBasketID();
            return basket;
        }


        public void AddToBasket(int productID, int quantity)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);


            if (basketLine == null)
            {
                basketLine = new BasketLine
                {
                    ProductID = productID,
                    BasketID = BasketID,
                    Quantity = quantity,
                    DateCreated = DateTime.Now
                };
                db.BasketLines.Add(basketLine);
            }
            else
            {
                basketLine.Quantity += quantity;
            }
            db.SaveChanges();
        }


        public void RemoveLine(int productID)
        {
            var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                b.ProductID == productID);
            if (basketLine != null)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public void UpdateBasket(List<BasketLine> lines)
        {
            foreach (var line in lines)
            {
                var basketLine = db.BasketLines.FirstOrDefault(b => b.BasketID == BasketID &&
                    b.ProductID == line.ProductID);
                if (basketLine != null)
                {
                    if (line.Quantity == 0)
                    {
                        RemoveLine(line.ProductID);
                    }
                    else
                    {
                        basketLine.Quantity = line.Quantity;
                    }
                }
            }
            db.SaveChanges();
        }


        public void EmptyBasket()
        {
            var basketLines = db.BasketLines.Where(b => b.BasketID == BasketID);
            foreach (var basketLine in basketLines)
            {
                db.BasketLines.Remove(basketLine);
            }
            db.SaveChanges();
        }


        public List<BasketLine> GetBasketLines()
        {
            return db.BasketLines.Where(b => b.BasketID == BasketID).ToList();
        }


        public decimal GetTotalCost()
        {
            decimal basketTotal = decimal.Zero;


            if (GetBasketLines().Count > 0)
            {
                basketTotal = db.BasketLines.Where(b => b.BasketID == BasketID).Sum(b =>
                    b.Product.Price * b.Quantity);
            }


            return basketTotal;
        }


        public int GetNumberOfItems()
        {
            int numberOfItems = 0;
            if (GetBasketLines().Count > 0)
            {
                numberOfItems = db.BasketLines.Where(b => b.BasketID == BasketID).Sum(b =>
                b.Quantity);
            }


            return numberOfItems;
        }


        public void MigrateBasket(string userName)
        {
            //find the current basket and store it in memory using ToList()
            var basket = db.BasketLines.Where(b => b.BasketID == BasketID).ToList();


            //find if the user already has a basket or not and store it in memory using //ToList()
            var usersBasket = db.BasketLines.Where(b => b.BasketID == userName).ToList();


            //if the user has a basket then add the current items to it
            if (usersBasket != null)
            {
                //set the basketID to the username
                string prevID = BasketID;
                BasketID = userName;
                //add the lines in anonymous basket to the user's basket
                foreach (var line in basket)
                {
                    AddToBasket(line.ProductID, line.Quantity);
                }
                //delete the lines in the anonymous basket from the database
                BasketID = prevID;
                EmptyBasket();
            }
            else
            {
                //if the user does not have a basket then just migrate this one
                foreach (var basketLine in basket)
                {
                    basketLine.BasketID = userName;
                }
                db.SaveChanges();
            }
            HttpContext.Current.Session[BasketSessionKey] = userName;
        }
    }
}

Adding a Basket View Model

Add a new view model to display the basket contents and total by creating a new class named BasketViewModel.cs in the ViewModels folder. This class will be used as the model to base the Basket Index view on and simply contains a list of BasketLines and the total cost of the basket. The DisplayFormat attribute assigned to the TotalCost property specifies that it should be displayed in the local (server specific) currency format.

using BabyStore.Models;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;


namespace BabyStore.ViewModels
{
    public class BasketViewModel
    {
        public List<BasketLine> BasketLines { get; set; }
        [Display(Name = "Basket Total:")]
        [DisplayFormat(DataFormatString = "{0:c}")]
        public decimal TotalCost { get; set; }
    }
}

Adding a Basket Controller

We’re now going to create a BasketController class to handle requests for updating the basket. The BasketController class effectively acts as a proxy class, calling the methods of the Basket class and then redirecting to the Index method. The Index method then calls the Index view to redisplay the contents of the basket.

Start by adding an empty MVC5 controller by right-clicking on the Controllers folder and choosing AddController. Then add an MVC5 Controller - Empty. Name the new controller BasketController.

Update the new BasketController class as follows to update the Index method. The updates create a new basket and a BasketViewModel and assign the lines and cost of the basket to it. It then passes the view model to the Index view.

using BabyStore.Models;                
using BabyStore.ViewModels;
using System.Web.Mvc;


namespace BabyStore.Controllers
{
    public class BasketController : Controller
    {
        // GET: Basket
        public ActionResult Index()
        {
            Basket basket = Basket.GetBasket();
            BasketViewModel viewModel = new BasketViewModel
            {
                BasketLines = basket.GetBasketLines(),
                TotalCost = basket.GetTotalCost()
            };
            return View(viewModel);
        }
    }
}

Next update the BasketController class to add a new method named AddToBasket, which first calls the GetBasket method of the Basket class to obtain the current basket and then calls the AddToBasket method, passing in a productID and a quantity to add to the basket. After this, the method redirects to the Index action, resulting in the Index view being displayed with the updates.

using BabyStore.Models;
using BabyStore.ViewModels;
using System.Web.Mvc;


namespace BabyStore.Controllers
{
    public class BasketController : Controller
    {
        // GET: Basket
        public ActionResult Index()
        {
            Basket basket = Basket.GetBasket();
            BasketViewModel viewModel = new BasketViewModel
            {
                BasketLines = basket.GetBasketLines(),
                TotalCost = basket.GetTotalCost()
            };
            return View(viewModel);
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult AddToBasket(int id, int quantity)
        {
            Basket basket = Basket.GetBasket();
            basket.AddToBasket(id, quantity);
            return RedirectToAction("Index");
        }
    }
}

Following this, add a new method to the BasketController class named UpdateBasket. This method takes BasketViewModel as an input parameter and then passes its BasketLines property to the UpdateBasket method of the Basket class.

using BabyStore.Models;
using BabyStore.ViewModels;
using System.Web.Mvc;


namespace BabyStore.Controllers
{
    public class BasketController : Controller
    {
        // GET: Basket
        public ActionResult Index()
        {
            Basket basket = Basket.GetBasket();
            BasketViewModel viewModel = new BasketViewModel
            {
                BasketLines = basket.GetBasketLines(),
                TotalCost = basket.GetTotalCost()
            };
            return View(viewModel);
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult AddToBasket(int id, int quantity)
        {
            Basket basket = Basket.GetBasket();
            basket.AddToBasket(id, quantity);
            return RedirectToAction("Index");
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult UpdateBasket(BasketViewModel viewModel)
        {
            Basket basket = Basket.GetBasket();
            basket.UpdateBasket(viewModel.BasketLines);
            return RedirectToAction("Index");
        }
    }
}

Next add a RemoveLine method. This differs from most of the other methods in the BasketController and pretty much all other the other controller classes in this book because it is an HttpGet version of a method that updates the database. In earlier chapters, I recommended using HttpPost to perform database updates, but here we have a scenario where we’re not technically going to be able to use a HTML form to submit a request to remove a line from the basket, so we’re going to use HttpGet. You will see in the view file that we generate an HTML form that surrounds all of the lines in the basket and so we cannot then include another HTML form within this larger form because it is not valid HTML. Instead, we'll create a set of hyperlinks to this method. Using HttpGet in this scenario is acceptable because in order to create a basket with items in it, the user first needs to submit an HTML form using POST.

using BabyStore.Models;
using BabyStore.ViewModels;
using System.Web.Mvc;


namespace BabyStore.Controllers
{
    public class BasketController : Controller
    {
        // GET: Basket
        public ActionResult Index()
        {
            Basket basket = Basket.GetBasket();
            BasketViewModel viewModel = new BasketViewModel
            {
                BasketLines = basket.GetBasketLines(),
                TotalCost = basket.GetTotalCost()
            };
            return View(viewModel);
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult AddToBasket(int id, int quantity)
        {
            Basket basket = Basket.GetBasket();
            basket.AddToBasket(id, quantity);
            return RedirectToAction("Index");
        }


        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult UpdateBasket(BasketViewModel viewModel)
        {
            Basket basket = Basket.GetBasket();
            basket.UpdateBasket(viewModel.BasketLines);
            return RedirectToAction("Index");
        }


        [HttpGet]
        public ActionResult RemoveLine(int id)
        {
            Basket basket = Basket.GetBasket();
            basket.RemoveLine(id);
            return RedirectToAction("Index");
        }
    }
}

Adding a Basket Index View

We now need to add a view to display the basket. We’re only going to create one view to display the basket because of the way the basket functions. A basket is effectively created by adding a product to it and updated by adding or removing more products. You add a product via the Product Details page and changing the quantity, removing a product (resulting in removing a BasketLine), and emptying the basket are all done from one page; the Basket Index page.

Prior to adding a new Basket Index view, remove the line of code <li>@Html.ActionLink("Home", "Index", "Home")</li> from the ViewsShared\_Layout.cshtml file to remove the home link and make some space in the navigation bar for use later in the chapter.

To add a new Basket Index view, right-click on the Index method in the ControllersBasketController.cs file and add a new view named Index, using the Details Template option. Set the Model Class to BasketViewModel. Set the Data Context to blank and check the Reference Script Libraries and Use a Layout Page options. Figure 8-2 shows the option to select.

A419071_1_En_8_Fig2_HTML.jpg
Figure 8-2. Adding a new Basket Index view

Update the newly create ViewsBasketIndex.cshtml file as follows. This updates the title and the layout to use divs rather than dl, dd, and dt tags:

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <div class="row">
        <div class="col-md-8">
            @Html.DisplayNameFor(model => model.TotalCost)
        </div>
        <div class="col-md-1">
            @Html.DisplayFor(model => model.TotalCost)
        </div>
    </div>
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

In the code, the div with the CSS class col-md-8 will be eight columns wide and the one with col-md-1 will be one column wide. Right-click in the view file and choose View in Browser from the menu to see the new HTML page generated by the Basket Index view file. It should appear as shown in Figure 8-3.

A419071_1_En_8_Fig3_HTML.jpg
Figure 8-3. The Basket Index page updated to use divs

Now add some headings to the view file. They will be used to show headings for each line in the basket as follows:

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <div class="row">
        <div class="col-md-4"><label>Item</label></div>
        <div class="col-md-3"><label>Quantity</label></div>
        <div class="col-md-1"><label>Price</label></div>
        <div class="col-md-1"><label>Subtotal</label></div>
    </div>
    <hr />
    <div class="row">
        <div class="col-md-8">
            @Html.DisplayNameFor(model => model.TotalCost)
        </div>
        <div class="col-md-1">
            @Html.DisplayFor(model => model.TotalCost)
        </div>
    </div>
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

The headings will appear in the HTML page, as shown in Figure 8-4.

A419071_1_En_8_Fig4_HTML.jpg
Figure 8-4. The Basket Index HTML page with added headings

Now add some code to show each product added to the basket, along with the thumbnail version of its main image as follows:

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <div class="row">
        <div class="col-md-4"><label>Item</label></div>
        <div class="col-md-3"><label>Quantity</label></div>
        <div class="col-md-1"><label>Price</label></div>
        <div class="col-md-1"><label>Subtotal</label></div>
    </div>
    <hr />
    @for (int i = 0; i < Model.BasketLines.Count; i++)
    {
        <div class="row">
            <div class="col-md-4">
                @Html.ActionLink(Model.BasketLines[i].Product.Name, "Details",
                   "Products", new { id = Model.BasketLines[i].ProductID }, null)<br />
                     @if (Model.BasketLines[i].Product.ProductImageMappings != null &&
                        Model.BasketLines[i].Product.ProductImageMappings.Any())
                     {
                         <a href="@Url.Action("Details", "Products", new { id =
                             Model.BasketLines[i].ProductID })">
                             <img src="@(Url.Content(Constants.ProductThumbnailPath) +  
                                Model.BasketLines[i].Product.ProductImageMappings.OrderBy(pim
                              => pim.ImageNumber).ElementAt(0).ProductImage.FileName)">
                         </a>
                     }
                 </div>
             </div>
         <hr />
    }
    <div class="row">
        <div class="col-md-8">
            @Html.DisplayNameFor(model => model.TotalCost)
        </div>
        <div class="col-md-1">
            @Html.DisplayFor(model => model.TotalCost)
        </div>
    </div>
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

This additional code loops through each BasketLine in the view model and displays the name of each product, plus the product's main image as a thumbnail. Both these elements are generated as hyperlinks to the product's details. Note that rather than use a foreach loop, we have used a traditional for loop with a counter. The reason for this is to ensure that the MVC Framework can automatically recognize the values submitted by the form as a BasketViewModel. I will explain this further after the next section.

Allowing a User to Add to Basket

We are going to allow users to add products to their baskets from the Products Details page, so we now need to add an HTML form to this view. The form will target the AddToBasket action method of the BasketController class. In order to generate this form, add the following code to ViewsProductsDetails.cshtml file as the last element in the <dl> tags just before the closing </dl> element:

@model BabyStore.Models.Product

@{
    ViewBag.Title = "Product Details";
}


<h2>@ViewBag.Title</h2>

<div>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Category.Name)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Category.Name)
        </dd>


        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>


        <dt>
            @Html.DisplayNameFor(model => model.Description)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Description)
        </dd>


        <dt>
            @Html.DisplayNameFor(model => model.Price)
        </dt>


        <dd>
            @Html.DisplayFor(model => model.Price)
        </dd>
        @if (Model.ProductImageMappings != null && Model.ProductImageMappings.Any())
        {
            <dt></dt>
            <dd>
                <img src="@(Url.Content(Constants.ProductImagePath) +
                    Model.ProductImageMappings.OrderBy(pim =>  pim.ImageNumber).ElementAt(0).ProductImage.FileName)" style=padding:5px>
            </dd>
            <dt></dt>
            <dd>
                @foreach (var item in Model.ProductImageMappings.OrderBy(pim =>
                    pim.ImageNumber))
                {
                    <a href="@(Url.Content(Constants.ProductImagePath) +
                        item.ProductImage.FileName)">
                        <img src="@(Url.Content(Constants.ProductThumbnailPath) +
                            item.ProductImage.FileName)" style=padding:5px>
                    </a>
                }
            </dd>
        }
        <dt>
            Quantity:
        </dt>
        <dd>
            @using (Html.BeginForm("AddToBasket", "Basket"))
            {
                @Html.AntiForgeryToken()
                @Html.HiddenFor(model => model.ID)
                @Html.DropDownList("quantity", Enumerable.Range(1, 10).Select(i => new
                     SelectListItem { Text = i.ToString(), Value = i.ToString() }))
                <input type="submit" class="btn btn-primary btn-xs" value="Add to Basket">
            }
        </dd>
    </dl>
</div>
<p>
    @Html.ActionLink("Edit", "Edit", new { id = Model.ID }) |
    @Html.ActionLink("Back to List", "Index")
</p>

This form contains a DropDownList helper that uses a LINQ query to generate an HTML select control showing the values 1 to 10. This control is named "quantity" and the form submits this quantity value along with the product's ID to the AddToBasket method of the BasketController class. I have chosen to add this control directly to the view, but if required you could create a view model and add this new quantity property to it for completeness. To see this in action, start the web site without debugging and click on the View All Our Products link from the home page. Then click on the image for the 3 Pack of Bibs product. You will then see the new form at the bottom of the page, as shown in Figure 8-5.

A419071_1_En_8_Fig5_HTML.jpg
Figure 8-5. The new “Add to Basket” form displayed in a product’s details

Enter 3 as the quantity and click the Add to Basket button. You will be redirected to the Basket Index page, as shown in Figure 8-6, and the basket will now show the product's main image from the thumbnails folder. The basket total will also be displayed, in this case it will be 3 x £8.99, which is £26.97. The process that got you here was that the new form called the AddToBasketMethod of the BasketController with the product's ID and quantity. This in turn called the Basket class and created a new BasketLine entry by calling the AddToBasket method while first generating a new BasketID and storing this in the session.

A419071_1_En_8_Fig6_HTML.jpg
Figure 8-6. “3 Pack of Bibs” products added to the basket

To see the effect on the database, open Server Explorer and then expand the StoreContext connection and view the data of the BasketLines table. You should see something similar to Figure 8-7, although DateCreated and BasketID will differ. The BasketID is a GUID generated when first calling the Basket class and this will be stored in the session under the key “BasketID”.

A419071_1_En_8_Fig7_HTML.jpg
Figure 8-7. The new BasketLine entry in the BasketLines database table

Note that if a user adds a previously added item to their basket via the Product Details page, then the quantity of the existing item is simply updated for the additional quantity of items. A new BasketLine is not created.

Updating the Basket: Model Binding to a List or an Array

Now that we have items displayed in the basket, we need a way to be able to update the quantity of items in the basket. The ViewsBasketIndex.cshtml file is based on the BasketViewModel and this is the type expected by the UpdateBasket method in the ControllersBasketController.cs file. This type contains a list of the BasketLine type, and we are going to use an HTML form inside the view to update the quantity of each of these BasketLines. Therefore, we need a way to ensure that a repeating list of BasketLines is correctly bound by the model-binding process.

The model-binding process for list types relies on index numbers to perform binding correctly. Each HTML control must contain an index number. This is the reason that we used a traditional style for loop rather than a foreach loop when displaying the images for each product. With a foreach loop, each HTML control generated for each line would have the same name, so we would have several inputs with the same name for the productID and quantity.

If this sounds a bit confusing, to see this in action, update the ViewsBasketIndex.cshtml file as follows to allow the user to update the quantity of each line in the basket. We’re also going to fill in the data to display a product's price and the subtotal of the line.

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>

<div>
    @using (Html.BeginForm("UpdateBasket", "Basket"))
    {
        @Html.AntiForgeryToken();
        <input class="btn btn-sm btn-success" type="submit" value="Update Basket" />
        <hr />
        <div class="row">
            <div class="col-md-4"><label>Item</label></div>
            <div class="col-md-3"><label>Quantity</label></div>
            <div class="col-md-1"><label>Price</label></div>
            <div class="col-md-1"><label>Subtotal</label></div>
        </div>
        <hr />
        for (int i = 0; i < Model.BasketLines.Count; i++)
           {
        <div class="row">
            <div class="col-md-4">
                @Html.ActionLink(Model.BasketLines[i].Product.Name, "Details",
                   "Products", new { id = Model.BasketLines[i].ProductID }, null)<br />
                     @if (Model.BasketLines[i].Product.ProductImageMappings != null &&
                        Model.BasketLines[i].Product.ProductImageMappings.Any())
                     {
                         <a href="@Url.Action("Details", "Products", new { id =
                             Model.BasketLines[i].ProductID })">
                             <img src="@(Url.Content(Constants.ProductThumbnailPath) +  
                                Model.BasketLines[i].Product.ProductImageMappings.OrderBy(pim
                              => pim.ImageNumber).ElementAt(0).ProductImage.FileName)">
                         </a>
                     }
                 </div>
                <div class="col-md-3">
                    @Html.HiddenFor(productID => Model.BasketLines[i].ProductID)                    
                    @Html.TextBoxFor(quantity => Model.BasketLines[i].Quantity)
                     <p>@Html.ValidationMessageFor(quantity => Model.BasketLines[i].Quantity,
                      "", new { @class = "text-danger" })</p>
                 </div>
                 <div class="col-md-1">@Html.DisplayFor(price =>
                    Model.BasketLines[i].Product.Price)</div>
                 <div class="col-md-1">@((Model.BasketLines[i].Quantity *
                    Model.BasketLines[i].Product.Price).ToString("c"))</div>
             </div>
             <hr />
         }
     }
     <div class="row">
         <div class="col-md-8">
             @Html.DisplayNameFor(model => model.TotalCost)
         </div>
         <div class="col-md-1">
             @Html.DisplayFor(model => model.TotalCost)
         </div>
    </div>
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

The new code adds an HTML form that targets the UpdateBasket method in the BasketController class. We’ve added this form so it surrounds all the lines of HTML generated by displaying the each BasketLine item in the BasketViewModel and we’ve added a Submit button at the top of the form labeled “Update Basket”. Ensure you remove the @ character from the beginning of the for statement.

We’ve also added a hidden input for the ProductId, a text box input for the Quantity. We have used the index number i in these elements. As an example, if we had a basket with two lines in it, when the HTML is generated for each control, it would be generated with the names as follows: BasketLines[0].ProductId, BasketLines[0].Quantity for the first line followed by BasketLines[1].ProductId, BasketLines[1].Quantity for the second line.

When these are now submitted to the UpdateBasket method, it can recognize that each of these controls represents an item in the BasketLines list property of the BasketViewModel.

The input for submitting the quantity is based on the Quantity property of the BasketLine class, which allows a range of 0 to 50, so we have also used the Html.ValidationMessageFor helper to show a validation message if required.

We've also added code to display the price of the product and the subtotal of the current line. We've calculated the subtotal in the view because we are not interested in storing it anywhere. Since this is calculated on the fly, we have also given an example of formatting this in the view as currency by using .ToString("c").

To see how the model binding works in detail, add a break point to the line containing the first bracket after the line of code public ActionResult UpdateBasket(BasketViewModel viewModel) in the ControllersBasketController.cs file and then start the web site with debugging. This is likely to have destroyed your previous session, so add two Black Pram and Pushchair Systems to the basket. Then add one Blue Rabbit. The basket should now appear as shown in Figure 8-8, with a quantity, price, and subtotal for each line in the basket.

A419071_1_En_8_Fig8_HTML.jpg
Figure 8-8. The updated basket showing a quantity, price, and subtotal for each line in the basket

View the HTML source of the Basket Index page by right-clicking in the browser and choosing View Source from the menu. If you look at the source of each of the basket, you will now see the HTML controls generated by using the Index value in the view code. For the pram line, the HTML source code for the ProductId and the Quantity input elements appears as follows, with the element names shown in bold:

<input id="BasketLines_0__ProductId" name="BasketLines[0].ProductId" type="hidden"
    value="11" />
<input data-val="true" data-val-number="The field Quantity must be a number." data-val-
    range="Please enter a quantity between 0 and 50" data-val-range-max="50" data-val-range-
    min="0" data-val-required="The Quantity field is required." id="BasketLines_0__Quantity"
    name="BasketLines[0].Quantity" type="text" value="2" />

For the rabbit line, the source code appears as:

<input id="BasketLines_1__ProductId" name="BasketLines[1].ProductId" type="hidden" value="4"
    />
<input data-val="true" data-val-number="The field Quantity must be a number." data-val-
    range="Please enter a quantity between 0 and 50" data-val-range-max="50" data-val-range-
    min="0" data-val-required="The Quantity field is required." id="BasketLines_1__Quantity"
    name="BasketLines[1].Quantity" type="text" value="1" />

Next, update the quantity of the Black Pram and Pushchair System to 3 and click the Update Basket button. The breakpoint you added to the UpdateBasket method of the BasketController class will be hit. Examine the contents of the viewModel variable that have been passed to the method by the MVC Framework by hovering over it. You will see that this variable has correctly bound the ProductId and Quantity for both lines. Figure 8-9 shows the updated quantity value for the first BasketLine element (containing the pram) with an updated quantity of 3.

A419071_1_En_8_Fig9_HTML.jpg
Figure 8-9. Debugging the UpdateBasket method to show model binding of the viewModel’s BasketLine list

Click the Continue button in Visual Studio. The web site will reappear with the basket now updated, as shown in Figure 8-10.

A419071_1_En_8_Fig10_HTML.jpg
Figure 8-10. The basket updated with three Black Pram and Pushchair Systems and price totals

Deleting a Line or Product from the Basket

To complete the functionality in the Basket Index view, update the ViewsBasketIndex.cshtml file to add a new link for each BasketLine. This will allow the users to remove a line from the basket by adding the code shown in bold:

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>

<div>
    @using (Html.BeginForm("UpdateBasket", "Basket"))
    {
        @Html.AntiForgeryToken();
        <input class="btn btn-sm btn-success" type="submit" value="Update Basket" />
        <hr />
        <div class="row">
            <div class="col-md-4"><label>Item</label></div>
            <div class="col-md-3"><label>Quantity</label></div>
            <div class="col-md-1"><label>Price</label></div>
            <div class="col-md-1"><label>Subtotal</label></div>
        </div>
        <hr />
        for (int i = 0; i < Model.BasketLines.Count; i++)
        {
            <div class="row">
                <div class="col-md-4">
                    @Html.ActionLink(Model.BasketLines[i].Product.Name, "Details",
                   "Products", new { id = Model.BasketLines[i].ProductID }, null)<br />
                    @if (Model.BasketLines[i].Product.ProductImageMappings != null &&
                        Model.BasketLines[i].Product.ProductImageMappings.Any())
                    {
                        <a href="@Url.Action("Details", "Products", new { id =
                             Model.BasketLines[i].ProductID })">
                            <img src="@(Url.Content(Constants.ProductThumbnailPath) +
                                Model.BasketLines[i].Product.ProductImageMappings.OrderBy(pim
                                => pim.ImageNumber).ElementAt(0).ProductImage.FileName)">
                        </a>
                    }
                </div>
                <div class="col-md-3">
                    @Html.HiddenFor(productID => Model.BasketLines[i].ProductID)
                    @Html.TextBoxFor(quantity => Model.BasketLines[i].Quantity)
                    <p>
                        @Html.ValidationMessageFor(quantity => Model.BasketLines[i].Quantity,
                            "", new { @class = "text-danger" })
                    </p>
                </div>
                <div class="col-md-1">
                    @Html.DisplayFor(price => Model.BasketLines[i].Product.Price)
                </div>
                <div class="col-md-1">
                    @((Model.BasketLines[i].Quantity *
                        Model.BasketLines[i].Product.Price).ToString("c"))
                </div>
                <div class="col-md-1">
                    @Html.ActionLink("Remove", "RemoveLine", "Basket", new
                        { id = Model.BasketLines[i].Product.ID }, null)
                </div>
            </div>
            <hr />
        }
    }
    <div class="row">
        <div class="col-md-8">
            @Html.DisplayNameFor(model => model.TotalCost)
        </div>
        <div class="col-md-1">
            @Html.DisplayFor(model => model.TotalCost)
        </div>
    </div>
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

This new link targets the RemoveLine method of the BasketController class. This link is contained in the HTML form to update the basket and so you will recall this is why the RemoveLine method is an HttpGet method as opposed to HttpPost. Figure 8-11 shows the new Remove link as it appears in the Basket Index page.

A419071_1_En_8_Fig11_HTML.jpg
Figure 8-11. The Remove link added to every line in the basket

If you remove a line from the basket by using the Remove link, it is also deleted from the BasketLines database table. It is also possible to remove a line/product from the basket by setting its quantity to zero and updating the basket.

Finally, add some code to the ViewsBasketIndex.cshtml file to check if the basket has any items in it. If it does not, then display a message telling the user their basket is empty. Also add a Continue Shopping link by updating the view as follows:

@model BabyStore.ViewModels.BasketViewModel

@{
    ViewBag.Title = "Your Basket";
}


<h2>@ViewBag.Title</h2>
@if (Model.BasketLines.Count() > 0)
{
    <div>
        @using (Html.BeginForm("UpdateBasket", "Basket"))
        {
        ...code omitted for brevity
    </div>
}
else
{
    <p>Your Basket is empty</p>
}
<div>
    @Html.ActionLink("Continue Shopping", "Index", "Products")
</div>
@section Scripts {
    @Scripts.Render("∼/bundles/jqueryval")
}

Now if a user tries to view an empty basket, they will receive the message shown in Figure 8-12, including a Continue Shopping link.

A419071_1_En_8_Fig12_HTML.jpg
Figure 8-12. Viewing an empty basket

Displaying a Basket Summary

We are now going to add a basket summary to the main navigation bar of the web site. This will allow the users to click on it to view their baskets.

Create a new class named BasketSummaryViewModel in the ViewModels folder to include a property to hold the number of items in the basket and the total cost of the basket as follows:

using System.ComponentModel.DataAnnotations;

namespace BabyStore.ViewModels
{
    public class BasketSummaryViewModel
    {
        public int NumberOfItems { get; set; }
        [DataType(DataType.Currency)]
        [DisplayFormat(DataFormatString = "{0:c}")]
        public decimal TotalCost { get; set; }
    }
}

Next, add a Summary method to the ControllersBasketController.cs file to return a PartialViewResult and set the values in an instance of the BasketSummaryViewModel. This happens by calling the GetNumberOfItems and GetTotalCost methods from the Basket instance, as follows:

public PartialViewResult Summary()
{
    Basket basket = Basket.GetBasket();
    BasketSummaryViewModel viewModel = new BasketSummaryViewModel
    {
        NumberOfItems = basket.GetNumberOfItems(),
        TotalCost = basket.GetTotalCost()
    };
    return PartialView(viewModel);
}

Right-click on the new method and choose Add View. Create a new empty view named Summary with the model class BasketSummaryViewModel.

Open the newly created ViewsBasketSummary.cshtml file and edit it as follows:

@model BabyStore.ViewModels.BasketSummaryViewModel

<ul class="nav navbar-nav navbar-right">
    <li>
        @Html.ActionLink("Your basket: " + Model.NumberOfItems + " items(s) " +
            HttpUtility.HtmlDecode(Html.DisplayFor(model => Model.TotalCost).ToString()),
            "Index", "Basket")
    </li>
</ul>

This code looks a little awkward with regard to displaying the total value, but this is required to ensure the pound sign is shown correctly. The Html.DisplayFor helper is called for the basket's TotalValue property, but this then needs to be converted to a string and passed into the HttpUtility.HtmlDecode function to get the HTML for the currency symbol to display as £ and not as £.

Finally, add the new basket summary to the main navigation bar by updating the ViewsShared\_layout.cshtml file. You do so by adding a new line of code after the line that displays the loginPartial view as follows:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("∼/Content/css")
    @Scripts.Render("∼/bundles/modernizr")
</head>
<body>
    <div class="navbar navbar-inverse navbar-static-top">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" data-
                    target=".navbar-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Baby Store", "Index", "Home", new { area = "" }, new {
                    @class = "navbar-brand" })
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li>@Html.ActionLink("Shop by Category", "Index", "Categories")</li>
                    <li>@Html.RouteLink("View all our Products", "ProductsIndex")</li>
                    @if (Request.IsAuthenticated && User.IsInRole("Admin"))
                    {
                        <li>@Html.ActionLink("Admin", "Index", "Admin")</li>
                    }
                </ul>
                @using (Html.BeginRouteForm("ProductsIndex", FormMethod.Get, new { @class =
                    "navbar-form navbar-left" }))
                {
                    <div class="form-group">
                        @Html.TextBox("Search", null, new { @class = "form-control",
                            @placeholder = "Search Products" })
                    </div>
                    <button type="submit" class="btn btn-default">Submit</button>
                }
                @Html.Partial("_LoginPartial")
                @Html.Action("Summary", "Basket")
            </div>
        </div>
    </div>
    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>


    @Scripts.Render("∼/bundles/jquery")
    @Scripts.Render("∼/bundles/bootstrap")
    @RenderSection("scripts", required: false)
</body>
</html>

Using the Html.Action method has allowed us to call a method from a different controller and include its output in this view. Start the web site without debugging and add items to your basket. You will see the number of items and total cost update in the summary as you add and remove items from the basket. Figure 8-13 shows the new basket summary included in the navigation bar. It reflects the current five items shown in the basket.

A419071_1_En_8_Fig13_HTML.jpg
Figure 8-13. The web site now containing the basket summary in the navigation bar

The web site doesn't format the navigation bar correctly when the admin user logs in following the addition of the basket summary, so update the max-width of the web site to 1500pixels by updating the following in the Contentootstrap.css file:

@media (min-width: 1200px) {
  .container {
    max-width: 1500px;
  }

Migrating a Basket When a User Logs In or Registers

A couple of tasks remain to be dealt with in regard to the basket. We need to migrate the basket to use the current user's username as the BasketID whenever a user logs in or a new user registers. This will retain users' baskets even when they log out of the web site.

Migrating the Basket Upon Login

To migrate the basket when a user logs in, we need to modify the Login method of ControllersAccountController.cs file. We’re going to add code that migrates a user’s basket to use their username as the basketID rather than a GUID.

We also need to address the scenario whereby a new user logs in without the previous user logging out. We don’t want the basket to migrate to the other user. This can occur if a user is redirected to the login page if they try to access an admin page. In this scenario, we do not want to migrate the basket to the admin user; we want it to remain associated with the original user.

Modify the Login method of the AccountController class with the code shown in bold:

// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{


    if (!ModelState.IsValid)
    {
        return View(model);
    }


    //check if a user was previously logged in without logging out. This can occur for example  
    //if a logged in user is redirected to an admin page and then an admin user logs in
    bool userWasLoggedIn = false;
    if (!string.IsNullOrWhiteSpace(User.Identity.Name))
    {
        userWasLoggedIn = true;
    }


    // This doesn't count login failures towards account lockout
    // To enable password failures to trigger account lockout, change to shouldLockout: true
    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password,
        model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            //this is needed to ensure the previous user's basket is not carried over
            if (userWasLoggedIn)
            {
                Session.Abandon();
            }
            Basket basket = Basket.GetBasket();
            //if there was no previously logged in user migrate the basket from GUID to the
            //username
            if (!userWasLoggedIn)
            {
                basket.MigrateBasket(model.Email);
            }
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe =
            model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            ViewBag.ReturnUrl = returnUrl;
            return View(model);
    }
}

To see the effect of this new code, start the web site without debugging add some products to your basket without logging in. Then log in as [email protected] with the password P@ssw0rd1. The BasketID property for each BasketLine should now have been migrated to [email protected]. Validate this by using Server Explorer in Visual Studio to view the data of the BasketLines database table (via the StoreContext connection). You should see that the records in the database relating to the current basket have been updated to use [email protected] as the BasketID. Figure 8-14 shows some sample products we have added to the basket without logging in, after which we logged in as [email protected]. Figure 8-15 shows the affected records of the database updated to use the username as the BasketID.

A419071_1_En_8_Fig14_HTML.jpg
Figure 8-14. The basket migrated to [email protected]
A419071_1_En_8_Fig15_HTML.jpg
Figure 8-15. The BasketLines database showing the basket records updated to use [email protected] as the BasketID

If you now log out of the web site, you will see that the basket summary still shows the items in the basket belonging to [email protected]. To fix this issue, it is necessary to ensure that the session key is updated on logging out. Modify the LogOff method of the AccountController class as follows:

//
// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    Session.Abandon();
    return RedirectToAction("Index", "Home");
}

This Session.Abandon method abandons the current session when the user logs out and ensures that any user who follows the previously logged in user cannot see what was previously in the basket.

Migrating the Basket Upon Registration

It is also necessary to migrate the basket to use a user’s e-mail address when a new user registers. A new user may have added items to an anonymous basket and in order to make a purchase they must then register. Therefore, after they have registered, we need to ensure they do not lose the contents of their shopping basket. Modify the HttpPost version of Register in the AccountController.cs file so that it contains code for migrating the basket to the new user as follows:

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model, string returnUrl)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser {
            UserName = model.Email,
            Email = model.Email,
            DateOfBirth = model.DateOfBirth,
            FirstName = model.FirstName,
            LastName = model.LastName,
            Address = model.Address
        };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await UserManager.AddToRoleAsync(user.Id, "Users");
            await SignInManager.SignInAsync(user, isPersistent:false, rememberBrowser:false);
            Basket basket = Basket.GetBasket();
            basket.MigrateBasket(model.Email);
            // For more information on how to enable account confirmation and password reset
               please visit http://go.microsoft.com/fwlink/?LinkID=320771
            // Send an email with this link
            // string code = await UserManager.GenerateEmailConfirmationTokenAsync(user.Id);
            // var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id,
                   code = code }, protocol: Request.Url.Scheme);
            // await UserManager.SendEmailAsync(user.Id, "Confirm your account", "Please  
            // confirm your account by clicking <a href="" + callbackUrl + "">here</a>");
            // redirect the user back to the page they came from if it was local otherwise send
               them to home page
            return RedirectToLocal(returnUrl);
        }
        ViewBag.ReturnUrl = returnUrl;
        AddErrors(result);
    }


    // If we got this far, something failed, redisplay form
    return View(model);
}

Start the web site without debugging and add some items to the basket. Now register as the new user [email protected] using the values shown in Figure 8-16 and the password P@ssw0rd.

A419071_1_En_8_Fig16_HTML.jpg
Figure 8-16. Registering the user [email protected]

When you click the Create button, the user will be registered and the BasketID in the database table BasketItems for the records relating to the basket will be updated to [email protected], as shown in Figure 8-17.

A419071_1_En_8_Fig17_HTML.jpg
Figure 8-17. The BasketID field updated for the basket belonging to the newly registered [email protected]

Summary

In this chapter, you saw how to create a shopping basket and allow a user to anonymously add and remove items from the basket and update the quantity of items in the basket. I showed you how to use ASP.NET MVC model binding to bind to a list in order to update the basket. You also learned how to display a basket summary by using the Html.Action helper method to call an action from a different controller and include its output in another view. Finally, you saw how to migrate the basket from using a randomly generated GUID to be associated with a real user when a user logs in or registers.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset