Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adding Quantity Update Feature to Shopping Cart for ASP.NET MVC Music Store 3.0

0.00/5 (No votes)
16 Apr 2014 1  
The article shows how to enhance the shopping cart of the Microsoft ASP.NET MVC Music Store 3.0 sample application by making the workflow more practical with a quantity update feature and unobtrusive data validations.

Introduction

The Music Store 3.0 sample application is an excellent tutorial for ASP.NET MVC 3.0. The shopping cart, however, doesn't follow the practical workflow and lacks some important features. For example, we need to go back to select the same album item and add it to cart to increase the quantity by one. Clicking the "Remove from cart" link each time deducts the quantity by one, not removing the selected cart album item. We need to repeat the same action multiple times to add or remove a particular album item having a quantity more than one. To correct the problems, we need a feature for editing and refreshing the quantity field on the page, which is a basic requirement for a shopping cart of any real online store.

Since I couldn't find any articles or discussions regarding the shopping cart quantity updating issue for the Music Store sample application, I tried to do the work based on the functional requirements listed below.

  • Clicking the "Remove from cart" link will remove the selected album item in the cart no matter what the quantity is.
  • Make the Quantity field editable.
  • Add a "Refresh quantity" link to perform album item count updates including database actions. The link should also perform the same item removal action when the Quantity field for a particular album item is empty or has a zero value.
  • Implement custom client-side jQuery unobtrusive validations and server-side error handling for the input field.

When running the application, the shopping cart screen looks like this.

1.png

Changes for Remove from Cart Link

We only need to remove some lines of code in the RemoveFromCart() function in the ShoppingCart class and $(".RemoveLink").click(function ()) in the JavaScript section on the shopping cart view. The lines to be removed are shown below with comment symbols plus double dashes. As a result, the unwanted behavior, deducting the count one at a time, is removed from the code.

In Models\ShoppingCart.cs:

public int RemoveFromCart(int id)
{
    // Get the cart 
    var cartItem = storeDB.Carts.Single(
        cart => cart.CartId == ShoppingCartId
        && cart.RecordId == id);

    int itemCount = 0;

    if (cartItem != null)
    {
        //--if (cartItem.Count > 1)
        //--{
        //--    cartItem.Count--;
        //--    itemCount = cartItem.Count;
        //--}
        //--else
        //--{
            storeDB.Carts.Remove(cartItem);
        //--}
        // Save changes 
        storeDB.SaveChanges();
    }
    return itemCount;
}

In Views\Index.cshtml:

$(".RemoveLink").click(function () {
    // Get the id from the link 
    var recordToDelete = $(this).attr("data-id");
    if (recordToDelete != '') {
        clearUpdateMessage();
        // Perform the ajax post 
        $.post("/ShoppingCart/RemoveFromCart", { "id": recordToDelete },
            function (data) {
                // Successful requests get here 
                // Update the page elements 
                //--if (data.ItemCount == 0) {
                $('#row-' + data. DeleteId).fadeOut('slow');
                //--} else {
                //--    $('#item-count-' + data.DeleteId).text(data.ItemCount);
                //--}
                $('#cart-total').text(data.CartTotal);
                $('#update-message').text(data.Message);
                $('#cart-status').text('Cart (' + data.CartCount + ')');
            });
    }

The "Remove from cart" link may not be important when we provide the "Refresh quantity" link with input value zero or null for the quantity, which performs the same action as the "Remove from cart" link.

Adding an Input TextBox for the Quantity Field

The shopping cart view is associated with the ShoppingCartViewModel that contains the collection of Cart objects in the data model. Since we need the input field for each cart item in the CartItems list, the structure is similar to a data grid with an editable column. We also need an index number to define the individual cart item. On the shopping cart view, we can change the @foreach() loop into the for() loop but I just want to keep the original code as much as possible.

  1. Add a local variable just above the @foreach() line:
  2. @{int ix = 0;}
  3. In the @foreach loop, replace the <td> tag holding Count data with the HTML Helper TextBoxFor(). We also need to adjust the text box width (default value 300px for the input text element set in site.css) and align the number characters to the right.
  4. Old code:

    <td id="item-count-@item.RecordId"> 
       @item.Count
    </td>

    New code:

    <td>
    @Html.TextBoxFor(model => model.CartItems[ix].Count, 
          new { style = "width:30px; text-align:right;" 
        })
    </td>
  5. Add code for matching the current index number as the last line in the foreach() loop, just before the closing curly bracket.
  6. i++;

Adding the Refresh Quantity Link

We need to obtain the input text box ID value and pass it to the AJAX call in addition to the cart item ID value. By default, the Razor engine automatically renders the list of element IDs by the format of "generic-list-object-name_index-number__object-date-property". Note that there are double underscore characters after the index number. In our case, the ID value of the first input element should be "CartItem_0__Count and the second "CartItem_1__Count", etc. There is also a particular format for the element "name" attribute but we do not need it here. If you want to customize the element ID values, you need to use @Html.TextBox() instead of @Html.TextBoxFor(). But in this case, you need to specify custom attributes in the third parameter of @Html.TextBox() for manually rendering the unobtrusive validation HTML.

We also need to define a custom attribute "txt-id" for passing the input element ID in the “Refresh quantity” link. Add the <a> tag just before the line of "Remove from cart" <a> tag:

<a href="#" class="RefreshQuantity" data-id="@item.RecordId" 
   txt-id="CartItems_@(ix)__Count">Refresh quantity</a>&nbsp;|&nbsp;

Adding a jQuery AJAX Function for Refreshing Quantity

We already know the data passed from the custom attributes: date-id and txt-id. Creating the client–side AJAX function for refreshing the quantity updates is easy. Just copy the click function for RemoveLink and do some modifications.

$(".RefreshQuantity").click(function () {
    // Get the id from the link 
    var recordToUpdate = $(this).attr("data-id");
    var countToUpdate = $("#" + $(this).attr("txt-id")).val();
    if (recordToUpdate != '') {        
        // Perform the ajax post 
        $.post("/ShoppingCart/UpdateCartCount", { "id": recordToUpdate, "cartCount": countToUpdate },
            function (data) {
                // Successful requests get here 
                // Update the page elements                        
                if (data.ItemCount == 0) {
                    $('#row-' + data. DeleteId).fadeOut('slow');
                }
                $('#update-message').text(data.Message);
                $('#cart-total').text(data.CartTotal);
                $('#cart-status').text('Cart (' + data.CartCount + ')');                
            });
    }

We declare a vaviable countToUpdate which holds the data from the user input and then passes it as the second JSON parameter "cartCount" for the AJAX post. Handling the callback data is the same as for the "Removing from cart" link.

Adding Server-side Functions for Refreshing Quantity

We do not change the data properties in any model this time. We just add functions to save the album item in the cart to the database and then return the data in JSON format for messaging purpose.

In Controllers\ShoppingCartController.cs:

[HttpPost]
public ActionResult UpdateCartCount(int id, int cartCount)
{
    // Get the cart 
    var cart = ShoppingCart.GetCart(this.HttpContext);

    // Get the name of the album to display confirmation 
    string albumName = storeDB.Carts
        .Single(item => item.RecordId == id).Album.Title;

    // Update the cart count 
    int itemCount = cart.UpdateCartCount(id, cartCount);

    //Prepare messages
    string msg = "The quantity of " + Server.HtmlEncode(albumName) +
            " has been refreshed in your shopping cart.";
    if (itemCount == 0) msg = Server.HtmlEncode(albumName) +
            " has been removed from your shopping cart.";
    //
    // Display the confirmation message 
    var results = new ShoppingCartRemoveViewModel
    {
        Message = msg,
        CartTotal = cart.GetTotal(),
        CartCount = cart.GetCount(),
        ItemCount = itemCount,
        DeleteId = id
    }; 
    return Json(results);
}

In Models\ShoppingCart.cs:

public int UpdateCartCount(int id, int cartCount)
{
    // Get the cart 
    var cartItem = storeDB.Carts.Single(
        cart => cart.CartId == ShoppingCartId
        && cart.RecordId == id);

    int itemCount = 0;

    if (cartItem != null)
    {
        if (cartCount > 0)
        {
            cartItem.Count = cartCount;
            itemCount = cartItem.Count;
        }
        else
        {
            storeDB.Carts.Remove(cartItem);
        }
        // Save changes 
        storeDB.SaveChanges();
    }
    return itemCount;
}

Our quantity input field accepts the value zero. If the value zero is passed from the cartCount parameter, the updating process will remove the cart item, the same action as the "Remove from cart" link. If you don't like this behavior and want to disallow zero value entry, you may change the range validation setting as mentioned in the next section.

So far so good. When running the application and adding multiple album items from Home page or the Genre menu to the shopping cart, we will get the quantity editable list on the Shopping Cart page. We can then change the quantity, and refresh or delete any album item in the cart, as the desired functionality (see the screenshot shown previously).

Adding Unobtrusive Validations and Server Error Handling

Before the editable quantity field is put into use, we need to add data validation approaches based on these rules.

  • The input value must be numeric.
  • The value range must be between 0 and 100.
  • An empty value is allowed but treated as value 0. "Field display-name is required" message will not be rendered.
  • The error message should be displayed on the top of the list or grid, not inside of it.

When running the application, the screens are shown as below.

2.png

3.png

MVC 3.0 comes with the nice feature of jQuery unobtrusive validations for data input but there are a few sample applications that show the implementation for a list of the same model objects (entities). Let’s do the unobtrusive validations for the list of same input fields here.

Firstly, add the ComponentModel and DataAnnotations namespace references and validation attributes to Cart.cs. The code in the file should be like this:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc; 

namespace MvcMusicStore.Models
{
    public class Cart
    {
        [Key]
        public int RecordId { get; set; }
        public string CartId { get; set; }
        public int AlbumId { get; set; }
        
        [Required(AllowEmptyStrings = true, ErrorMessage = " ")]
        [Range(0, 100, ErrorMessage = "Quantity must be between 0 and 100")]
        [DisplayName("Quantity")]
        public int Count { get; set; }        
        
        public System.DateTime DateCreated { get; set; }
        public virtual Album Album { get; set; }
    }
}

We set AllowEmptyStrings to true for the Required attribute. We then set ErrorMessage to contain one space character for the attribute. If setting it to an empty string (""), we will get the error "ErrorMessageString or ErrorMessageResourceName must be set, but not both". This occurs due mostly to a bug for setting the default ErrorMessage in the System.ComponentModel.DataAnnotations.RequiredAttribute class. When setting a custom message for the attribute, it does not allow an empty string. But placing a space character in the message will do the trick and disable the message display.

We then override the default display name "Count" with "Quantity" so that any default error message using the "Count" name will show "Quantity" instead.

When setting the Range attribute, MVC also sets the numeric checker and renders "The field display-name must be numeric" message automatically.

Next, let's change something in Views\ShoppingCart\index.cshtml and make the validations in effect.

  1. The Music Store 3.0 sample application already enables unobtrusive JavaScript using the setting in the site root web.config file but the jQuery library references are specified locally on each involved view, not in the global template Shared\_Layout.cshtml. Add the jQuery library references to Views\ShoppingCart\index.cshtml so that the beginning of the view should have these lines:
  2. @model MvcMusicStore.ViewModels.ShoppingCartViewModel           
    @{ 
        ViewBag.Title = "Shopping Cart";   
    } 
    
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
  3. Add code to include the page content in an HTML form. The validation will not work without having the HTML form.
  4. @using (Html.BeginForm())
    {
        @*All lines from <h3> tag to end are inside here*@
    }
  5. Place a <div> tag containing the code to display the validation error messages above the code line <div id="update-message"></div>. This will render a list of tags for error messages based on a list of album items currently in the cart. We want to display error messages on the top of the grid structure, not in each row in the column. Fortunately, the Razor engine and HTML Helper allow us to place the validation error messages in other places, not always around the input field areas.
  6. <div>
         @for (int i = 0; i < Model.CartItems.Count; i++)
         { 
            <p>
                 @Html.ValidationMessageFor(model => model.CartItems[i].Count)
            </p> 
         }
    </div>
  7. Add code into the jQuery $(".RefreshQuantity").click(function ()) to set the quantity to zero when input value is empty.
  8. $(".RefreshQuantity").click(function () {
        // Get the id from the link 
        var recordToUpdate = $(this).attr("data-id");
        
        //Set quantity number to 0 if input value is empty
        var countToUpdate = 0;
        if ($("#" + $(this).attr("txt-id")).val() !== '') {
            countToUpdate = $("#" + $(this).attr("txt-id")).val();
        }
        
       if (recordToUpdate != '') {
       - - - -

Finally, we need to manually handle the server-side validation errors. When the input value is above the maximum of the range and the client-side error message is displayed, clicking the "Refresh quantity" link a second time will send invalid data to the server and cause a server error "Validation failed for one or more entities". We need to catch the error and return a custom error message to the client-side. This is done by adding and modifying code in the UpdateCartCount() function of ShoppingCartController.cs. The revised function is shown below.

[HttpPost]
public ActionResult UpdateCartCount(int id, int cartCount)
{
    ShoppingCartRemoveViewModel results = null;
    try
    {
        // Get the cart 
        var cart = ShoppingCart.GetCart(this.HttpContext);

        // Get the name of the album to display confirmation 
        string albumName = storeDB.Carts
            .Single(item => item.RecordId == id).Album.Title;

        // Update the cart count 
        int itemCount = cart.UpdateCartCount(id, cartCount);

        //Prepare messages
        string msg = "The quantity of " + Server.HtmlEncode(albumName) +
                " has been refreshed in your shopping cart.";
        if (itemCount == 0) msg = Server.HtmlEncode(albumName) +
                " has been removed from your shopping cart.";
        //
        // Display the confirmation message 
        results = new ShoppingCartRemoveViewModel
        {
            Message = msg,
            CartTotal = cart.GetTotal(),
            CartCount = cart.GetCount(),
            ItemCount = itemCount,
            DeleteId = id
        };                
    }
    catch
    {
        results = new ShoppingCartRemoveViewModel
        {
            Message = "Error occurred or invalid input...",
            CartTotal = -1,
            CartCount = -1,
            ItemCount = -1,
            DeleteId = id
        };
    }
    return Json(results);
}

When a server validation error occurs, we don't need to pass data in the AJAX callback except the record ID (DeleteId, in this case) so we set the value of all other data fields to -1. In the AJAX callback function from Views\ShoppingCart\index.cshtml, we then check that only if data.ItemCount is not -1, should the cart-total and cart-status (for Cart Summary partial view) be processed.

function (data) {
    // Successful requests get here 
    // Update the page elements                        
    if (data.ItemCount == 0) {
        $('#row-' + data.DeleteId).fadeOut('slow');
    }
    $('#update-message').text(data.Message);
    
    //Only process the callback data when no server error occurs
    if (data.ItemCount != -1) {
        $('#cart-total').text(data.CartTotal);
        $('#cart-status').text('Cart (' + data.CartCount + ')');
    }
}

If we continue to click the "Refresh quantity" link without correcting the out-of-range data, the custom server error message is shown on the Shopping Cart page.

4.png

Fine Tune for Shopping Cart

At this point, there are still some minor issues that need to be fixed.

  1. When entering only one or more space characters, there is neither a validation error message display nor a process similar to null quantity occurring. We need to add a trim function to make it an empty string. Thus refreshing the shopping cart will remove the album item. The jQuery built-in trim function doesn't work for my IE browser so that I added a custom function to Views\ShoppingCart\index.cshtml.
  2. if (typeof String.prototype.trim !== 'function') {
        String.prototype.trim = function () {
            return this.replace(/^\s+|\s+$/g, '');
        }
    }

    Then we need to call the trim() function for checking the empty string in $(".RefreshQuantity").click(function ()). The updated code is like this:

    var countToUpdate = 0;
    if ($("#" + $(this).attr("txt-id")).val().trim() !== '') {
        countToUpdate = $("#" + $(this).attr("txt-id")).val();
    }
  3. The client-side validation message can automatically be cleared when re-entering the correct number into the Quantity field. But the server-side error message display doesn't have such a behavior. We can add a JavaScript function to Views\ShoppingCart\index.cshtml.
  4. function clearUpdateMessage() {
        // Reset update-message area
        $('#update-message').text('');
    }

    The function is called from the onkeyup and onchange event triggers of the input element. We just add attribute items into the second parameter of @Html.TextBoxFor().

    @Html.TextBoxFor(model => model.CartItems[ix].Count, 
               new { style = "width:30px; text-align:right;",
                    onkeyup = "clearUpdateMessage();",
                    onchange = "clearUpdateMessage();"                          
        })
  5. The HTML encoded message string returned from the AJAX call cannot automatically be decoded when rendered from jQuery code. We need to manually handle the issue for some special characters, for example, the "Górecki" shown on the first screenshot (otherwise "G&#243;recki" would be displayed). We can use a simple JavaScript function to decode the HTML entities.
  6. function htmlDecode(value) {
        if (value) {
            return $('<div />').html(value).text();
        }
        else {
            return '';
        }
    }

    We can then call the htmlDecode function for decoding the data.message string in $(".RefreshQuantity").click(function ()).

    $('#update-message').text(htmlDecode(data.Message));

Conclusion

By adding the quantity update feature into the shopping cart for the MVC Music Store sample application, we not only make the business logic for the shopping car more realistic but also dig more into the jQuery/AJAX data updates and unobtrusive input validations, especially for the ViewModel that contains the input fields in a list of model objects.

Happy coding with the ASP.NET MVC!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here