ASP .NET - MVC Music Store - Cesta de compras com atualizações Ajax


Neste tutorial vamos implementar a cesta de compras e usar os recursos do AJAX.

Vamos permitir que os usuários coloquem itens em sua cesta de compras sem se registrar, mas eles vão precisar se registrar como convidados para fechar o pedido e fazer o pagamento, ou seja, fazer o checkout.

O processo de compra e checkout serão separados em dois controladores:

  1. O controlador CestaCompras que permite que um usuário anônimo adicione itens a uma cesta;
  2. O controlador Checkout que lida com o processo de encerramento das compras (checkout).

Vamos começar com a cesta de compras e a seguir construir o encerramento e o pagamento.

Adicionando as classes Cesta , Pedido e DetalhesPedido no Modelo de classes (Model)

Nossos processos de cesta de compras e Checkout irão fazer uso de algumas novas classes que serão necessárias para dar suporte ao processo de selecionar itens e fazer o pagamento.Vamos incluir estas classes na pasta Models do projeto.

Clique com o botão direito do mouse sobre a pasta Models e a seguir selecione Add -> Class;

A seguir selecione o template Class e informe o nome Cesta.vb e clique no botão Add;

A seguir digite o código abaixo nesta classe:

Imports System.ComponentModel.DataAnnotations

Public Class Cesta

  
 <Key> _
    Public Property RegistroId() As Integer

    Public Property CestaId() As String
    Public Property AlbumId() As Integer
    Public Property Contador() As Integer
    Public Property DateCriacao() As System.DateTime
    Public Overridable Property Album() As Album
    Private m_Album As Album

End Class

A única novidade nesta classe é o atributo <Key> para a propriedade RegistroId. Nossa cesta de itens terá um identificador string chamado CestaId para permitir o acesso anônimo às compras, mas a classe incluí um "chave primária" chamada RegistroId. Por convenção, o Entity Framework Code-First espera que a chave primária para a tabela chamada Cesta seja CestaId ou ID, mas nós podemos alterar este comportamento usando data annotations via código.

Neste exemplo estamos usando o atributo <Key> para indicar o campo que será definido como a chave primária na tabela Cesta.

Este é um exemplo de como podemos usar as convenções do EF Code First e de como podemos também sobrescrever o comportamento definido por padrão.

Vamos agora repetir o procedimento adotado acima e incluir uma classe chamada Pedido (Pedido.vb) com o seguinte código:

Imports System.Collections.Generic

Public Class Pedido
    Public Property PedidoId() As Integer
    Public Property Usuario() As String
    Public Property Nome() As String
    Public Property Sobrenome() As String
    Public Property Endereco() As String
    Public Property Cidade() As String
    Public Property Estado() As String
    Public Property CodigoPostal() As String
    Public Property Pais() As String
    Public Property Telefone() As String
    Public Property Email() As String
    Public Property Total() As Decimal
    Public Property PedidoData() As System.DateTime
    Public Property PedidoDetalhes() As List(Of PedidoDetalhe)
End Class

Esta classe trata do resumo e entrega de informações para um pedido. Ele não irá compilar ainda, porque ela tem uma propriedade de navegação chamada PedidoDetalhe que depende de uma classe que ainda não criamos. Vamos corrigir isso adicionando uma classe chamada PedidoDetalhe.vb na pasta Models contendo o seguinte código:

Public Class PedidoDetalhe

    Public Property PedidoDetalheId() As Integer
    Public Property PedidoId() As Integer
    Public Property AlbumId() As Integer
    Public Property Quantidade() As Integer
    Public Property PrecoUnitario() As Decimal
    Public Overridable Property Album() As Album
    Public Overridable Property Pedido() As Pedido

End Class

Para concluir temos que atualizar a nossa classe MusicStoreEntities para incluir os DbSets que expões essas novas classes. O código da classe deverá ficar conforme abaixo:

Imports System.Data.Entity

Imports MvcMusicStore

Public Class MusicStoreEntities
    Inherits DbContext

    Public Property Albuns() As DbSet(Of Album)
    Public Property Generos() As DbSet(Of Genero)
    Public Property Artistas As DbSet(Of Artista)
    '
    Public Property Cestas() As DbSet(Of Cesta)
    Public Property Pedidos() As DbSet(Of Pedido)
    Public Property PedidoDetalhes As DbSet(Of PedidoDetalhe)

End Class

Gerenciando a lógica de negócio da cesta de compras

A seguir, vamos criar a classe CestaCompras na pasta Models. A classe CestaCompras manipula o acesso aos dados da tabela Cesta. Além disso, ela vai lidar com a lógica de negócios para adicionar e remover itens da cesta de compras.

Como nós não queremos exigir que os usuários se inscrevam em uma conta apenas para adicionar itens a cesta de compras, vamos atribuir aos usuários um identificador temporário exclusivo (usando um GUID, ou identificador exclusivo) quando ele acessar o carrinho de compras. Vamos guardar essa identificação usando a classe Session da ASP .NET.

Nota: A sessão ASP.NET é um local conveniente para armazenar informações específicas do usuário, que expira depois de deixar o site. Embora o uso indevido da sessão possa ter implicações de desempenho em sites maiores, o uso comedido irá funcionar bem para fins de demonstração.

A classe CestaCompras expõe os métodos a seguir:

  1. AdicionarNaCesta - leva um item (álbum) como um parâmetro e adiciona na cesta do usuário. Desde que a tabela Cesta rastreia uma quantidade para cada item(álbum), ela inclui a lógica para criar uma nova linha se necessário ou apenas incrementar a quantidade, se o usuário já encomendou uma cópia do item (álbum).
  2. RemoverDaCesta - pega um ID do item e remove-o do carrinho do usuário. Se o usuário só tinha uma cópia do item em seu carrinho, a linha é removida.
  3. AdicionarNaCesta - leva um item(álbum) como um parâmetro e adiciona a cesta do usuário. Desde que a tabela cesta rastreia uma quantidade para cada item(álbum), ela inclui a lógica para criar uma nova linha se necessário ou apenas incrementar a quantidade, se o usuário já encomendou uma cópia do item.
  4. EsvaziaCesta remove todos os itens da cesta de compras do usuário.
  5. GetItemsDaCesta - recupera uma lista de Itens da cesta para exibição ou processamento.
  6. GetContador - recupera um número total de álbuns que um usuário tem em sua Cesta de compras.
  7. GetTotal - calcula o custo total de todos os itens na cesta
  8. CriaPedido - converte a cesta de compras para um pedido durante a fase de checkout.
  9. GetCesta - é um método estático que permite aos nossos controladores obter um objeto cesta. Ele utiliza o método GetCestaId para lidar com a leitura da Cesta da sessão do usuário. O método requer um HttpContextBase de modo que possa ler o valor de CestaId do usuário da sessão do usuário;

Clique com o botão direito do mouse sobre a pasta Models e selecione Add->Class informando o nome CestaCompras.vb e informando o código abaixo nesta classe:

Imports System.Collections.Generic
Imports System.Linq
Imports System.Web
Imports System.Web.Mvc

Public Class CestaCompras

    Private storeDB As New MusicStoreEntities()

    Private Property CestaComprasId As String
    Public Const CestaSessionKey As String = "CestaId"

    Public Shared Function GetCesta(context As HttpContextBase) As CestaCompras
        Dim cesta = New CestaCompras()
        cesta.CestaComprasId = cesta.GetCestaId(context)
        Return cesta
    End Function

    ' Método Helper para simplificar as chamadas a cesta de compras
    Public Shared Function GetCesta(controller As Controller) As CestaCompras
        Return GetCesta(controller.HttpContext)
    End Function

    Public Sub AdicionarNaCesta(album As Album)
        ' Obtem as instâncias de cesta e album
        Dim cestaItem = storeDB.Cestas.SingleOrDefault(Function(c) c.CestaId = CestaComprasId AndAlso c.AlbumId = album.AlbumId)

        If cestaItem Is Nothing Then
            ' Cria um novo item na cesta se não existir itens
            cestaItem = New Cesta() With { _
                 .AlbumId = album.AlbumId, _
                 .CestaId = CestaComprasId, _
                 .Contador = 1, _
                 .DateCriacao = DateTime.Now _
            }
            storeDB.Cestas.Add(cestaItem)
        Else
            ' Ise o item existe na cesta então incrementa a quantidade
            cestaItem.Contador += 1
        End If
        ' Salva as alterações
        storeDB.SaveChanges()
    End Sub

    Public Function RemoveDaCesta(id As Integer) As Integer
        ' Pega a cesta
        Dim cestaItem = storeDB.Cestas.Single(Function(cesta) cesta.CestaId = CestaComprasId AndAlso cesta.RegistroId = id)
        Dim contaItem As Integer = 0
        If cestaItem IsNot Nothing Then
            If cestaItem.Contador > 1 Then
                cestaItem.Contador -= 1
                contaItem = cestaItem.Contador
            Else
                storeDB.Cestas.Remove(cestaItem)
            End If
            ' Salva as alterações
            storeDB.SaveChanges()
        End If
        Return contaItem
    End Function

    Public Sub EsvaziaCesta()
        Dim cestaItens = storeDB.Cestas.Where(Function(cesta) cesta.CestaId = CestaComprasId)
        For Each cestaItem In cestaItens
            storeDB.Cestas.Remove(cestaItem)
        Next
        ' Salva as alterações
        storeDB.SaveChanges()
    End Sub

    Public Function GetItemsDaCesta() As List(Of Cesta)
        Return storeDB.Cestas.Where(Function(cesta) cesta.CestaId = CestaComprasId).ToList()
    End Function

    Public Function GetContador() As Integer
        'Pega o contador de cada item na cesta e soma
        Dim conta As System.Nullable(Of Integer) = (From cestaItens In storeDB.Cestas _
                                                    Where cestaItens.CestaId = CestaComprasId _
                                                    Select CType(cestaItens.Contador, System.Nullable(Of Integer))).Sum()
        ' Retorna 0 se as entradas forem nulas
        Return If(conta, 0)
    End Function

    Public Function GetTotal() As Decimal
        ' Multiplica o preco do album pelo contador do album
        ' para obter o preco atual de cada item dos albuns na cesta
        ' soma todas os precos dos albuns para obter o total da esta
         Dim total As System.Nullable(Of Decimal) = (From cartItems In storeDB.Cestas _
                                             Where cartItems.CestaId = CestaComprasId _
                                             Select CType(cartItems.Contador, System.Nullable(Of Integer)) * cartItems.Album.Preco).Sum()
        Return If(total, Decimal.Zero)
    End Function

    Public Function CriaPedido(pedido As Pedido) As Integer
        Dim pedidoTotal As Decimal = 0

        Dim cestaItens = GetItemsDaCesta()
        ' Percorre os itens na cesta adicionando os detalhes do pedido para cada um
        For Each item In cestaItens
            Dim _pedidoDetalhe = New PedidoDetalhe() With { _
                .AlbumId = item.AlbumId, _
                .PedidoId = pedido.PedidoId, _
                .PrecoUnitario = item.Album.Preco, _
                .Quantidade = item.Contador _
            }
            ' Define o total do pedido na cesta de compras
            pedidoTotal += (item.Contador * item.Album.Preco)
            storeDB.PedidoDetalhes.Add(_pedidoDetalhe)
        Next
        ' Define o total do pedido para o contador pedidoTotal
        pedido.Total = pedidoTotal

        ' Salva o pedido
        storeDB.SaveChanges()
        ' esvazia a cesta
        EsvaziaCesta()
        ' retorna o pedidoid como o numero de confirmação
        Return pedido.PedidoId
    End Function

    ' Estamos usando HttpContextBase para permitir o acesso aos cookies
    Public Function GetCestaId(context As HttpContextBase) As String
        If context.Session(CestaSessionKey) Is Nothing Then
            If Not String.IsNullOrWhiteSpace(context.User.Identity.Name) Then
                context.Session(CestaSessionKey) = context.User.Identity.Name
            Else
                ' Gera um novo GUID randomico usando System.Guid class
                Dim tempCestaId As Guid = Guid.NewGuid()
                ' Envia um tempCestaID de volta ao cliente como um cookie
                context.Session(CestaSessionKey) = tempCestaId.ToString()
            End If
        End If
        Return context.Session(CestaSessionKey).ToString()
    End Function

    ' Quando o usuário que estiver logado migrar sua cesta para um autenticado com seu nome de usuário
    Public Sub MigraCesta(usuario As String)
        Dim _cestaCompras = storeDB.Cestas.Where(Function(c) c.CestaId = CestaComprasId)
        For Each item In _cestaCompras
            item.CestaId = usuario
        Next
        storeDB.SaveChanges()
    End Sub

End Class

Definindo a ViewModel

Nosso controlador de compras vai precisar comunicar muita informação complexa para suas views que não estão mapeadas de forma clara para os nossos objetos de modelo. Nós não queremos modificar nossos modelos para atender às nossas views; as classes do modelo devem representar o nosso domínio, não a interface do usuário.

Uma solução seria passar a informação para nossas views usando a classe ViewBag, mas passar um monte de informações via ViewBag fica difícil de gerenciar.

Outra solução para esse problema é usar uma ViewModel. Ao usar este padrão criamos classes fortemente tipados que são otimizadas para nossos cenários views específicos, e que expõem propriedades para os valores/conteudos dinâmicos necessários por nossos templates views.

Obs: A ViewModel contém toda lógica de interface e a referência ao modelo, de modo a atuar como modelo para a View;

Nossas classes de controlador podem preencher e transmitir essas classes views otimizadas para o nosso template view usar. Isso permite a segurança de tipo, a verificação em tempo de compilação, e a utilização do editor IntelliSense dentro dos templates views.

Vamos criar dois View Model para uso em nosso controlador Cesta de Compras:

Vamos criar uma pasta nova chamada ViewModels na raiz do nosso projeto para manter as coisas organizadas. Selecione o projeto MvcMusicStore e no menu Project clique em New Folder e informe o nome da pasta como ViewModels.

Agora clique com o botão direito sobre a pasta ViewModels e selecione Add->Class e informe o nome CestaComprasViewModel.vb;

A seguir digite o código abaixo nesta classe:

Imports System.Collections.Generic

Public Class CestaComprasViewModel
    Public Property CestaItens() As List(Of Cesta)
    Public Property CestaTotal() As Decimal
End Class

Nossa classe possui duas propriedades:

  1. CestaItens - Uma lista de itens da cesta
  2. CestaTotal - Um valor decimal para tratar o preço total para todos os itens da cesta;

Vamos criar também a classe CestaComprasRemoveViewModel.vb na pasta ViewModels com as seguintes propriedades:

Public Class CestaComprasRemoveViewModel

    Public Property Mensagem() As String
    Public Property CestaTotal() As Decimal
    Public Property CestaContador() As Integer
    Public Property ItemContador() As Integer
    Public Property DeletaId() As Integer

End Class

O controlador Cesta de Compras

O controlador da Cesta de Compras tem três objetivos principais:

  1. a adição de itens a uma cesta,
  2. a remoção de itens da cesta,
  3. a visualização dos itens na cesta

Ele vai fazer uso das três classes que acabamos de criar: CestaComprasViewModel, CestaComprasRemoveViewModel, e CestaCompras.

Como nos controladores StoreController e StoreManagerController, vamos adicionar um campo para manter uma instância de MusicStoreEntities.

Vamos adicionar um controlador de Cesta de compras ao projeto usando o modelo de controlador de Empty.

Clique com o botão direito sobre a pasta Controllers e selecione Add->Controller;

A seguir informe o nome CestaComprasController, escolha o template Empty e clique no botão Add;

A seguir digite o código abaixo na classe CestaComprasController:

Imports System.Linq
Imports System.Web.Mvc
Imports MvcMusicStore.MvcMusicStore

Namespace MvcMusicStore

    Public Class CestaComprasController
        Inherits System.Web.Mvc.Controller

        Private storeDB As New MusicStoreEntities()

        ' GET: /CestaCompras
        Function Index() As ActionResult
            Dim cesta = CestaCompras.GetCesta(Me.HttpContext)
            ' Configura nosso ViewModel
            Dim viewModel = New CestaComprasViewModel() With { _
                .CestaItens = cesta.GetItemsDaCesta(), _
                .CestaTotal = cesta.GetTotal() _
            }
            ' Retorna a view
            Return View(viewModel)
        End Function

        ' GET: /Store/AdicinarNaCesta/5
        Public Function AdicinarNaCesta(id As Integer) As ActionResult
            ' Retorna o album do banco de dados
            Dim albumAdicionado = storeDB.Albuns.[Single](Function(album) album.AlbumId = id)
            ' Adiciona na cesta
            Dim cesta = CestaCompras.GetCesta(Me.HttpContext)
            cesta.AdicionarNaCesta(albumAdicionado)
            ' Volta a pagina principal para mais compras
            Return RedirectToAction("Index")
        End Function

        ' AJAX: /CestaCompras/RemoverDaCesta/5
        <HttpPost> _
        Public Function RemoverDaCesta(id As Integer) As ActionResult
            ' Remove o item da cesta
            Dim cesta = CestaCompras.GetCesta(Me.HttpContext)
            ' Pega o nome do album para exibir a confirmação
            Dim nomeAlbum As String = storeDB.Cestas.[Single](Function(item) item.RegistroId = id).Album.Titulo
            ' Remove da cesta
            Dim contaItem As Integer = cesta.RemoveDaCesta(id)
            ' exie a mensagem 
            Dim results = New CestaComprasRemoveViewModel() With { _
                 .Mensagem = Server.HtmlEncode(nomeAlbum) & " foi removido da sua cesta de compras.", _
                 .CestaTotal = cesta.GetTotal(), _
                 .CestaContador = cesta.GetContador(), _
                 .ItemContador = contaItem, _
                 .DeletaId = id _
            }
            Return Json(results)
        End Function

        ' GET: /CestaCompras/CestaResumo
        <ChildActionOnly> _
        Public Function CestaResumo() As ActionResult
            Dim cesta = CestaCompras.GetCesta(Me.HttpContext)
            ViewData("CartCount") = cesta.GetContador()
            Return PartialView("CestaResumo")
        End Function

    End Class
End Namespace

Acima temos o código completo do controlador CestaComprasController que vamos analisar mais adiante.

Atualização via Ajax com jQuery

Vamos criar uma view Index que é fortemente tipada para o controlador CestaComprasViewModel e que usa um template List usando o mesmo método anterior.

Antes de continuar é bom dar um Build no projeto via menu BUILD -> Build Solution;

Clique com o botão direito no interior do código do método Index do controlador CestaComprasViewModel e selecione Add View;

No entanto, em vez de usar um Html.ActionLink para remover itens da cesta, vamos usar jQuery para "conectar" o evento de clique para todos os links nesta view que têm o código HTML RemoveLink.

Ao invés de postar o formulário, este manipulador de eventos de clique só vai fazer uma chamada AJAX para nossa Action RemoverDaCesta do controlador.

A action RemoverDaCesta retorna um resultado JSON serializado, que o nosso callback jQuery, então, analisa e executa quatro atualizações rápidas para a página usando jQuery:

  1. Remove o álbum excluído da lista;
  2. Atualiza a contagem do carrinho no cabeçalho;
  3. Exibe uma mensagem de atualização para o usuário;
  4. Atualiza o preço total da cesta;

Como o cenário de remoção está sendo tratado por um retorno de chamada Ajax na view Index, nós não precisamos de uma view adicional para a ação RemoverDaCesta.

Aqui está o código completo para a view /CestaCompras/Index:

@ModelType MvcMusicStore.CestaComprasViewModel

@Code
    ViewData("Title") = "Cesta de Compras"
End Code

<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 () {
            // pega o id do link
            var registroADeletar = $(this).attr("data-id");
            if (registroADeletar != '') {
                // realiza um post ajax
                $.post("/CestaCompras/RemoverDaCesta", { "id": registroADeletar },
                    function (data) {
                        if (data.ItemContador == 0) {
                            $('#row-' + data.DeletaId).fadeOut('slow');
                        } else {
                            $('#item-contador-' + data.DeletaId).text(data.ItemContador);
                        }
                        $('#cesta-total').text(data.CestaTotal);
                        $('#atualiza-mensagem').text(data.Mensagem);
                        $('#cesta-status').text('Cesta (' + data.CestaContador + ')');
                    });
            }
        });
    });
</script>

<h3>
    <em>Rever </em> sua cesta:
 </h3>

<p class="button">
    @Html.ActionLink("Checkout>>", "AddressAndPayment", "Checkout")
</p>
<div id="atualiza-mensagem">
</div>
<table>
    <tr>
        <th>
            Nome do Álbum
        </th>
        <th>
            Preço (unitário)
        </th>
        <th>
            Quantidade
        </th>
        <th></th>
    </tr>
 @For Each item In Model.CestaItens
     @<tr id="row-@item.RegistroId">
            <td>
                @Html.ActionLink(item.Album.Titulo, "Detalhes", "Store", New With {.id = item.AlbumId}, Nothing)
            </td>
            <td>
                @item.Album.Preco
            </td>
            <td id="item-contador-@item.RegistroId">
                @item.Contador
            </td>
            <td>
                <a href="#" class="RemoveLink" data-id="@item.RegistroId">Remover da cesta</a>
            </td>
    </tr>
Next
 <tr>
        <td>
            Total
        </td>
        <td>
        </td>
        <td>
        </td>
        <td id="cesta-total">
            @Model.CestaTotal
        </td>
    </tr>
</table>

Para testar o que acabamos de implementar precisamos ser capazes de adicionar itens a nossa cesta de compras. Vamos atualizar nossa view Detalhes da pasta Views/Store e incluir um botão "Adicionar na cesta". Podemos incluir algumas das informações adicionais de Álbuns também.

O código da view Detalhes.vbhtml da pasta /Views/Store atualizada é mostrado abaixo:

@ModelType MvcMusicStore.Album

@Code
    ViewBag.Title = "Album - " + Model.Titulo
End Code

<h2>@Model.Titulo</h2>

<p>
    <img alt="@Model.Titulo" src="@Model.AlbumArtUrl" />
</p>
<div id="album-details">
    <p>
        <em>Gênero:</em> @Model.Genero.Nome
    </p>
    <p>
        <em>Artista:</em> @Model.Artista.Nome
    </p>
    <p>
        <em>Preço:</em> @String.Format("{0:F}", Model.Preco)
    </p>
    <p class="button">
        @Html.ActionLink("Incluir na Cesta", "AdicionarNaCesta","CestaCompras", new { id = Model.AlbumId }, "")
    </p>
</div>

Agora estamos prontos para testar.

Execute o projeto e navegue para a url /Store:

A seguir escolha um gênero e clique no link para que seja exibido uma lista de álbuns:

Clicando no título do álbum teremos a exibição da view Detalhes dos álbuns incluindo o botão - Incluir na Cesta :

Clicando no botão - Incluir na Cesta - teremos a execução da view Index da Cesta de Compras exibindo um resumo dos itens:

Vamos repetir o procedimento e incluir mais alguns itens na cesta de compras:

Agora vamos testar o link - Remover da cesta - clicando no segundo item :

Vamos agora incluir mais um item mais de uma vez para verificar se o contador esta acumulando os itens e realizando a soma corretamente:

Temos assim a implementação da nossa cesta de compras funcionando onde já podemos selecionar e incluir e remover itens da cesta de compras.

No próximo tutorial vamos implementar o registro do usuário e a conclusão do processo de compra com a realização do checkout: ASP .NET - MVC Music Store - Registro de usuário e Checkout

Referências:


José Carlos Macoratti