Parte 7: Creación de la página principal

Por Rick Anderson

Descargar el proyecto completado

Crear la página principal

En esta sección, creará la página principal de la aplicación. Esta página será más compleja que la página de administración, por lo que la abordaremos en varios pasos. A lo largo del proceso, verá algunas técnicas de Knockout.js más avanzadas. Este es el diseño básico de la página:

Diagram of interaction between products, cart, orders, and order details elements of a main page.

Diagrama de interacción entre productos, carros, pedidos y elementos de detalles de pedidos de una página principal. El elemento products tiene la etiqueta GET A P I / products con una flecha que apunta al elemento items. El elemento items está conectado al elemento orders por una flecha etiquetada como POST A P I / orders. El elemento orders está conectado al elemento details con una flecha etiquetada como GET A P I / orders. El elemento details está etiquetado como GET A P I / orders / i d.

  • "Products" contiene una matriz de productos.
  • "Cart" contiene una matriz de productos con cantidades. Al hacer clic en "Add to Cart", se actualiza el carro.
  • "Orders" contiene una matriz de identificadores de pedido.
  • "Details" contiene los detalles de un pedido, que es una matriz de artículos (productos con cantidades)

Comenzaremos por definir algún diseño básico en HTML, sin enlace de datos ni script. Abra el archivo Views/Home/Index.cshtml y reemplace todo el contenido por lo siguiente:

<div class="content">
    <!-- List of products -->
    <div class="float-left">
    <h1>Products</h1>
    <ul id="products">
    </ul>
    </div>

    <!-- Cart -->
    <div id="cart" class="float-right">
    <h1>Your Cart</h1>
        <table class="details ui-widget-content">
    </table>
    <input type="button" value="Create Order"/>
    </div>
</div>

<div id="orders-area" class="content" >
    <!-- List of orders -->
    <div class="float-left">
    <h1>Your Orders</h1>
    <ul id="orders">
    </ul>
    </div>

   <!-- Order Details -->
    <div id="order-details" class="float-right">
    <h2>Order #<span></span></h2>
    <table class="details ui-widget-content">
    </table>
    <p>Total: <span></span></p>
    </div>
</div>

A continuación, agregue una sección de scripts y cree un elemento vista-modelo vacío:

@section Scripts {
  <script type="text/javascript" src="@Url.Content("~/Scripts/knockout-2.1.0.js")"></script>
  <script type="text/javascript">

    function AppViewModel() {
        var self = this;
        self.loggedIn = @(Request.IsAuthenticated ? "true" : "false");
    }

    $(document).ready(function () {
        ko.applyBindings(new AppViewModel());
    });

  </script>
}

En función del diseño planteado anteriormente, nuestro modelo de vista necesita elementos observables para productos, carros, pedidos y detalles. Agregue las siguientes variables al objeto AppViewModel:

self.products = ko.observableArray();
self.cart = ko.observableArray();
self.orders = ko.observableArray();
self.details = ko.observable();

Los usuarios pueden agregar artículos de la lista de productos al carro y quitar artículos del carro. Para encapsular estas funciones, crearemos otra clase de vista-modelo que represente un producto. Agrega el código siguiente a AppViewModel:

function AppViewModel() {
    // ...

    // NEW CODE
    function ProductViewModel(root, product) {
        var self = this;
        self.ProductId = product.Id;
        self.Name = product.Name;
        self.Price = product.Price;
        self.Quantity = ko.observable(0);

        self.addItemToCart = function () {
            var qty = self.Quantity();
            if (qty == 0) {
                root.cart.push(self);
            }
            self.Quantity(qty + 1);
        };

        self.removeAllFromCart = function () {
            self.Quantity(0);
            root.cart.remove(self);
        };
    }
}

La clase ProductViewModel contiene dos funciones que se usan para mover el producto hacia y desde el carro: addItemToCart agrega una unidad del producto al carro y removeAllFromCart quita todas las cantidades del producto.

Los usuarios pueden seleccionar un pedido existente y obtener los detalles del pedido. Encapsularemos esta funcionalidad en otro elemento vista-modelo:

function AppViewModel() {
    // ...

    // NEW CODE
    function OrderDetailsViewModel(order) {
        var self = this;
        self.items = ko.observableArray();
        self.Id = order.Id;

        self.total = ko.computed(function () {
            var sum = 0;
            $.each(self.items(), function (index, item) {
                sum += item.Price * item.Quantity;
            });
            return '$' + sum.toFixed(2);
        });

        $.getJSON("/api/orders/" + order.Id, function (order) {
            $.each(order.Details, function (index, item) {
                self.items.push(item);
            })
        });
    };
}

El elemento OrderDetailsViewModel se inicializa con un pedido y captura los detalles del pedido mediante el envío de una solicitud AJAX al servidor.

Además, observe la propiedad total de OrderDetailsViewModel. Esta propiedad es un tipo especial de elemento observable llamado elemento observable calculado. Como indica su nombre, un elemento observable calculado permite enlazar datos a un valor calculado; en este caso, el costo total del pedido.

A continuación, agregue estas funciones a AppViewModel:

  • resetCart quita todos los artículos del carro.
  • getDetails obtiene los detalles de un pedido (mediante la inserción de un nuevo elemento OrderDetailsViewModel en la lista details).
  • createOrder crea un nuevo pedido y vacía el carro.
function AppViewModel() {
    // ...

    // NEW CODE
    self.resetCart = function() {
        var items = self.cart.removeAll();
        $.each(items, function (index, product) {
            product.Quantity(0);
        });
    }

    self.getDetails = function (order) {
        self.details(new OrderDetailsViewModel(order));
    }

    self.createOrder = function () {
        var jqxhr = $.ajax({
            type: 'POST',
            url: "api/orders",
            contentType: 'application/json; charset=utf-8',
            data: ko.toJSON({ Details: self.cart }),
            dataType: "json",
            success: function (newOrder) {
                self.resetCart();
                self.orders.push(newOrder);
            },
            error: function (jqXHR, textStatus, errorThrown) {
                self.errorMessage(errorThrown);
            }  
        });
    };
};

Por último, inicialice el modelo de vista realizando solicitudes AJAX para los productos y los pedidos:

function AppViewModel() {
    // ...

    // NEW CODE
    // Initialize the view-model.
    $.getJSON("/api/products", function (products) {
        $.each(products, function (index, product) {
            self.products.push(new ProductViewModel(self, product));
        })
    });

    $.getJSON("api/orders", self.orders);
};

Sí, eso es mucho de código, pero lo vamos a crear paso a paso, por lo que esperamos que el diseño esté claro. Ahora, podemos agregar algunos enlaces de Knockout.js al código HTML.

Productos

Estos son los enlaces para la lista de productos:

<ul id="products" data-bind="foreach: products">
    <li>
        <div>
            <span data-bind="text: Name"></span> 
            <span class="price" data-bind="text: '$' + Price"></span>
        </div>
        <div data-bind="if: $parent.loggedIn">
            <button data-bind="click: addItemToCart">Add to Order</button>
        </div>
    </li>
</ul>

Aquí, se recorre en iteración la matriz de productos y se muestra el nombre y el precio. El botón "Add to Order" solo está visible cuando el usuario ha iniciado sesión.

El botón "Add to Order" llama al método addItemToCart de la instancia de ProductViewModel correspondiente al producto. Esto muestra una buena característica de Knockout.js: cuando un elemento vista-modelo contiene otros elementos vista-modelo, puede aplicar los enlaces al modelo interno. En este ejemplo, los enlaces que hay dentro de foreach se aplican a cada una de las instancias de ProductViewModel. Este enfoque es mucho más limpio que poner toda la funcionalidad en un único elemento vista-modelo.

Carro

Estos son los enlaces para el carro:

<div id="cart" class="float-right" data-bind="visible: cart().length > 0">
<h1>Your Cart</h1>
    <table class="details ui-widget-content">
    <thead>
        <tr><td>Item</td><td>Price</td><td>Quantity</td><td></td></tr>
    </thead>    
    <tbody data-bind="foreach: cart">
        <tr>
            <td><span data-bind="text: $data.Name"></span></td>
            <td>$<span data-bind="text: $data.Price"></span></td>
            <td class="qty"><span data-bind="text: $data.Quantity()"></span></td>
            <td><a href="#" data-bind="click: removeAllFromCart">Remove</a></td>
        </tr>
    </tbody>
</table>
<input type="button" data-bind="click: createOrder" value="Create Order"/>

Esto recorre en iteración la matriz del carro y muestra el nombre, el precio y la cantidad. Tenga en cuenta que el vínculo "Remove" y el botón "Create Order" están enlazados a las funciones del elemento vista-modelo.

Pedidos

Estos son los enlaces para la lista de pedidos:

<h1>Your Orders</h1>
<ul id="orders" data-bind="foreach: orders">
<li class="ui-widget-content">
    <a href="#" data-bind="click: $root.getDetails">
        Order # <span data-bind="text: $data.Id"></span></a>
</li>
</ul>

Esto recorre en iteración los pedidos y muestra el identificador de pedido. El evento de clic del vínculo está enlazado a la función getDetails.

Detalles de pedido

Estos son los enlaces para los detalles del pedido:

<div id="order-details" class="float-right" data-bind="if: details()">
<h2>Order #<span data-bind="text: details().Id"></span></h2>
<table class="details ui-widget-content">
    <thead>
        <tr><td>Item</td><td>Price</td><td>Quantity</td><td>Subtotal</td></tr>
    </thead>    
    <tbody data-bind="foreach: details().items">
        <tr>
            <td><span data-bind="text: $data.Product"></span></td>
            <td><span data-bind="text: $data.Price"></span></td>
            <td><span data-bind="text: $data.Quantity"></span></td>
            <td>
                <span data-bind="text: ($data.Price * $data.Quantity).toFixed(2)"></span>
            </td>
        </tr>
    </tbody>
</table>
<p>Total: <span data-bind="text: details().total"></span></p>
</div>

Esto recorre en iteración los artículos del pedido y muestra el producto, el precio y la cantidad. El elemento div circundante solo está visible si la matriz de detalles contiene uno o varios artículos.

Conclusión

En este tutorial, ha creado una aplicación que usa Entity Framework para comunicarse con la base de datos y ASP.NET Web API para proporcionar una interfaz orientada al público sobre la capa de datos. Usamos MVC 4 de ASP.NET para representar las páginas HTML y Knockout.js más jQuery para proporcionar interacciones dinámicas sin recargas de página.

Recursos adicionales: