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".
Se o usuário não estiver conectado, ele será solicitado a fazer isso.
Após o logon bem-sucedido, o usuário verá a exibição Endereço e Pagamento.
Depois de preencherem o formulário e enviarem o pedido, eles serão mostrados na tela de confirmação do pedido.
A tentativa de exibir uma ordem inexistente ou uma ordem que não pertence a você mostrará a exibição Erro.
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.
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.
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.
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.
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>