Part 7: Creating the Main Page
by Mike Wasson
Creating the Main Page
In this section, you will create the main application page. This page will be more complex than the Admin page, so we’ll approach it in several steps. Along the way, you’ll see some more advanced Knockout.js techniques. Here is the basic layout of the page:
- “Products” holds an array of products.
- “Cart” holds an array of products with quantities. Clicking “Add to Cart” updates the cart.
- “Orders” holds an array of order IDs.
- “Details” holds an order detail, which is an array of items (products with quantities)
We’ll start by defining some basic layout in HTML, with no data binding or script. Open the file Views/Home/Index.cshtml and replace all of the contents with the following:
[!code-htmlMain]
1: <div class="content">
2: <!-- List of products -->
3: <div class="float-left">
4: <h1>Products</h1>
5: <ul id="products">
6: </ul>
7: </div>
8:
9: <!-- Cart -->
10: <div id="cart" class="float-right">
11: <h1>Your Cart</h1>
12: <table class="details ui-widget-content">
13: </table>
14: <input type="button" value="Create Order"/>
15: </div>
16: </div>
17:
18: <div id="orders-area" class="content" >
19: <!-- List of orders -->
20: <div class="float-left">
21: <h1>Your Orders</h1>
22: <ul id="orders">
23: </ul>
24: </div>
25:
26: <!-- Order Details -->
27: <div id="order-details" class="float-right">
28: <h2>Order #<span></span></h2>
29: <table class="details ui-widget-content">
30: </table>
31: <p>Total: <span></span></p>
32: </div>
33: </div>
Next, add a Scripts section and create an empty view-model:
[!code-cshtmlMain]
1: @section Scripts {
2: <script type="text/javascript" src="@Url.Content("~/Scripts/knockout-2.1.0.js")"></script>
3: <script type="text/javascript">
4:
5: function AppViewModel() {
6: var self = this;
7: self.loggedIn = @(Request.IsAuthenticated ? "true" : "false");
8: }
9:
10: $(document).ready(function () {
11: ko.applyBindings(new AppViewModel());
12: });
13:
14: </script>
15: }
Based on the design sketched earlier, our view model needs observables for products, cart, orders, and details. Add the following variables to the AppViewModel
object:
[!code-javascriptMain]
1: self.products = ko.observableArray();
2: self.cart = ko.observableArray();
3: self.orders = ko.observableArray();
4: self.details = ko.observable();
Users can add items from the products list into the cart, and remove items from the cart. To encapsulate these functions, we’ll create another view-model class that represents a product. Add the following code to AppViewModel
:
[!code-javascriptMain]
1: function AppViewModel() {
2: // ...
3:
4: // NEW CODE
5: function ProductViewModel(root, product) {
6: var self = this;
7: self.ProductId = product.Id;
8: self.Name = product.Name;
9: self.Price = product.Price;
10: self.Quantity = ko.observable(0);
11:
12: self.addItemToCart = function () {
13: var qty = self.Quantity();
14: if (qty == 0) {
15: root.cart.push(self);
16: }
17: self.Quantity(qty + 1);
18: };
19:
20: self.removeAllFromCart = function () {
21: self.Quantity(0);
22: root.cart.remove(self);
23: };
24: }
25: }
The ProductViewModel
class contains two functions that are used to move the product to and from the cart: addItemToCart
adds one unit of the product to the cart, and removeAllFromCart
removes all quantities of the product.
Users can select an existing order and get the order details. We’ll encapsulate this functionality into another view-model:
[!code-javascriptMain]
1: function AppViewModel() {
2: // ...
3:
4: // NEW CODE
5: function OrderDetailsViewModel(order) {
6: var self = this;
7: self.items = ko.observableArray();
8: self.Id = order.Id;
9:
10: self.total = ko.computed(function () {
11: var sum = 0;
12: $.each(self.items(), function (index, item) {
13: sum += item.Price * item.Quantity;
14: });
15: return '$' + sum.toFixed(2);
16: });
17:
18: $.getJSON("/api/orders/" + order.Id, function (order) {
19: $.each(order.Details, function (index, item) {
20: self.items.push(item);
21: })
22: });
23: };
24: }
The OrderDetailsViewModel
is initialized with an order, and it fetches the order details by sending an AJAX request to the server.
Also, notice the total
property on the OrderDetailsViewModel
. This property is a special kind of observable called a computed observable. As the name implies, a computed observable lets you data bind to a computed value—in this case, the total cost of the order.
Next, add these functions to AppViewModel
:
resetCart
removes all items from the cart.getDetails
gets the details for an order (by pusing a newOrderDetailsViewModel
onto thedetails
list).createOrder
creates a new order and empties the cart.
[!code-javascriptMain]
1: function AppViewModel() {
2: // ...
3:
4: // NEW CODE
5: self.resetCart = function() {
6: var items = self.cart.removeAll();
7: $.each(items, function (index, product) {
8: product.Quantity(0);
9: });
10: }
11:
12: self.getDetails = function (order) {
13: self.details(new OrderDetailsViewModel(order));
14: }
15:
16: self.createOrder = function () {
17: var jqxhr = $.ajax({
18: type: 'POST',
19: url: "api/orders",
20: contentType: 'application/json; charset=utf-8',
21: data: ko.toJSON({ Details: self.cart }),
22: dataType: "json",
23: success: function (newOrder) {
24: self.resetCart();
25: self.orders.push(newOrder);
26: },
27: error: function (jqXHR, textStatus, errorThrown) {
28: self.errorMessage(errorThrown);
29: }
30: });
31: };
32: };
Finally, initialize the view model by making AJAX requests for the products and orders:
[!code-javascriptMain]
1: function AppViewModel() {
2: // ...
3:
4: // NEW CODE
5: // Initialize the view-model.
6: $.getJSON("/api/products", function (products) {
7: $.each(products, function (index, product) {
8: self.products.push(new ProductViewModel(self, product));
9: })
10: });
11:
12: $.getJSON("api/orders", self.orders);
13: };
OK, that’s a lot of code, but we built it up step-by-step, so hopefully the design is clear. Now we can add some Knockout.js bindings to the HTML.
Products
Here are the bindings for the product list:
[!code-htmlMain]
1: <ul id="products" data-bind="foreach: products">
2: <li>
3: <div>
4: <span data-bind="text: Name"></span>
5: <span class="price" data-bind="text: '$' + Price"></span>
6: </div>
7: <div data-bind="if: $parent.loggedIn">
8: <button data-bind="click: addItemToCart">Add to Order</button>
9: </div>
10: </li>
11: </ul>
This iterates over the products array and displays the name and price. The “Add to Order” button is visible only when the user is logged in.
The “Add to Order” button calls addItemToCart
on the ProductViewModel
instance for the product. This demonstrates a nice feature of Knockout.js: When a view-model contains other view-models, you can apply the bindings to the inner model. In this example, the bindings within the foreach
are applied to each of the ProductViewModel
instances. This approach is much cleaner than putting all of the functionality into a single view-model.
Cart
Here are the bindings for the cart:
[!code-htmlMain]
1: <div id="cart" class="float-right" data-bind="visible: cart().length > 0">
2: <h1>Your Cart</h1>
3: <table class="details ui-widget-content">
4: <thead>
5: <tr><td>Item</td><td>Price</td><td>Quantity</td><td></td></tr>
6: </thead>
7: <tbody data-bind="foreach: cart">
8: <tr>
9: <td><span data-bind="text: $data.Name"></span></td>
10: <td>$<span data-bind="text: $data.Price"></span></td>
11: <td class="qty"><span data-bind="text: $data.Quantity()"></span></td>
12: <td><a href="#" data-bind="click: removeAllFromCart">Remove</a></td>
13: </tr>
14: </tbody>
15: </table>
16: <input type="button" data-bind="click: createOrder" value="Create Order"/>
This iterates over the cart array and displays the name, price, and quantity. Note that the “Remove” link and the “Create Order” button are bound to view-model functions.
Orders
Here are the bindings for the orders list:
[!code-htmlMain]
1: <h1>Your Orders</h1>
2: <ul id="orders" data-bind="foreach: orders">
3: <li class="ui-widget-content">
4: <a href="#" data-bind="click: $root.getDetails">
5: Order # <span data-bind="text: $data.Id"></span></a>
6: </li>
7: </ul>
This iterates over the orders and shows the order ID. The click event on the link is bound to the getDetails
function.
Order Details
Here are the bindings for the order details:
[!code-htmlMain]
1: <div id="order-details" class="float-right" data-bind="if: details()">
2: <h2>Order #<span data-bind="text: details().Id"></span></h2>
3: <table class="details ui-widget-content">
4: <thead>
5: <tr><td>Item</td><td>Price</td><td>Quantity</td><td>Subtotal</td></tr>
6: </thead>
7: <tbody data-bind="foreach: details().items">
8: <tr>
9: <td><span data-bind="text: $data.Product"></span></td>
10: <td><span data-bind="text: $data.Price"></span></td>
11: <td><span data-bind="text: $data.Quantity"></span></td>
12: <td>
13: <span data-bind="text: ($data.Price * $data.Quantity).toFixed(2)"></span>
14: </td>
15: </tr>
16: </tbody>
17: </table>
18: <p>Total: <span data-bind="text: details().total"></span></p>
19: </div>
This iterates over the items in the order and displays the product, price, and quanity. The surrounding div is visible only if the details array contains one or more items.
Conclusion
In this tutorial, you created an application that uses Entity Framework to communicate with the database, and ASP.NET Web API to provide a public-facing interface on top of the data layer. We use ASP.NET MVC 4 to render the HTML pages, and Knockout.js plus jQuery to provide dynamic interactions without page reloads.
Additional resources:
|