Introduction
KnockoutJS is a very useful two way data binding library. There are some great articles on it on Code Project and the Knockout website itself has some very good tutorials and examples. If you are working with lists and arrays, you may find my article on searching filtering and sorting knockout lists useful. This article assumes you are familiar with Knockout and need some insight into using arrays with Knockout and passing that array data back to an MVC application.
The article provides a simple walk-through instruction on working with arrays in KnockoutJS, and demonstrates how to save KnockoutJS JSON data from the client browser to server using a mapped ViewModel
object.
Setup
This example uses a simple MVC project with no other dependencies other than KnockoutJS and some supporting libraries. Our example will use a basic model of a sales-person that has many customers each of whom can have many orders.
Server-side Code
The following is the simple model we will use to represent the "Sales person".
Sales person can sell in many regions
Sales person can sell to many customers, customers can have many orders
The following code sets up this simple model server-side.
public class SalesPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public List<region> Regions {get; set;}
public List<customer> Customers {get; set;}
public SalesPerson()
{
Regions = new List<region>();
Customers = new List<customer>();
}
}
public class Region
{
public int ID { get; set;}
public string SortOrder { get; set; }
public string Name { get; set; }
}
public class Customer
{
public int ID { get; set; }
public string SortOrder { get; set; }
public string Name { get; set; }
public List<order> Orders { get; set; }
public Customer()
{
Orders = new List<order>();
}
}
public class Order
{
public int ID { get; set; }
public string Date { get; set; }
public string Value { get; set; }
}
For this simple example, we are going to do the majority of the work client-side, and setup data client-side - therefore, we will keep things simple server-side. We will start with returning a simple view form the main index controller.
public ActionResult Index()
{
return View();
}
When we are finished our work in the client, we will be sending data back to a controller using the model we have just defined. This is done by simply declaring controller method that takes a parameter of the same type as our model.
public JsonResult SaveModel(SalesPerson SalesPerson)
{
var s = SalesPerson;
return null;
}
As an aside, if we wanted to take a pre-populated model server-side however, we could use the standard model passing workflow that MVC provides us with:
public ActionResult Index()
{
SalesPerson aalesPersonModel = new SalesPerson
return View(salesPersonModel);
}
In the cshtml view, we would then take in the model, serialise it to JSON and render it into a client-side JSON variable we would load into our Knockout model:
@Html.Raw(Json.Encode(Model))
Client-Side Code
The first thing we will do client side, is set up a JavaScript file in our MVC project to mirror our server-side model, and give it some functionality.
If we work backwards up the model tree, we can see more clearly how things are created.
Customer
s can have many order
s, so let's discuss that first.
var Order = function {
var self = this;
self.ID = ko.observable();
self.Date = ko.observable();
self.Value = ko.observable();
});
}
The above code is a very basic Knockout object model. Is has three fields, ID
, Date
and Value
. To make it more useful, we need to extend it a bit. We will "extend" to tell the observable a particular field/value is required, we will allow the model to take an argument of "data" into which we can pass a pre-populated model, and finally we will tell the model that if "data" is sent in, to "unwrap" it using the Knockout Mapping plugin. As there are no sub-array items in orders, there are no "options" passed to the ko.mapping
function "{}
"
Here is the updated model:
var Order = function (data) {
var self = this;
if (data != null) {
ko.mapping.fromJS(data, {}, self);
} else {
self.ID = ko.observable();
self.Date = ko.observable().extend({
required: true
});
self.Value = ko.observable().extend({
required: true
});
}
self.Value.extend({
required: {
message: '* Value needed'
}
});
}
Next up, we have the customer model, it follows the same pattern we discussed for the order. The additional thing to note here is that we tell it *when you encounter an object called "Orders
", unwrap it using the "orderMapping
" plugin.
var Customer = function (data) {
var self = this;
if (data != null) {
ko.mapping.fromJS(data, { Orders: orderMapping }, self);
} else {
self.ID = ko.observable();
self.SortOrder = ko.observable();
self.Name = ko.observable().extend({
required: true
});
self.Orders = ko.observable();
self.OrdersTotal = ko.computed(function () {
return self.FirstName() + " " + self.LastName();
}, self);
}
The "orderMapping
" simply tells Knockout how to unwrap any data it finds for the "Orders
" sub-array using the "Order
" object:
var orderMapping = {
create: function (options) {
return new Order(options.data);
}
};
For the customer
model, we will extend it differently, saying that it is required, and if no value is provided, to show the error message "* Name needed
".
self.Name.extend({
required: {
message: '* Name needed'
}
});
Finally, we add some operation methods to manage the CRUD of Order
s.
Knockout maintains an internal index of its array items, therefore when you call an action to do on an array item, it happens in the context of the currently selected item. This means we don't have to worry about sending in the selected-index of an item to delete
/insert
/update
/etc.
This method is called by the "x
" beside each existing order
record, and when called, delete
s the selected item form the array stack.
self.removeOrder = function (Order) {
self.Orders.remove(Order);
}
This method takes care of pushing a new item onto the array. note in particular that we don't create an anonymous object, instead we specifically declare the type of object we require.
self.addOrder = function () {
self.Orders.push(new Order({
ID: null,
Date: "",
Value: ""
}));
}
As we go higher up the Sales person model, and want to create a customer
, it has a child
object that is an array (unlike the order
object which stands on its own). When creating a new customer
object, we must therefore also initialise the array that will contain any future customer
orders. Note the order
s being created as an empty array "[]
"
self.addCustomer = function () {
self.Customers.push(new Customer({
ID: null,
Name: "",
Orders: []
}));
}
Finally, for initialization, we have a method that loads in-line JSON data into the Knockout ViewModel
we declared. Note how the mapping works in this case. the function says ... load the object called modelData
, and when you encounter an object called "regions
", unwrap it through:
self.loadInlineData = function () {
ko.mapping.fromJS(modeldata, { Regions: regionMapping, Customers: customerMapping }, self);
}
Note the options - it says load data from the object modeldata
, and when you enter a sub-object called regions
, use regionsmapping
method to unwrap it. Likewise with customer
s, use customermapping
.
The downloadable code gives further details.
Mark-up
The data binding of Knockout is simple and powerful. By adding attributes to mark-up tags, we bind to the data-model and any data in the model gets rendered in the browser for us.
Sales Person (Top Level Details) Mark-Up
Sales person
First name:
<input data-bind="value:FirstName" />
Last name:
<input data-bind="value:LastName" />
Regions Mark-Up
The tag control-flow operator "foreach
" tells Knockout "for each array item 'region
', render the contents of this div
container".
Note also the data-bind method "$parent.removeRegion
" which calls a simple delete
method in the model
<div data-bind="foreach:Regions">
<div class="Regionbox">Region: <input data-bind="value:Name" />
<a data-bind="click: $parent.removeRegion" href="#">x</a></div>
</div>
Customers Mark-Up
The customers mark-up carries the same patterns as previous code. What is important to note in this section of code is that there is a "for each
" data-bind *within* a "for each
" ... it's nested. We are therefore saying "render this mark-up for each customer
record you find, and for each customer
record you find, render each 'customer.order
' record you find."
The other new concept in this block of code is the data-bind "$index
". This attribute tells knockout to render the "array index" of the current item.
<div data-bind="foreach:Customers">
<div class="Customerbox">
Customer:
<input data-bind="value:Name" /> <a href="#" data-bind="click: $parent.removeCustomer">x</a>
<span style="float:right">
Index: <span data-bind="text:$index"></span>
</span>
<a href="#" data-bind="click: addOrder">Order +</a>
<br />
<div data-bind="foreach:Orders">
---- Order date:
<input data-bind="value:Date" />Value:
<input data-bind="value:Value" /> <a href="#" data-bind="click: $parent.removeOrder">x</a>
<br />
</div> <!-- foreach Orders -->
</div>
</div> <!-- foreach Customer -->
Sortable Plugin
Before we move to the data exchange part of this example, let's look at one more useful plugin when working with Knockout arrays and lists. Its "Knockout Sortable", provided by the very talented Ryan Niemeyer.
<div data-bind="sortable:Regions">
<div class="Regionbox">Region: <input data-bind="value:Name" /> <a data-bind="click: $parent.removeRegion" href="#">x</a></div>
</div>
By simply replacing the "for each
" array data-bin
attribute with "sortable
", our array object magically becomes drag-drop sortable. Look at the following animated GIF for an example.
Sending Data to MVC Server
Sending the datamodel
from client to server is achieved using a simple Ajax call. The main trick to serialising data form Knockout is to use the "ToJSON
" method. In our case, as we have nested array objects, we will pass this through the mapping methods.
self.saveDataToServer = function (){
var DataToSend = ko.mapping.toJSON(self);
$.ajax({
type: 'post',
url: '/home/SaveModel',
contentType: 'application/json',
data: DataToSend,
success: function (dataReceived) {
alert('sent: ' + dataReceived)
},
fail: function (dataReceived) {
alert('fail: ' + dataReceived)
}
});
};
As we have our models on both server and client mapped in structure, the JSON is converted by the MVC server and is directly accessible as a server-side data model:
public JsonResult SaveModel(SalesPerson SalesPerson)
{
var s = SalesPerson;
return null;
}
That's it - download the attached code to see further detail and experiment. I hope it is useful to some!
History
- 18th April, 2015 - Version 1 published