Parte 9: Registro e check-out

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 9 abrange o Registro e o Check-out.

Nesta seção, criaremos um CheckoutController que coletará o endereço e as informações de pagamento do comprador. Exigiremos que os usuários se registrem em nosso site antes de fazer check-out, portanto, esse controlador exigirá autorização.

Os usuários navegarão até o processo de check-out do carrinho de compras clicando no botão "Check-out".

Captura de tela da janela da Loja de Músicas mostrando o modo de exibição de check-out com o botão Check-out realçado por uma seta vermelha.

Se o usuário não estiver conectado, ele será solicitado a fazer isso.

Captura de tela da janela da Loja de Músicas mostrando o modo de exibição de logon com os campos Nome de usuário e Senha.

Após o logon bem-sucedido, o usuário verá a exibição Endereço e Pagamento.

Captura de tela da janela da Loja de Música mostrando o endereço e a exibição de pagamento com campos para coletar informações de endereço de envio e pagamento.

Depois de preencherem o formulário e enviarem o pedido, eles serão mostrados na tela de confirmação do pedido.

Captura de tela da janela da Loja de Música mostrando a exibição completa de check-out que informa ao usuário que o pedido foi concluído.

A tentativa de exibir uma ordem inexistente ou uma ordem que não pertence a você mostrará a exibição Erro.

Captura de tela da janela da Loja de Música mostrando a exibição de erro quando o usuário tenta exibir o pedido de outra pessoa ou uma ordem fictícia.

Migrando o carrinho de compras

Embora o processo de compra seja anônimo, quando o usuário clicar no botão Check-out, ele precisará se registrar e fazer logon. Os usuários esperam que mantenhamos suas informações de carrinho de compras entre as visitas, portanto, precisaremos associar as informações do carrinho de compras a um usuário quando ele concluir o registro ou o logon.

Na verdade, isso é muito simples de fazer, pois nossa classe ShoppingCart já tem um método que associará todos os itens no carrinho atual a um nome de usuário. Só precisaremos chamar esse método quando um usuário concluir o registro ou o logon.

Abra a classe AccountController que adicionamos quando estávamos configurando Associação e Autorização. Adicione uma instrução using referenciando MvcMusicStore.Models e adicione o seguinte método MigrateShoppingCart:

private void MigrateShoppingCart(string UserName)
{
    // Associate shopping cart items with logged-in user
    var cart = ShoppingCart.GetCart(this.HttpContext);
 
    cart.MigrateCart(UserName);
    Session[ShoppingCart.CartSessionKey] = UserName;
}

Em seguida, modifique a ação de postagem LogOn para chamar MigrateShoppingCart depois que o usuário tiver sido validado, conforme mostrado abaixo:

//
// POST: /Account/LogOn
[HttpPost]
 public ActionResult LogOn(LogOnModel model, string returnUrl)
 {
    if (ModelState.IsValid)
    {
        if (Membership.ValidateUser(model.UserName, model.Password))
        {
            MigrateShoppingCart(model.UserName);
                    
            FormsAuthentication.SetAuthCookie(model.UserName,
                model.RememberMe);
            if (Url.IsLocalUrl(returnUrl) && returnUrl.Length > 1
                && returnUrl.StartsWith("/")
                && !returnUrl.StartsWith("//") &&
                !returnUrl.StartsWith("/\\"))
            {
                return Redirect(returnUrl);
            }
            else
            {
                return RedirectToAction("Index", "Home");
            }
        }
        else
        {
            ModelState.AddModelError("", "The user name or password provided is incorrect.");
        }
    }
    // If we got this far, something failed, redisplay form
    return View(model);
 }

Faça a mesma alteração na ação Registrar postagem, imediatamente após a conta de usuário ser criada com êxito:

//
// POST: /Account/Register
[HttpPost]
 public ActionResult Register(RegisterModel model)
 {
    if (ModelState.IsValid)
    {
        // Attempt to register the user
        MembershipCreateStatus createStatus;
        Membership.CreateUser(model.UserName, model.Password, model.Email, 
               "question", "answer", true, null, out
               createStatus);
 
        if (createStatus == MembershipCreateStatus.Success)
        {
            MigrateShoppingCart(model.UserName);
                    
            FormsAuthentication.SetAuthCookie(model.UserName, false /*
                  createPersistentCookie */);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            ModelState.AddModelError("", ErrorCodeToString(createStatus));
        }
    }
    // If we got this far, something failed, redisplay form
    return View(model);
 }

É isso: agora um carrinho de compras anônimo será transferido automaticamente para uma conta de usuário após o registro ou logon bem-sucedido.

Criando o CheckoutController

Clique com o botão direito do mouse na pasta Controladores e adicione um novo Controlador ao projeto chamado CheckoutController usando o modelo Controlador vazio.

Captura de tela da janela Adicionar Controlador com o campo Nome do controlador preenchido com o texto Controlador de Check-out.

Primeiro, adicione o atributo Authorize acima da declaração de classe Controller para exigir que os usuários se registrem antes do check-out:

namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller

Observação: isso é semelhante à alteração que fizemos anteriormente ao StoreManagerController, mas nesse caso o atributo Authorize exigia que o usuário estivesse em uma função de Administrador. No Controlador de Check-out, estamos exigindo que o usuário seja conectado, mas não estamos exigindo que eles sejam administradores.

Para simplificar, não lidaremos com informações de pagamento neste tutorial. Em vez disso, estamos permitindo que os usuários marcar usando um código promocional. Armazenaremos esse código promocional usando uma constante chamada PromoCode.

Assim como no StoreController, declararemos um campo para manter uma instância da classe MusicStoreEntities, chamada storeDB. Para usar a classe MusicStoreEntities, precisaremos adicionar uma instrução using para o namespace MvcMusicStore.Models. A parte superior do controlador check-out é exibida abaixo.

using System;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        const string PromoCode = "FREE";

O CheckoutController terá as seguintes ações do controlador:

AddressAndPayment (método GET) exibirá um formulário para permitir que o usuário insira suas informações.

AddressAndPayment (método POST) validará a entrada e processará o pedido.

A conclusão será mostrada depois que um usuário concluir com êxito o processo de check-out. Essa exibição incluirá o número do pedido do usuário, como confirmação.

Primeiro, vamos renomear a ação Controlador de índice (que foi gerada quando criamos o controlador) para AddressAndPayment. Essa ação do controlador apenas exibe o formulário de check-out, para que não exija nenhuma informação de modelo.

//
// GET: /Checkout/AddressAndPayment
public ActionResult AddressAndPayment()
{
    return View();
}

Nosso método POST AddressAndPayment seguirá o mesmo padrão que usamos no StoreManagerController: ele tentará aceitar o envio do formulário e concluir o pedido e exibirá novamente o formulário se ele falhar.

Depois de validar que a entrada do formulário atende aos nossos requisitos de validação para um Pedido, marcar o valor do formulário PromoCode diretamente. Supondo que tudo esteja correto, salvaremos as informações atualizadas com o pedido, informaremos ao objeto ShoppingCart para concluir o processo de pedido e redirecionaremos para a ação Concluir.

//
// POST: /Checkout/AddressAndPayment
[HttpPost]
public ActionResult AddressAndPayment(FormCollection values)
{
    var order = new Order();
    TryUpdateModel(order);
 
    try
    {
        if (string.Equals(values["PromoCode"], PromoCode,
            StringComparison.OrdinalIgnoreCase) == false)
        {
            return View(order);
        }
        else
        {
            order.Username = User.Identity.Name;
            order.OrderDate = DateTime.Now;
 
            //Save Order
            storeDB.Orders.Add(order);
            storeDB.SaveChanges();
            //Process the order
            var cart = ShoppingCart.GetCart(this.HttpContext);
            cart.CreateOrder(order);
 
            return RedirectToAction("Complete",
                new { id = order.OrderId });
        }
    }
    catch
    {
        //Invalid - redisplay with errors
        return View(order);
    }
}

Após a conclusão bem-sucedida do processo de check-out, os usuários serão redirecionados para a ação Concluir controlador. Essa ação executará uma marcar simples para validar se a ordem realmente pertence ao usuário conectado antes de mostrar o número do pedido como uma confirmação.

//
// GET: /Checkout/Complete
public ActionResult Complete(int id)
{
    // Validate customer owns this order
    bool isValid = storeDB.Orders.Any(
        o => o.OrderId == id &&
        o.Username == User.Identity.Name);
 
    if (isValid)
    {
        return View(id);
    }
    else
    {
        return View("Error");
    }
}

Observação: a exibição Erro foi criada automaticamente para nós na pasta /Views/Shared quando iniciamos o projeto.

O código CheckoutController completo é o seguinte:

using System;
using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.Controllers
{
    [Authorize]
    public class CheckoutController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        const string PromoCode = "FREE";
        //
        // GET: /Checkout/AddressAndPayment
        public ActionResult AddressAndPayment()
        {
            return View();
        }
        //
        // POST: /Checkout/AddressAndPayment
        [HttpPost]
        public ActionResult AddressAndPayment(FormCollection values)
        {
            var order = new Order();
            TryUpdateModel(order);
 
            try
            {
                if (string.Equals(values["PromoCode"], PromoCode,
                    StringComparison.OrdinalIgnoreCase) == false)
                {
                    return View(order);
                }
                else
                {
                    order.Username = User.Identity.Name;
                    order.OrderDate = DateTime.Now;
 
                    //Save Order
                    storeDB.Orders.Add(order);
                    storeDB.SaveChanges();
                    //Process the order
                    var cart = ShoppingCart.GetCart(this.HttpContext);
                    cart.CreateOrder(order);
 
                    return RedirectToAction("Complete",
                        new { id = order.OrderId });
                }
            }
            catch
            {
                //Invalid - redisplay with errors
                return View(order);
            }
        }
        //
        // GET: /Checkout/Complete
        public ActionResult Complete(int id)
        {
            // Validate customer owns this order
            bool isValid = storeDB.Orders.Any(
                o => o.OrderId == id &&
                o.Username == User.Identity.Name);
 
            if (isValid)
            {
                return View(id);
            }
            else
            {
                return View("Error");
            }
        }
    }
}

Adicionando a exibição AddressAndPayment

Agora, vamos criar a exibição AddressAndPayment. Clique com o botão direito do mouse em uma das ações do controlador AddressAndPayment e adicione uma exibição chamada AddressAndPayment, que é fortemente tipada como um Pedido e usa o modelo Editar, conforme mostrado abaixo.

Captura de tela da janela Adicionar Exibição com o campo Nome da exibição, a caixa de seleção Criar um modo de exibição e as listas suspensas Classe de modelo e Scaffold realçadas em vermelho.

Essa exibição usará duas das técnicas que examinamos ao criar a exibição StoreManagerEdit:

  • Usaremos Html.EditorForModel() para exibir campos de formulário para o modelo order
  • Aproveitaremos as regras de validação usando uma classe Order com atributos de validação

Começaremos atualizando o código do formulário para usar Html.EditorForModel(), seguido por uma caixa de texto adicional para o Código Promocional. O código completo para a exibição AddressAndPayment é mostrado abaixo.

@model MvcMusicStore.Models.Order
@{
    ViewBag.Title = "Address And Payment";
}
<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>
@using (Html.BeginForm()) {
    
    <h2>Address And Payment</h2>
    <fieldset>
        <legend>Shipping Information</legend>
        @Html.EditorForModel()
    </fieldset>
    <fieldset>
        <legend>Payment</legend>
        <p>We're running a promotion: all music is free 
            with the promo code: "FREE"</p>
        <div class="editor-label">
            @Html.Label("Promo Code")
        </div>
        <div class="editor-field">
            @Html.TextBox("PromoCode")
        </div>
    </fieldset>
    
    <input type="submit" value="Submit Order" />
}

Definindo regras de validação para o Pedido

Agora que nossa exibição está configurada, vamos configurar as regras de validação para nosso modelo order, como fizemos anteriormente para o modelo de Álbum. Clique com o botão direito do mouse na pasta Modelos e adicione uma classe chamada Order. Além dos atributos de validação que usamos anteriormente para o Álbum, também usaremos uma Expressão Regular para validar o endereço de email do usuário.

using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace MvcMusicStore.Models
{
    [Bind(Exclude = "OrderId")]
    public partial class Order
    {
        [ScaffoldColumn(false)]
        public int OrderId { get; set; }
        [ScaffoldColumn(false)]
        public System.DateTime OrderDate { get; set; }
        [ScaffoldColumn(false)]
        public string Username { get; set; }
        [Required(ErrorMessage = "First Name is required")]
        [DisplayName("First Name")]
        [StringLength(160)]
        public string FirstName { get; set; }
        [Required(ErrorMessage = "Last Name is required")]
        [DisplayName("Last Name")]
        [StringLength(160)]
        public string LastName { get; set; }
        [Required(ErrorMessage = "Address is required")]
        [StringLength(70)]
        public string Address { get; set; }
        [Required(ErrorMessage = "City is required")]
        [StringLength(40)]
        public string City { get; set; }
        [Required(ErrorMessage = "State is required")]
        [StringLength(40)]
        public string State { get; set; }
        [Required(ErrorMessage = "Postal Code is required")]
        [DisplayName("Postal Code")]
        [StringLength(10)]
        public string PostalCode { get; set; }
        [Required(ErrorMessage = "Country is required")]
        [StringLength(40)]
        public string Country { get; set; }
        [Required(ErrorMessage = "Phone is required")]
        [StringLength(24)]
        public string Phone { get; set; }
        [Required(ErrorMessage = "Email Address is required")]
        [DisplayName("Email Address")]
       
        [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}",
            ErrorMessage = "Email is is not valid.")]
        [DataType(DataType.EmailAddress)]
        public string Email { get; set; }
        [ScaffoldColumn(false)]
        public decimal Total { get; set; }
        public List<OrderDetail> OrderDetails { get; set; }
    }
}

A tentativa de enviar o formulário com informações ausentes ou inválidas agora mostrará a mensagem de erro usando a validação do lado do cliente.

Captura de tela da janela da Loja de Música mostrando o endereço e o modo de exibição de pagamento com uma cadeia de caracteres de informações inválidas nos campos de telefone e email.

Ok, nós fizemos a maior parte do trabalho duro para o processo de check-out; só temos algumas chances e terminamos para terminar. Precisamos adicionar duas exibições simples e precisamos cuidar da entrega das informações do carrinho durante o processo de logon.

Adicionando o modo de exibição Checkout Complete

O modo de exibição Checkout Complete é bastante simples, pois ele só precisa exibir a ID do pedido. Clique com o botão direito do mouse na ação Concluir controlador e adicione uma exibição chamada Complete, que é fortemente tipada como um int.

Captura de tela da janela Adicionar Exibição com o campo Nome da exibição e a lista suspensa Classe de modelo realçada em retângulos vermelhos.

Agora, atualizaremos o código de exibição para exibir a ID do pedido, conforme mostrado abaixo.

@model int
@{
    ViewBag.Title = "Checkout Complete";
}
<h2>Checkout Complete</h2>
<p>Thanks for your order! Your order number is: @Model</p>
<p>How about shopping for some more music in our 
    @Html.ActionLink("store",
"Index", "Home")
</p>

Atualizando o modo de exibição De erro

O modelo padrão inclui uma exibição De erro na pasta Exibições compartilhadas para que possa ser reutilizada em outro lugar do site. Essa exibição de Erro contém um erro muito simples e não usa o layout do site, portanto, vamos atualizá-lo.

Como essa é uma página de erro genérica, o conteúdo é muito simples. Incluiremos uma mensagem e um link para navegar até a página anterior no histórico se o usuário quiser tentar novamente a ação.

@{
    ViewBag.Title = "Error";
}
 
<h2>Error</h2>
 
<p>We're sorry, we've hit an unexpected error.
    <a href="javascript:history.go(-1)">Click here</a> 
    if you'd like to go back and try that again.</p>