Compartir a través de


Parte 9: Registro y finalización de la compra

por Jon Galloway

MVC Music Store es una aplicación de tutorial que presenta y explica paso a paso cómo usar ASP.NET MVC y Visual Studio para el desarrollo web.

MVC Music Store es una implementación ligera de la tienda de muestras que vende álbumes de música en línea e implementa la administración básica del sitio, el inicio de sesión de usuario y la funcionalidad del carro de la compra.

En esta serie de tutoriales se detallan todos los pasos realizados para compilar la aplicación de ejemplo ASP.NET MVC Music Store. La parte 9 abarca Registro y finalización de la compra.

En esta sección, crearemos un CheckoutController que recopilará la dirección del comprador y la información de pago. Solicitaremos a los usuarios que se registren en nuestro sitio antes de llevar a cabo la finalización de la compra, por lo que este controlador requerirá autorización.

Los usuarios navegarán al proceso de finalización de la compra desde su carro de la compra haciendo clic en el botón "Finalización de la compra".

Screenshot of the Music Store window showing the checkout view with the Checkout button highlighted by a red arrow.

Si el usuario no ha iniciado sesión, se le pedirá que lo haga.

Screenshot of the Music Store window showing the log on view with User name and Password fields.

Después de iniciar sesión correctamente, al usuario se le mostrará la vista Dirección y Pago.

Screenshot of the Music Store window showing the address and payment view with fields to collect shipping address and payment information.

Una vez que se haya rellenado el formulario y enviado el pedido, se mostrará la pantalla de confirmación del pedido.

Screenshot of the Music Store window showing the checkout complete view that informs the user that the order is complete.

Si se intentase ver un pedido inexistente o uno que no perteneciera, se mostrará la vista Error.

Screenshot of the Music Store window showing the error view when the user attempts to view another person's order or a fictitious order.

Migración del carro de la compra

Aunque el proceso de compra es anónimo, cuando el usuario haga clic en el botón Finalización de la compra, se le pedirá que se registre e inicie sesión. Los usuarios esperarán que se mantenga la información del carro de la compra entre las visitas, por lo que tendremos que asociar la información del carro de la compra con un usuario cuando se complete el registro o el inicio de sesión.

Esto es realmente muy sencillo de hacer, ya que la clase ShoppingCart ya tiene un método que asociará todos los elementos del carro actual con un nombre de usuario. Solo será necesario llamar a este método cuando un usuario complete el registro o el inicio de sesión.

Abra la clase AccountController que se agregó al configurar Pertenencia y autorización. Agregue una instrucción using que haga referencia a MvcMusicStore.Models y agregue el siguiente 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;
}

A continuación, modifique la acción posterior a LogOn para llamar a MigrateShoppingCart una vez que se valide el usuario, tal y como se muestra a continuación:

//
// 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);
 }

Realice el mismo cambio en la acción posterior a Registrar, inmediatamente después de crear correctamente la cuenta de usuario:

//
// 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);
 }

Es decir, ahora un carro de la compra anónimo se transferirá automáticamente a una cuenta de usuario después de un registro o inicio de sesión correctos.

Creación de CheckoutController

Haga clic con el botón derecho en la carpeta Controllers y agregue un nuevo controlador al proyecto denominado CheckoutController mediante la plantilla Controlador vacío.

Screenshot of the Add Controller window with the Controller name field filled with the text Checkout Controller.

En primer lugar, agregue el atributo Authorize que hay encima de la declaración de clase Controller para requerir que los usuarios se registren antes de la finalización de la compra:

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

Nota: esto es similar al cambio realizado anteriormente en StoreManagerController, pero en ese caso el atributo Authorize requería que el usuario tuviera un rol Administrador. En el controlador de Finalización de la compra, se requiere que el usuario inicie sesión, pero no es necesario que sea administrador.

Por motivos de simplicidad, no se tratará la información de pago en este tutorial. En su lugar, se permite a los usuarios realizar la finalización de la compra con un código promocional. Almacenaremos este código promocional con una constante denominada PromoCode.

Como en StoreController, declararemos un campo para contener una instancia de la clase MusicStoreEntities, denominada storeDB. Para usar la clase MusicStoreEntities, es necesario agregar una instrucción using para el espacio de nombres MvcMusicStore.Models. A continuación, aparecerá la parte superior del controlador Finalización de la compra.

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";

CheckoutController tendrá las siguientes acciones de controlador:

AddressAndPayment (método GET) mostrará un formulario para permitir al usuario escribir su información.

AddressAndPayment (método POST) validará la entrada y procesará el pedido.

Complete se mostrará después de que un usuario haya terminado correctamente el proceso de finalización de la compra. Esta vista incluirá el número de pedido del usuario, como confirmación.

En primer lugar, cambiaremos el nombre de la acción Controlador de índice (que se generó al crear el controlador) a AddressAndPayment. Esta acción del controlador solo muestra el formulario de finalización de la compra, por lo que no requiere ninguna información del modelo.

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

El método AddressAndPayment POST seguirá el mismo patrón que se usó en StoreManagerController: intentará aceptar el envío del formulario, completará el pedido y volverá a mostrar el formulario si se produjese un error.

Después de validar que la entrada del formulario cumple con los requisitos de validación de un pedido, comprobaremos el valor del formulario PromoCode directamente. Suponiendo que todo fuera correcto, guardaremos la información actualizada con el pedido, indicaremos al objeto ShoppingCart que complete el proceso de pedido y redirigiremos a la acción Completar.

//
// 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);
    }
}

Después de completar correctamente el proceso de finalización de la compra, los usuarios se redirigirán a la acción Completar controlador. Esta acción realizará una comprobación sencilla para validar que el pedido pertenezca realmente al usuario que inició sesión antes de mostrar el número de pedido como confirmación.

//
// 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");
    }
}

Nota: la vista Error se creó automáticamente para nosotros en la carpeta /Views/Shared cuando se inició el proyecto.

El código de CheckoutController completo es el siguiente:

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");
            }
        }
    }
}

Adición de la vista AddressAndPayment

Ahora crearemos la vista AddressAndPayment. Haga clic con el botón derecho en una de las acciones del controlador AddressAndPayment y agregue una vista denominada AddressAndPayment, que está fuertemente tipada como pedido y usa la plantilla Editar, tal y como se muestra a continuación.

Screenshot of the Add View window with the View name field, the Create a view checkbox, and the Model class and Scaffold dropdowns highlighted in red.

Esta vista hará uso de dos de las técnicas que examinamos al compilar la vista StoreManagerEdit:

  • Usaremos Html.EditorForModel() para mostrar campos de formulario para el modelo Pedido
  • Sacaremos provecho de las reglas de validación mediante una clase Order con atributos de validación

Comenzaremos actualizando el código del formulario para usar Html.EditorForModel(), seguido de un cuadro de texto adicional para el código promocional. A continuación, se muestra el código completo de la vista AddressAndPayment.

@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" />
}

Definición de reglas de validación del pedido

Ahora que la vista ya está configurada, estableceremos las reglas de validación para nuestro modelo Pedido como hicimos anteriormente con el modelo Álbum. Haga clic con el botón derecho en la carpeta Models y agregue una clase denominada Order. Además de los atributos de validación que se usaron anteriormente para Álbum, también usaremos una expresión regular para validar la dirección de correo electrónico del usuario.

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; }
    }
}

Si intentase enviar el formulario con información que faltase o no fuera válida, ahora se mostrará el mensaje de error mediante la validación del lado cliente.

Screenshot of the Music Store window showing the address and payment view with a string of invalid information in the phone and email fields.

Muy bien, ya hemos realizado la mayor parte del duro trabajo del proceso de finalización de la compra. Ya queda poco para terminar. Es necesario agregar dos vistas sencillas, y es necesario encargarse de la transferencia de la información del carro durante el proceso de inicio de sesión.

Adición de la vista Finalización de la compra completada

La vista Finalización de la compra completada es bastante sencilla, ya que solo es necesario mostrar el id. de pedido. Haga clic con el botón derecho en la acción Completar controlador y agregue una vista denominada Completar, que está fuertemente tipada como valor int.

Screenshot of the Add View window with the View name field and the Model class dropdown highlighted in red rectangles.

Ahora actualizaremos el código de vista para mostrar el id. de pedido, tal y como se muestra a continuación.

@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>

Actualización de la vista Error

La plantilla predeterminada incluye una vista Error en la carpeta Shared views para que se pueda volver a usar en otro lugar del sitio. Esta vista Error contiene un error muy sencillo y no usa el diseño de nuestro sitio, por lo que lo actualizaremos.

Como se trata de una página de error genérica, el contenido es muy sencillo. Incluiremos un mensaje y un vínculo para navegar a la página anterior del historial en caso de que el usuario quiera volver a probar su acción.

@{
    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>