title | authors | intro | types | categories | published | updated | status | |||
---|---|---|---|---|---|---|---|---|---|---|
A Knockout.js price calculator |
|
A pricing calculator is usually a very difficult p... |
|
|
2011/12/01 12:00:00 |
2011/12/01 13:00:00 |
archived |
A pricing calculator is usually a very difficult piece of front end engineering to produce. In this blog I’m going to let you into a little secret, a secret that removes all of the complication out of creating html that needs to change and respond to changes in the underlying data. The secret is knockout.js a JavaScript Library that can be used with any platform but I’m going to show you how to use it in ASP.NET MVC.
Download Source Code Demo View Demo
I’m currently organizing a stag party for my Best friend James Konarczak, we’ve been friends for over 25 years so It’s pretty important that I don’t mess things up when I’m planning his party. There are hundreds of stag websites on the market but many of the one’s I have visited make it really difficult to calculate the total cost of a stag party. What I wanted was a simple calculator that would enable me to roughly calculate the cost so that I could start asking people if they were interested in coming and start taking deposits.
I decided to crack open Visual Studio and start putting together the kind of pricing calculator that I wish existed.
Although I have used the example of a stag party pricing calculator the methods I’m using could be applied to solve all sorts of problems from shopping carts to editable data grids. The knockout.js website also has an interactive tutorial that enables you to play with the library and work through a number of examples.
To get started you will need to download a few things, firstly you’ll need Microsoft Visual Web Developer 2010 Express which you can get for free from you’ll also need knockout.js which you can download from and is licensed under the open source MIT License.
Start Web Developer Express and go to File > New > Project. Select Visual C# on the right hand side and from the list select ASP.NET MVC 3 Web Application. Enter a project name then press OK. When you’re asked to select a template choose **empty **and tick the **use HTML5 semantic **markup check box.
You add knockout to your project the same way would add jQuery: by adding the script tags to the head of the document and putting the JavaScript files in the scripts folder. Add the script tags to the default layout page which can be found in Views/Shared/_Layout.cshtml
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery.tmpl.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-1.2.1.js")" type="text/javascript"></script>
Firstly we are going to create a model. A model is a collection of objects that represent your system. Our system comprises of a few objects: A shopping cart, products and categories. We just need to create each of these objects with simple properties to capture the data that the user will enter.
using System.Collections.Generic;namespace KnockoutExample.Models { public class Category { public int Id { get; set; } public string Name { get; set; } public List Products { get; set; } } }
With ASP.NET MVC you can generate a database from your objects. All you need to do is create a class that inherits from DbContext. For each object in you system, you create a property and give it a name. When the application starts the database will be created automatically including any relationships between objects.
using System.Data.Entity;namespace KnockoutExample.Models { public class StagParty : DbContext { public DbSet product { get; set; } public DbSet category { get; set; } } }
The C in MVC stands for Controller. In our application we will add a controller called CartController. To add a controller, right click on the controller folder and select Controller > Add. Add an ActionResult onto this controller and call it index. When you go to the URL http://mysite.com/cart/ this controller will be executed.
namespace Stagulator.Controllers { public class CartController : Controller { // GET: /Cart/ public ActionResult Index() { var service = new CartService(); ViewBag.ProductInfo = service.FetchCatogries(); return View(); }[HttpPost] public ActionResult Save(CartViewModel cart) { // Save logic would go here. var notice = string.Format("The {0} products for {1} guests has now been booked", cart.CartLines.Count, cart.GuestNumber); return Json(notice, JsonRequestBehavior.AllowGet); } }
}
As it stands the Action Result we have just added will return a view. A view is just a document that contains the HTML. The view can also run code; the syntax used on the page is called Razor. To add view create the folder Views/Cart then right click and choose add View.
In the view add a div called cart, this will be where the cart will appear. In a script block add a variable called productCategories. Populate this with the ViewBag.productInfo property that was passed down by the server. By using @Json.Encode and @Html.Raw the C# property will be converted by Razor into JSON.
// Apply the data from the server to the variable var productCategories = @Html.Raw(@Json.Encode(ViewBag.productInfo)); var viewModel = new Cart(); ko.applyBindings(viewModel, document.getElementById("cart"));
We now need to use JavaScript to create an object that represents a sales line in the cart, we do this by adding the properties, category, product, quantity and subtotal. Setting them as ko.observable ensures that when these variables change it will inform the rest of the model that changes have been made.
var CartLine = function() { this.category = ko.observable(); this.product = ko.observable(); this.quantity = ko.observable(1); this.subtotal = ko.dependentObservable(function() { return this.product() ? this.product().Price * parseInt("0"+this.quantity(), 10) : 0; }.bind(this));// Whenever the category changes, reset the product selection this.category.subscribe(function() { this.product(undefined); }.bind(this)); };</pre>
With the subtotal property we use the ko.dependentObservable function. This lets the knockout library know that this property is reliant on other properties in the model. When changes happen with the price or quantity properties this dependent property will also recalculate any UI that is bound to these properties will also update.
this.subtotal = ko.dependentObservable(function() { return this.product() ? this.product().Price * parseInt("0"+this.quantity(), 10) : 0; }.bind(this));
Next we will add the cart model to the JavaScript. You’ll notice that we add the property lines and add an array of cartLines to that property. With the guest property I have added a default value of 10 to the property by passing it in via the constructor.
// This is an object that represents the Cart var Cart = function () { // Stores an array of lines, and from these, can work out the grandTotal this.lines = ko.observableArray([new CartLine()]); // Put one line in by default this.guests = ko.observable(10); this.costPerPerson = ko.dependentObservable(function () { var totals = 0; for (var i = 0; i < this.lines().length; i++) totals += this.lines()[i].subtotal(); return totals; } .bind(this));this.grandTotal = ko.dependentObservable(function () { return this.costPerPerson() * parseInt("0" + this.guests(), 10); } .bind(this)); // Operations this.addLine = function () { this.lines.push(new CartLine()); }; this.removeLine = function (line) { this.lines.remove(line); }; this.save = function () { // Creates a Javascript object object with the same property names as the C# object var dataToSave = $.map(this.lines(), function (line) { return line.product() ? line.product() : undefined; }); var guest = this.guests(); var JsonObject = new function () { this.GuestNumber = guest; this.CartLines = dataToSave; }; // Convert the object to JSON var json = JSON.stringify(JsonObject); // Post the object to the server using jQuery $.ajax({ url: '@Url.Action("save","cart")', type: 'POST', dataType: 'json', data: json, error: function (data) { alert('Something Went Wrong'); }, contentType: 'application/json; charset=utf-8', success: function (data) { alert(data); } }); }; };</pre>
I would like the user to be able to change the number of guests that are attending the stag party. To do this I create an input field and use the data-bind attribute to bind the guest’s property of my model to the input field.
Total Guests:
Some of the properties like Cost Per Person do not need to be editable, so we bind them to a span. In the data-bind attribute we can call any functions we like. The formatCurrency function formats the number by adding a £ to the front.
Binding single properties is straight forward but more complex properties like the collection of CartLines requires a different approach. For this we will need to use the knockout template engine. In the data-bind we specify a template name: cartRowTemplate and we pass in the lines property.
The cartRowTemplate is defined using a script tag with the id set as the name of the template. In this case the template is called cartRowTemplate. It’s simply html that will be repeated for each of the items in the lines collection. As before, we bind the properties using the data-bind attribute.
<script id='cartRowTemplate' type='text/html'>
Remove
</script>
We can also use the data-bind syntax to bind buttons to events. There are two buttons that need to be wired up, “Add Product”, which adds a new item to the lines collection and “Submit Order”, which will send the cart data to the server.
Add product Submit order
The “Submit Order” button is wired to the save function on the model. This function converts the data into a JSON object and posts it to the save action of the cart controller. The @Url.Action helper creates the URL of the save action for us.
Over on the server side we add a method called save to the Cart Controller. The JSON object will be converted to a c# cart object by ASP.NET Automagically. We could save that data, however we just send back a message to the user.
[HttpPost] public ActionResult Save(CartViewModel cart) { // Save logic would go here. var notice = string.Format("The {0} products for {1} guests has now been booked", cart.CartLines.Count, cart.GuestNumber); return Json(notice, JsonRequestBehavior.AllowGet); }
To finish the demo I edited the default CSS and added same dummy data to the application by editing the Global.asax file and creating a database initializer. I also changed the default controller to Cart so that it would be the first page that loaded if you visited the root of the website.