Parte 5: Lógica de negocios

por Joe Stagner

Tailspin Spyworks muestra cómo es extraordinariamente simple crear aplicaciones eficaces y escalables para la plataforma .NET. Muestra cómo usar las excelentes características nuevas de ASP.NET 4 para crear una tienda en línea, como compras, desprotección y administración.

En esta serie de tutoriales se detallan todos los pasos realizados para compilar la aplicación de ejemplo Tailspin Spyworks. La parte 5 agrega cierta lógica de negocios.

Agregar algunas lógicas de negocios

Queremos que nuestra experiencia de compra esté disponible siempre que alguien visite nuestro sitio web. Los visitantes podrán examinar y agregar elementos al carro de la compra incluso si no están registrados o conectados. Cuando estén listos para finalizar la compra, se les dará la opción de autenticarse y, si aún no son miembros, podrán crear una cuenta.

Esto significa que necesitaremos implementar la lógica para convertir el carro de la compra de un estado anónimo a un estado "Usuario registrado".

Vamos a crear un directorio denominado "Clases" y haga clic con el botón derecho en la carpeta y cree un nuevo archivo "Clase" denominado MyShoppingCart.cs

Screenshot that shows the new Class file named My Shopping Cart dot C S.

Screenshot that shows the contents of the Classes folder.

Como se mencionó anteriormente, extenderemos la clase que implementa la página de MyShoppingCart.aspx y lo haremos con la construcción potente "Clase parcial” de .NET.

La llamada generada para nuestro archivo MyShoppingCart.aspx.cf tiene este aspecto.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace TailspinSpyworks
{
    public partial class MyShoppingCart : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
    }
}

Tenga en cuenta el uso de la palabra clave "parcial".

El archivo de clase que acabamos de generar tiene este aspecto.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace TailspinSpyworks.Classes
{
    public class MyShoppingCart
    {
    }
}

Combinaremos nuestras implementaciones agregando también la palabra clave parcial a este archivo.

Nuestro nuevo archivo de clase ahora tiene este aspecto.

namespace TailspinSpyworks.Classes
{
    public partial class MyShoppingCart
    {
    }
}

El primer método que agregaremos a nuestra clase es el método "AddItem". Este es el método al que se llamará en última instancia cuando el usuario haga clic en los vínculos "Agregar al arte" en las páginas Lista de productos y Detalles del producto.

Anexe lo siguiente a las instrucciones using en la parte superior de la página.

using TailspinSpyworks.Data_Access;

Y agregue este método a la clase MyShoppingCart.

//------------------------------------------------------------------------------------+
public void AddItem(string cartID, int productID, int quantity)
{
  using (CommerceEntities db = new CommerceEntities())
    {
    try 
      {
      var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                              c.ProductID == productID select c).FirstOrDefault();
      if(myItem == null)
        {
        ShoppingCart cartadd = new ShoppingCart();
        cartadd.CartID = cartID;
        cartadd.Quantity = quantity;
        cartadd.ProductID = productID;
        cartadd.DateCreated = DateTime.Now;
        db.ShoppingCarts.AddObject(cartadd);
        }
      else
        {
        myItem.Quantity += quantity;
        }
      db.SaveChanges();
      }
    catch (Exception exp)
      {
      throw new Exception("ERROR: Unable to Add Item to Cart - " + 
                                                          exp.Message.ToString(), exp);
      }
   }
}

Usamos LINQ to Entities para ver si el elemento ya está en el carro. Si es así, actualizamos la cantidad de pedido del elemento; de lo contrario, creamos una nueva entrada para el elemento seleccionado.

Para llamar a este método, implementaremos una página de AddToCart.aspx que no solo clasice este método, sino que, a continuación, mostraremos el carrito de la compra actual después de agregar el elemento.

Haga clic con el botón derecho en el nombre de la solución en el Explorador de soluciones y agregue una nueva página denominada AddToCart.aspx tal como hemos hecho anteriormente.

Aunque podríamos usar esta página para mostrar resultados provisionales como problemas de existencias bajos, etc., en nuestra implementación, la página no se representará realmente, sino que llamará a la lógica "Add" y se redirigirá.

Para ello, agregaremos el código siguiente al evento Page_Load.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Diagnostics;

namespace TailspinSpyworks
{
    public partial class AddToCart : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            string rawId = Request.QueryString["ProductID"];
            int productId;
            if (!String.IsNullOrEmpty(rawId) && Int32.TryParse(rawId, out productId))
            {
                MyShoppingCart usersShoppingCart = new MyShoppingCart();
                String cartId = usersShoppingCart.GetShoppingCartId();
                usersShoppingCart.AddItem(cartId, productId, 1);
            }
            else
            {
                Debug.Fail("ERROR : We should never get to AddToCart.aspx 
                                                   without a ProductId.");
                throw new Exception("ERROR : It is illegal to load AddToCart.aspx 
                                                   without setting a ProductId.");
            }
            Response.Redirect("MyShoppingCart.aspx");
        }
    }
}

Tenga en cuenta que estamos recuperando el producto para agregar al carro de la compra desde un parámetro QueryString y llamando al método AddItem de nuestra clase.

Suponiendo que no se produzca ningún error, el control se pasa a la página de ShoppingCart.aspx, que implementaremos completamente a continuación. Si se produce un error, se produce una excepción.

Actualmente no hemos implementado aún un controlador de errores global, por lo que esta excepción no sería controlada por nuestra aplicación, pero lo solucionaremos en breve.

Tenga en cuenta también el uso de la instrucción Debug.Fail() (disponible a través de using System.Diagnostics;)).

Si la aplicación se ejecuta dentro del depurador, este método mostrará un cuadro de diálogo detallado con información sobre el estado de las aplicaciones junto con el mensaje de error que se especifica.

Cuando se ejecuta en producción, se omite la instrucción Debug.Fail().

Observará en el código anterior una llamada a un método en nuestros nombres de clase de carro de la compra "GetShoppingCartId".

Agregue el código para implementar el método de la manera siguiente.

Tenga en cuenta que también hemos agregado botones de actualización y finalización de la compra y una etiqueta donde podemos mostrar el "total" del carro.

public const string CartId = "TailSpinSpyWorks_CartID";

//--------------------------------------------------------------------------------------+
public String GetShoppingCartId()
{
  if (Session[CartId] == null)
     {
     Session[CartId] = System.Web.HttpContext.Current.Request.IsAuthenticated ? 
                                        User.Identity.Name : Guid.NewGuid().ToString();
     }
  return Session[CartId].ToString();
}

Ahora podemos agregar elementos a nuestro carro de la compra, pero no hemos implementado la lógica para mostrar el carro después de agregar un producto.

Por lo tanto, en la página MyShoppingCart.aspx agregaremos un control EntityDataSource y un control GridVire como se indica a continuación.

<div id="ShoppingCartTitle" runat="server" class="ContentHead">Shopping Cart</div>
<asp:GridView ID="MyList" runat="server" AutoGenerateColumns="False" ShowFooter="True" 
                          GridLines="Vertical" CellPadding="4"
                          DataSourceID="EDS_Cart"  
                          DataKeyNames="ProductID,UnitCost,Quantity" 
                          CssClass="CartListItem">              
  <AlternatingRowStyle CssClass="CartListItemAlt" />
  <Columns>
    <asp:BoundField DataField="ProductID" HeaderText="Product ID" ReadOnly="True" 
                                          SortExpression="ProductID"  />
    <asp:BoundField DataField="ModelNumber" HeaderText="Model Number" 
                                            SortExpression="ModelNumber" />
    <asp:BoundField DataField="ModelName" HeaderText="Model Name" 
                                          SortExpression="ModelName"  />
    <asp:BoundField DataField="UnitCost" HeaderText="Unit Cost" ReadOnly="True" 
                                         SortExpression="UnitCost" 
                                         DataFormatString="{0:c}" />         
    <asp:TemplateField> 
      <HeaderTemplate>Quantity</HeaderTemplate>
      <ItemTemplate>
         <asp:TextBox ID="PurchaseQuantity" Width="40" runat="server" 
                      Text='<%# Bind("Quantity") %>'></asp:TextBox> 
      </ItemTemplate>
    </asp:TemplateField>           
    <asp:TemplateField> 
      <HeaderTemplate>Item Total</HeaderTemplate>
      <ItemTemplate>
        <%# (Convert.ToDouble(Eval("Quantity")) *  
             Convert.ToDouble(Eval("UnitCost")))%>
      </ItemTemplate>
    </asp:TemplateField>
    <asp:TemplateField> 
    <HeaderTemplate>Remove Item</HeaderTemplate>
      <ItemTemplate>
        <center>
          <asp:CheckBox id="Remove" runat="server" />
        </center>
      </ItemTemplate>
    </asp:TemplateField>
  </Columns>
  <FooterStyle CssClass="CartListFooter"/>
  <HeaderStyle  CssClass="CartListHead" />
</asp:GridView>

<div>
  <strong>
    <asp:Label ID="LabelTotalText" runat="server" Text="Order Total : ">  
    </asp:Label>
    <asp:Label CssClass="NormalBold" id="lblTotal" runat="server" 
                                                   EnableViewState="false">
    </asp:Label>
  </strong> 
</div>
<br />
<asp:imagebutton id="UpdateBtn" runat="server" ImageURL="Styles/Images/update_cart.gif" 
                                onclick="UpdateBtn_Click"></asp:imagebutton>
<asp:imagebutton id="CheckoutBtn" runat="server"  
                                  ImageURL="Styles/Images/final_checkout.gif"    
                                  PostBackUrl="~/CheckOut.aspx">
</asp:imagebutton>
<asp:EntityDataSource ID="EDS_Cart" runat="server" 
                      ConnectionString="name=CommerceEntities" 
                      DefaultContainerName="CommerceEntities" EnableFlattening="False" 
                      EnableUpdate="True" EntitySetName="ViewCarts" 
                      AutoGenerateWhereClause="True" EntityTypeFilter="" Select=""                         
                      Where="">
  <WhereParameters>
    <asp:SessionParameter Name="CartID" DefaultValue="0" 
                                        SessionField="TailSpinSpyWorks_CartID" />
  </WhereParameters>
</asp:EntityDataSource>

Llame al formulario en el diseñador para que pueda hacer doble clic en el botón Actualizar carro y generar el controlador de eventos de clic que se especifica en la declaración del marcado.

Implementaremos los detalles más adelante, pero esto nos permitirá compilar y ejecutar la aplicación sin errores.

Al ejecutar la aplicación y agregar un elemento al carro de la compra, verá esto.

Screenshot that shows the updated shopping cart.

Tenga en cuenta que hemos desviado de la presentación de cuadrícula "predeterminada" mediante la implementación de tres columnas personalizadas.

El primero es un campo editable "enlazado" para la cantidad:

<asp:TemplateField> 
      <HeaderTemplate>Quantity</HeaderTemplate>
      <ItemTemplate>
         <asp:TextBox ID="PurchaseQuantity" Width="40" runat="server" 
                      Text='<%# Bind("Quantity") %>'></asp:TextBox> 
      </ItemTemplate>
    </asp:TemplateField>

La siguiente es una columna "calculada" que muestra el total del elemento de la línea (el costo del artículo multiplicado por la cantidad que se va a pedir):

<asp:TemplateField> 
      <HeaderTemplate>Item Total</HeaderTemplate>
      <ItemTemplate>
        <%# (Convert.ToDouble(Eval("Quantity")) *  
             Convert.ToDouble(Eval("UnitCost")))%>
      </ItemTemplate>
    </asp:TemplateField>

Por último, tenemos una columna personalizada que contiene un control CheckBox que el usuario usará para indicar que el elemento debe quitarse del gráfico de compras.

<asp:TemplateField> 
    <HeaderTemplate>Remove Item</HeaderTemplate>
      <ItemTemplate>
        <center>
          <asp:CheckBox id="Remove" runat="server" />
        </center>
      </ItemTemplate>
    </asp:TemplateField>

Screenshot that shows the updated Quantity and Remove Items.

Como puede ver, la línea Total del pedido está vacía, por lo que vamos a agregar alguna lógica para calcular el total de pedidos.

Primero implementaremos un método "GetTotal" en nuestra clase MyShoppingCart.

En el archivo MyShoppingCart.cs agregue el código siguiente.

//--------------------------------------------------------------------------------------+
public decimal GetTotal(string cartID)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        decimal cartTotal = 0;
        try
        {
            var myCart = (from c in db.ViewCarts where c.CartID == cartID select c);
            if (myCart.Count() > 0)
            {
                cartTotal = myCart.Sum(od => (decimal)od.Quantity * (decimal)od.UnitCost);
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Calculate Order Total - " + 
            exp.Message.ToString(), exp);
        }
        return (cartTotal);
     }
}

A continuación, en el controlador de eventos Page_Load, podemos llamar al método GetTotal. Al mismo tiempo, agregaremos una prueba para ver si el carro de la compra está vacío y ajustaremos la pantalla en consecuencia si es así.

Ahora, si el carro de la compra está vacío, obtenemos esto:

Screenshot that shows the empty shopping cart.

Y si no, vemos nuestro total.

Screenshot that shows the total amount for the items in the shopping cart.

Sin embargo, esta página aún no está completa.

Necesitaremos lógica adicional para recalcular el carro de la compra quitando los elementos marcados para su eliminación y determinando los nuevos valores de cantidad, ya que es posible que el usuario haya cambiado algunos en la cuadrícula.

Permite agregar un método "RemoveItem" a nuestra clase de carro de la compra en MyShoppingCart.cs para controlar el caso cuando un usuario marca un elemento para su eliminación.

//------------------------------------------------------------------------------------+
public void RemoveItem(string cartID, int  productID)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        try
        {
            var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                         c.ProductID == productID select c).FirstOrDefault();
            if (myItem != null)
            {
                db.DeleteObject(myItem);
                db.SaveChanges();
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Remove Cart Item - " + 
                                  exp.Message.ToString(), exp);
        }
    }
}

Ahora vamos a asignar un método para controlar la circunstancia cuando un usuario simplemente cambia la calidad que se va a pedir en GridView.

//--------------------------------------------------------------------------------------+
public void UpdateItem(string cartID, int productID, int quantity)
{
    using (CommerceEntities db = new CommerceEntities())
    {
        try
        {
            var myItem = (from c in db.ShoppingCarts where c.CartID == cartID && 
                    c.ProductID == productID select c).FirstOrDefault();
            if (myItem != null)
            {
                myItem.Quantity = quantity;
                db.SaveChanges();
            }
        }
        catch (Exception exp)
        {
            throw new Exception("ERROR: Unable to Update Cart Item - " +     
                                exp.Message.ToString(), exp);
        }
    }
}

Con las características básicas Quitar y Actualizar en su lugar, podemos implementar la lógica que realmente actualiza el carro de la compra en la base de datos. (En MyShoppingCart.cs)

//-------------------------------------------------------------------------------------+
public void UpdateShoppingCartDatabase(String cartId, 
                                       ShoppingCartUpdates[] CartItemUpdates)
{
  using (CommerceEntities db = new CommerceEntities())
    {
    try
      {
      int CartItemCOunt = CartItemUpdates.Count();
      var myCart = (from c in db.ViewCarts where c.CartID == cartId select c);
      foreach (var cartItem in myCart)
        {
        // Iterate through all rows within shopping cart list
        for (int i = 0; i < CartItemCOunt; i++)
          {
          if (cartItem.ProductID == CartItemUpdates[i].ProductId)
             {
             if (CartItemUpdates[i].PurchaseQantity < 1 || 
   CartItemUpdates[i].RemoveItem == true)
                {
                RemoveItem(cartId, cartItem.ProductID);
                }
             else 
                {
                UpdateItem(cartId, cartItem.ProductID, 
                                   CartItemUpdates[i].PurchaseQantity);
                }
              }
            }
          }
        }
      catch (Exception exp)
        {
        throw new Exception("ERROR: Unable to Update Cart Database - " + 
                             exp.Message.ToString(), exp);
        }            
    }           
}

Observará que este método espera dos parámetros. Uno es el identificador del carro de la compra y el otro es una matriz de objetos de tipo definido por el usuario.

Para minimizar la dependencia de nuestra lógica en los detalles de la interfaz de usuario, hemos definido una estructura de datos que podemos usar para pasar los artículos del carro de la compra a nuestro código sin necesidad de acceder directamente al control GridView.

public struct ShoppingCartUpdates
{
    public int ProductId;
    public int PurchaseQantity;
    public bool RemoveItem;
}

En nuestro archivo MyShoppingCart.aspx.cs podemos usar esta estructura en nuestro controlador de eventos Update Button Click como se indica a continuación. Tenga en cuenta que además de actualizar el carro también actualizaremos el total del carro.

//--------------------------------------------------------------------------------------+
protected void UpdateBtn_Click(object sender, ImageClickEventArgs e)
{
  MyShoppingCart usersShoppingCart = new MyShoppingCart();
  String cartId = usersShoppingCart.GetShoppingCartId();

  ShoppingCartUpdates[] cartUpdates = new ShoppingCartUpdates[MyList.Rows.Count];
  for (int i = 0; i < MyList.Rows.Count; i++)
    {
    IOrderedDictionary rowValues = new OrderedDictionary();
    rowValues = GetValues(MyList.Rows[i]);
    cartUpdates[i].ProductId =  Convert.ToInt32(rowValues["ProductID"]);
    cartUpdates[i].PurchaseQantity = Convert.ToInt32(rowValues["Quantity"]); 

    CheckBox cbRemove = new CheckBox();
    cbRemove = (CheckBox)MyList.Rows[i].FindControl("Remove");
    cartUpdates[i].RemoveItem = cbRemove.Checked;
    }

   usersShoppingCart.UpdateShoppingCartDatabase(cartId, cartUpdates);
   MyList.DataBind();
   lblTotal.Text = String.Format("{0:c}", usersShoppingCart.GetTotal(cartId));
}

Tenga en cuenta con especial interés esta línea de código:

rowValues = GetValues(MyList.Rows[i]);

GetValues() es una función auxiliar especial que implementaremos en MyShoppingCart.aspx.cs como se indica a continuación.

//--------------------------------------------------------------------------------------+
public static IOrderedDictionary GetValues(GridViewRow row)
{
  IOrderedDictionary values = new OrderedDictionary();
  foreach (DataControlFieldCell cell in row.Cells)
    {
    if (cell.Visible)
      {
      // Extract values from the cell
      cell.ContainingField.ExtractValuesFromCell(values, cell, row.RowState, true);
      }
    }
    return values;
}

Esto proporciona una manera limpia de acceder a los valores de los elementos enlazados en nuestro control GridView. Dado que el control CheckBox "Quitar elemento" no está enlazado, accederemos a él a través del método FindControl().

En esta fase del desarrollo del proyecto estamos listos para implementar el proceso de finalización de la compra.

Antes de hacerlo, vamos a usar Visual Studio para generar la base de datos de pertenencia y agregar un usuario al repositorio de pertenencia.