Partilhar via


Parte 8: Carrinho de compras com atualizações do Ajax

por Jon Galloway

O MVC Music Store é um aplicativo de tutorial que apresenta e explica passo a passo como usar ASP.NET MVC e Visual Studio para desenvolvimento na Web.

A MVC Music Store é uma implementação leve de loja de exemplo que vende álbuns de música online e implementa a administração básica do site, a entrada do usuário e a funcionalidade do carrinho de compras.

Esta série de tutoriais detalha todas as etapas executadas para criar o aplicativo de exemplo ASP.NET MVC Music Store. A parte 8 cobre o Carrinho de Compras com o Ajax Atualizações.

Permitiremos que os usuários coloquem álbuns em seu carrinho sem se registrarem, mas eles precisarão se registrar como convidados para concluir o check-out. O processo de compra e check-out será separado em dois controladores: um Controlador do ShoppingCart que permite adicionar anonimamente itens a um carrinho e um Controlador de Checkout que manipula o processo de check-out. Começaremos com o Carrinho de Compras nesta seção e, em seguida, criaremos o processo de Checkout na seção a seguir.

Adicionando as classes de modelo Cart, Order e OrderDetail

Nossos processos de Carrinho de Compras e Checkout usarão algumas novas classes. Clique com o botão direito do mouse na pasta Modelos e adicione uma classe Cart (Cart.cs) com o código a seguir.

using System.ComponentModel.DataAnnotations;
 
namespace MvcMusicStore.Models
{
    public class Cart
    {
        [Key]
        public int      RecordId    { get; set; }
        public string   CartId      { get; set; }
        public int      AlbumId     { get; set; }
        public int      Count       { get; set; }
        public System.DateTime DateCreated { get; set; }
        public virtual Album Album  { get; set; }
    }
}

Essa classe é muito semelhante a outras que usamos até agora, com exceção do atributo [Key] para a propriedade RecordId. Nossos itens cart terão um identificador de cadeia de caracteres chamado CartID para permitir compras anônimas, mas a tabela inclui uma chave primária de inteiro chamada RecordId. Por convenção, o Entity Framework Code-First espera que a chave primária de uma tabela chamada CartId seja CartId ou ID, mas podemos facilmente substituir isso por meio de anotações ou código, se desejarmos. Este é um exemplo de como podemos usar as convenções simples no Entity Framework Code-First quando elas nos servem, mas não somos restringidas por elas quando não o fazem.

Em seguida, adicione uma classe Order (Order.cs) com o código a seguir.

using System.Collections.Generic;
 
namespace MvcMusicStore.Models
{
    public partial class Order
    {
        public int    OrderId    { get; set; }
        public string Username   { get; set; }
        public string FirstName  { get; set; }
        public string LastName   { get; set; }
        public string Address    { get; set; }
        public string City       { get; set; }
        public string State      { get; set; }
        public string PostalCode { get; set; }
        public string Country    { get; set; }
        public string Phone      { get; set; }
        public string Email      { get; set; }
        public decimal Total     { get; set; }
        public System.DateTime OrderDate      { get; set; }
        public List<OrderDetail> OrderDetails { get; set; }
    }
}

Essa classe acompanha as informações de resumo e entrega de um pedido. Ele ainda não será compilado, pois tem uma propriedade de navegação OrderDetails que depende de uma classe que ainda não criamos. Vamos corrigir isso agora adicionando uma classe chamada OrderDetail.cs, adicionando o código a seguir.

namespace MvcMusicStore.Models
{
    public class OrderDetail
    {
        public int OrderDetailId { get; set; }
        public int OrderId { get; set; }
        public int AlbumId { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public virtual Album Album { get; set; }
        public virtual Order Order { get; set; }
    }
}

Faremos uma última atualização para nossa classe MusicStoreEntities para incluir DbSets que expõem essas novas classes de Modelo, incluindo também um Artista> DbSet<. A classe MusicStoreEntities atualizada aparece como abaixo.

using System.Data.Entity;
 
namespace MvcMusicStore.Models
{
    public class MusicStoreEntities : DbContext
    {
        public DbSet<Album>     Albums  { get; set; }
        public DbSet<Genre>     Genres  { get; set; }
        public DbSet<Artist>    Artists {
get; set; }
        public DbSet<Cart>     
Carts { get; set; }
        public DbSet<Order>     Orders
{ get; set; }
        public DbSet<OrderDetail>
OrderDetails { get; set; }
    }
}

Gerenciando a lógica de negócios do Carrinho de Compras

Em seguida, criaremos a classe ShoppingCart na pasta Modelos. O modelo ShoppingCart manipula o acesso de dados à tabela Cart. Além disso, ele manipulará a lógica de negócios para adicionar e remover itens do carrinho de compras.

Como não queremos exigir que os usuários se inscrevam em uma conta apenas para adicionar itens ao carrinho de compras, atribuiremos aos usuários um identificador exclusivo temporário (usando um GUID ou identificador globalmente exclusivo) quando eles acessarem o carrinho de compras. Armazenaremos essa ID usando a classe ASP.NET Session.

Observação: a sessão de ASP.NET é um local conveniente para armazenar informações específicas do usuário que expirarão após a saída do site. Embora o uso indevido do estado de sessão possa ter implicações de desempenho em sites maiores, nosso uso leve funcionará bem para fins de demonstração.

A classe ShoppingCart expõe os seguintes métodos:

AddToCart usa um Álbum como um parâmetro e o adiciona ao carrinho do usuário. Como a tabela Cart rastreia a quantidade de cada álbum, ela inclui lógica para criar uma nova linha, se necessário ou apenas incrementar a quantidade se o usuário já tiver pedido uma cópia do álbum.

RemoveFromCart pega uma ID do Álbum e a remove do carrinho do usuário. Se o usuário tiver apenas uma cópia do álbum no carrinho, a linha será removida.

EmptyCart remove todos os itens do carrinho de compras de um usuário.

GetCartItems recupera uma lista de CartItems para exibição ou processamento.

GetCount recupera um número total de álbuns que um usuário tem em seu carrinho de compras.

GetTotal calcula o custo total de todos os itens no carrinho.

CreateOrder converte o carrinho de compras em um pedido durante a fase de check-out.

GetCart é um método estático que permite que nossos controladores obtenham um objeto cart. Ele usa o método GetCartId para lidar com a leitura do CartId da sessão do usuário. O método GetCartId requer o HttpContextBase para que ele possa ler o CartId do usuário da sessão do usuário.

Aqui está a classe ShoppingCart completa:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MvcMusicStore.Models
{
    public partial class ShoppingCart
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        string ShoppingCartId { get; set; }
        public const string CartSessionKey = "CartId";
        public static ShoppingCart GetCart(HttpContextBase context)
        {
            var cart = new ShoppingCart();
            cart.ShoppingCartId = cart.GetCartId(context);
            return cart;
        }
        // Helper method to simplify shopping cart calls
        public static ShoppingCart GetCart(Controller controller)
        {
            return GetCart(controller.HttpContext);
        }
        public void AddToCart(Album album)
        {
            // Get the matching cart and album instances
            var cartItem = storeDB.Carts.SingleOrDefault(
                c => c.CartId == ShoppingCartId 
                && c.AlbumId == album.AlbumId);
 
            if (cartItem == null)
            {
                // Create a new cart item if no cart item exists
                cartItem = new Cart
                {
                    AlbumId = album.AlbumId,
                    CartId = ShoppingCartId,
                    Count = 1,
                    DateCreated = DateTime.Now
                };
                storeDB.Carts.Add(cartItem);
            }
            else
            {
                // If the item does exist in the cart, 
                // then add one to the quantity
                cartItem.Count++;
            }
            // Save changes
            storeDB.SaveChanges();
        }
        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;
        }
        public void EmptyCart()
        {
            var cartItems = storeDB.Carts.Where(
                cart => cart.CartId == ShoppingCartId);
 
            foreach (var cartItem in cartItems)
            {
                storeDB.Carts.Remove(cartItem);
            }
            // Save changes
            storeDB.SaveChanges();
        }
        public List<Cart> GetCartItems()
        {
            return storeDB.Carts.Where(
                cart => cart.CartId == ShoppingCartId).ToList();
        }
        public int GetCount()
        {
            // Get the count of each item in the cart and sum them up
            int? count = (from cartItems in storeDB.Carts
                          where cartItems.CartId == ShoppingCartId
                          select (int?)cartItems.Count).Sum();
            // Return 0 if all entries are null
            return count ?? 0;
        }
        public decimal GetTotal()
        {
            // Multiply album price by count of that album to get 
            // the current price for each of those albums in the cart
            // sum all album price totals to get the cart total
            decimal? total = (from cartItems in storeDB.Carts
                              where cartItems.CartId == ShoppingCartId
                              select (int?)cartItems.Count *
                              cartItems.Album.Price).Sum();

            return total ?? decimal.Zero;
        }
        public int CreateOrder(Order order)
        {
            decimal orderTotal = 0;
 
            var cartItems = GetCartItems();
            // Iterate over the items in the cart, 
            // adding the order details for each
            foreach (var item in cartItems)
            {
                var orderDetail = new OrderDetail
                {
                    AlbumId = item.AlbumId,
                    OrderId = order.OrderId,
                    UnitPrice = item.Album.Price,
                    Quantity = item.Count
                };
                // Set the order total of the shopping cart
                orderTotal += (item.Count * item.Album.Price);
 
                storeDB.OrderDetails.Add(orderDetail);
 
            }
            // Set the order's total to the orderTotal count
            order.Total = orderTotal;
 
            // Save the order
            storeDB.SaveChanges();
            // Empty the shopping cart
            EmptyCart();
            // Return the OrderId as the confirmation number
            return order.OrderId;
        }
        // We're using HttpContextBase to allow access to cookies.
        public string GetCartId(HttpContextBase context)
        {
            if (context.Session[CartSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
                {
                    context.Session[CartSessionKey] =
                        context.User.Identity.Name;
                }
                else
                {
                    // Generate a new random GUID using System.Guid class
                    Guid tempCartId = Guid.NewGuid();
                    // Send tempCartId back to client as a cookie
                    context.Session[CartSessionKey] = tempCartId.ToString();
                }
            }
            return context.Session[CartSessionKey].ToString();
        }
        // When a user has logged in, migrate their shopping cart to
        // be associated with their username
        public void MigrateCart(string userName)
        {
            var shoppingCart = storeDB.Carts.Where(
                c => c.CartId == ShoppingCartId);
 
            foreach (Cart item in shoppingCart)
            {
                item.CartId = userName;
            }
            storeDB.SaveChanges();
        }
    }
}

ViewModels

Nosso controlador de carrinho de compras precisará comunicar algumas informações complexas aos seus modos de exibição que não são mapeados de forma limpa para nossos objetos Model. Não queremos modificar nossos Modelos para atender às nossas exibições; As classes de modelo devem representar nosso domínio, não a interface do usuário. Uma solução seria passar as informações para nossos Modos de Exibição usando a classe ViewBag, como fizemos com as informações suspensas do Gerenciador de Lojas, mas passar muitas informações via ViewBag fica difícil de gerenciar.

Uma solução para isso é usar o padrão ViewModel . Ao usar esse padrão, criamos classes fortemente tipadas que são otimizadas para nossos cenários de exibição específicos e que expõem propriedades para os valores dinâmicos/conteúdo necessários para nossos modelos de exibição. Nossas classes de controlador podem preencher e passar essas classes com otimização de exibição para nosso modelo de exibição a ser usado. Isso permite a segurança de tipo, a verificação em tempo de compilação e o editor IntelliSense nos modelos de exibição.

Criaremos dois Modelos de Exibição para uso em nosso controlador de Carrinho de Compras: o ShoppingCartViewModel armazenará o conteúdo do carrinho de compras do usuário e o ShoppingCartRemoveViewModel será usado para exibir informações de confirmação quando um usuário remover algo do carrinho.

Vamos criar uma nova pasta ViewModels na raiz do nosso projeto para manter as coisas organizadas. Clique com o botão direito do mouse no projeto, selecione Adicionar/Nova Pasta.

Captura de tela da janela do projeto mostrando o menu de clique com o botão direito do mouse com as opções Adicionar e Nova Pasta realçadas em amarelo.

Nomeie a pasta ViewModels.

Captura de tela do Gerenciador de Soluções mostrando a pasta recém-criada e recém-nomeada, Exibir Modelos, realçada com uma caixa preta.

Em seguida, adicione a classe ShoppingCartViewModel na pasta ViewModels. Ele tem duas propriedades: uma lista de itens do Carrinho e um valor decimal para manter o preço total de todos os itens no carrinho.

using System.Collections.Generic;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartViewModel
    {
        public List<Cart> CartItems { get; set; }
        public decimal CartTotal { get; set; }
    }
}

Agora adicione o ShoppingCartRemoveViewModel à pasta ViewModels, com as quatro propriedades a seguir.

namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartRemoveViewModel
    {
        public string Message { get; set; }
        public decimal CartTotal { get; set; }
        public int CartCount { get; set; }
        public int ItemCount { get; set; }
        public int DeleteId { get; set; }
    }
}

O controlador de carrinho de compras

O controlador carrinho de compras tem três main finalidades: adicionar itens a um carrinho, remover itens do carrinho e exibir itens no carrinho. Ele usará as três classes que acabamos de criar: ShoppingCartViewModel, ShoppingCartRemoveViewModel e ShoppingCart. Assim como no StoreController e storeManagerController, adicionaremos um campo para manter uma instância de MusicStoreEntities.

Adicione um novo controlador carrinho de compras ao projeto usando o modelo controlador Vazio.

Captura de tela da janela Adicionar Controlador com o Controlador do Carrinho de Compras no campo Nome do controlador e realçado em azul.

Aqui está o Controlador do ShoppingCart completo. As ações Index e Add Controller devem parecer muito familiares. As ações do controlador Remove e CartSummary tratam de dois casos especiais, que discutiremos na seção a seguir.

using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
using MvcMusicStore.ViewModels;
 
namespace MvcMusicStore.Controllers
{
    public class ShoppingCartController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        //
        // GET: /ShoppingCart/
        public ActionResult Index()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            // Set up our ViewModel
            var viewModel = new ShoppingCartViewModel
            {
                CartItems = cart.GetCartItems(),
                CartTotal = cart.GetTotal()
            };
            // Return the view
            return View(viewModel);
        }
        //
        // GET: /Store/AddToCart/5
        public ActionResult AddToCart(int id)
        {
            // Retrieve the album from the database
            var addedAlbum = storeDB.Albums
                .Single(album => album.AlbumId == id);
 
            // Add it to the shopping cart
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            cart.AddToCart(addedAlbum);
 
            // Go back to the main store page for more shopping
            return RedirectToAction("Index");
        }
        //
        // AJAX: /ShoppingCart/RemoveFromCart/5
        [HttpPost]
        public ActionResult RemoveFromCart(int id)
        {
            // Remove the item from 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;
 
            // Remove from cart
            int itemCount = cart.RemoveFromCart(id);
 
            // Display the confirmation message
            var results = new ShoppingCartRemoveViewModel
            {
                Message = Server.HtmlEncode(albumName) +
                    " has been removed from your shopping cart.",
                CartTotal = cart.GetTotal(),
                CartCount = cart.GetCount(),
                ItemCount = itemCount,
                DeleteId = id
            };
            return Json(results);
        }
        //
        // GET: /ShoppingCart/CartSummary
        [ChildActionOnly]
        public ActionResult CartSummary()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            ViewData["CartCount"] = cart.GetCount();
            return PartialView("CartSummary");
        }
    }
}

Ajax Atualizações com jQuery

Em seguida, criaremos uma página índice de carrinho de compras fortemente tipada para o ShoppingCartViewModel e usaremos o modelo Modo de Exibição de Lista usando o mesmo método de antes.

Captura de tela da janela Adicionar Exibição mostrando o campo Nome da exibição, o mecanismo de exibição, a classe de modelo e as listas suspensas Scaffold e o seletor Usar um arquivo de layout.

No entanto, em vez de usar um Html.ActionLink para remover itens do carrinho, usaremos jQuery para "conectar" o evento de clique para todos os links nessa exibição que têm a classe HTML RemoveLink. Em vez de postar o formulário, esse manipulador de eventos de clique fará apenas um retorno de chamada AJAX para nossa ação do controlador RemoveFromCart. O RemoveFromCart retorna um resultado serializado JSON, que nosso retorno de chamada jQuery analisa e executa quatro atualizações rápidas para a página usando jQuery:

    1. Remove o álbum excluído da lista
    1. Atualizações a contagem de carrinhos no cabeçalho
    1. Exibe uma mensagem de atualização para o usuário
    1. Atualizações o preço total do carrinho

Como o cenário de remoção está sendo tratado por um retorno de chamada do Ajax na exibição Índice, não precisamos de uma exibição adicional para a ação RemoveFromCart. Aqui está o código completo para a exibição /ShoppingCart/Index:

@model MvcMusicStore.ViewModels.ShoppingCartViewModel
@{
    ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js"
type="text/javascript"></script>
<script type="text/javascript">
    $(function () {
        // Document.ready -> link up remove event handler
        $(".RemoveLink").click(function () {
            // Get the id from the link
            var recordToDelete = $(this).attr("data-id");
            if (recordToDelete != '') {
                // 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 + ')');
                    });
            }
        });
    });
</script>
<h3>
    <em>Review</em> your cart:
 </h3>
<p class="button">
    @Html.ActionLink("Checkout
>>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
    <tr>
        <th>
            Album Name
        </th>
        <th>
            Price (each)
        </th>
        <th>
            Quantity
        </th>
        <th></th>
    </tr>
    @foreach (var item in
Model.CartItems)
    {
        <tr id="row-@item.RecordId">
            <td>
                @Html.ActionLink(item.Album.Title,
"Details", "Store", new { id = item.AlbumId }, null)
            </td>
            <td>
                @item.Album.Price
            </td>
            <td id="item-count-@item.RecordId">
                @item.Count
            </td>
            <td>
                <a href="#" class="RemoveLink"
data-id="@item.RecordId">Remove
from cart</a>
            </td>
        </tr>
    }
    <tr>
        <td>
            Total
        </td>
        <td>
        </td>
        <td>
        </td>
        <td id="cart-total">
            @Model.CartTotal
        </td>
    </tr>
</table>

Para testar isso, precisamos ser capazes de adicionar itens ao nosso carrinho de compras. Atualizaremos nossa exibição Detalhes da Loja para incluir um botão "Adicionar ao carrinho". Enquanto estamos nisso, podemos incluir algumas das informações adicionais do Álbum que adicionamos desde a última vez que atualizamos esta exibição: Gênero, Artista, Preço e Arte do Álbum. O código de exibição Detalhes da Loja atualizado é exibido conforme mostrado abaixo.

@model MvcMusicStore.Models.Album
@{
    ViewBag.Title = "Album - " + Model.Title;
 }
<h2>@Model.Title</h2>
<p>
    <img alt="@Model.Title"
src="@Model.AlbumArtUrl" />
</p>
<div id="album-details">
    <p>
        <em>Genre:</em>
        @Model.Genre.Name
    </p>
    <p>
        <em>Artist:</em>
        @Model.Artist.Name
    </p>
    <p>
        <em>Price:</em>
        @String.Format("{0:F}",
Model.Price)
    </p>
    <p class="button">
        @Html.ActionLink("Add to
cart", "AddToCart", 
        "ShoppingCart", new { id = Model.AlbumId }, "")
    </p>
</div>

Agora podemos clicar na loja e testar a adição e remoção de Álbuns de e para nosso carrinho de compras. Execute o aplicativo e navegue até o Índice da Loja.

Captura de tela da janela da Music Store mostrando os detalhes de gênero definidos de todos os dados do álbum inseridos no banco de dados.

Em seguida, clique em um Gênero para exibir uma lista de álbuns.

Captura de tela da janela da Music Store mostrando a lista de álbuns associados ao gênero Disco no banco de dados do álbum.

Clicar em um título de Álbum agora mostra nossa exibição atualizada de Detalhes do Álbum, incluindo o botão "Adicionar ao carrinho".

Captura de tela da janela da Loja de Músicas mostrando a exibição de Detalhes do Álbum atualizada e o botão Adicionar ao carrinho.

Clicar no botão "Adicionar ao carrinho" mostra nossa exibição Índice do Carrinho de Compras com a lista de resumo do carrinho de compras.

Captura de tela da janela da Loja de Música mostrando a exibição Carrinho de Compras com uma lista de resumo de todos os itens no carrinho.

Depois de carregar seu carrinho de compras, você pode clicar no link Remover do carrinho para ver a atualização do Ajax no carrinho de compras.

Captura de tela da janela da Loja de Música mostrando o modo de exibição Carrinho de Compras com o álbum removido da lista de resumo.

Criamos um carrinho de compras funcional que permite que usuários não registrados adicionem itens ao carrinho. Na seção a seguir, permitiremos que eles se registrem e concluam o processo de check-out.