In our previous Knockout & ASP.NET Web API Databinding and Templating example we saw how we could create a ViewModel and bind it to a Template in Knockout.
Two-way data-binding is great as long as you are fine with direct updates to your view model. However if you want an intermediate step where in the UI changes can be cancelled, the two-way data-binding gets in the way, sort of. However, there is a neat solution for it, in form of the Protected Observable plugin by Ryan Nemiyer.
Secondly if we have a list of items in our view model and only a few of them have undergone changes, ideally we would like to post only the ones that have changed. To achieve this behavior, we will update our view model and adapt some of Ryan’s change tracking logic to implement a KO change tracker.
We start off with the previous project that we built while learning about Knockout Templating. This project is an ASP.NET Web API project with a single page that’s manipulated by KO. It shows a list of blogs on the Left and on selecting a blog, an editable form is shown on the right hand side. We will update this application to make sure KO doesn’t update the ViewModel unless we tell it to, by explicitly committing changes to the ViewModel.
This article has been co-authored by Sumit Maitra and me.
Getting Started
Step 1: We update KnockoutJS using Nuget
PM> update-package knockoutjs
PM> update-package jquery
PM> update-package jquery.UI.Combined
Step 2: Once we have the latest in jQuery and Knockout goodness, time to get Ryan’s protected observable. I couldn’t find it in a downloadable form so I grabbed it from this JS Fiddle and saved it in a new JavaScript file under the scripts folder called knockout.protectedobservable.js. The entire plugin code is as follows
//wrapper for an observable that protects value until committed
ko.protectedObservable = function(initialValue) {
//private variables
var _temp = initialValue;
var _actual = ko.observable(initialValue);
var result = ko.dependentObservable({
read: _actual,
write: function(newValue) {
_temp = newValue;
}
});
//commit the temporary value to our observable, if it is different
result.commit = function() {
if (_temp !== _actual()) {
_actual(_temp);
}
};
//notify subscribers to update their value with the original
result.reset = function() {
_actual.valueHasMutated();
_temp = _actual();
};
return result;
};
Step 3: Now that we’ve got the observable code, let’s modify our application so that we have two states, one View state and one Edit state. In the View State, we’ll see the Blog item as a non-editable entry. On explicitly requesting an Edit, we’ll present an Editable View “Update” and “Cancel” buttons.
The Updated chtml markup is as follows.
<div id="body">
<div class="left-section" style="height: 600px; width: 25%; float: left; background-color: #b9b9b9">
<h2>Blogs</h2>
<button data-bind="click: newBlog">New Blog</button>
<button id="saveAll">Save</button>
<ul data-bind="foreach: blogs">
<li style="list-style: none">
<a data-bind="text: Title, id: Id, click: $parent.selectBlog" href="#"></a>
</li>
</ul>
</div>
<div class="right-section" style="height: 600px;
width: 75%; float: left; background-color: #d9d9d9"
data-bind="with: selectedBlog">
<div id="selectView">
<div>
Title:
<label data-bind="text: Title" />
</div>
<div>
<div>
Post:
</div>
<span data-bind="text: Post"></span>
</div>
<div>
<button id="edit" data-bind="click: $parent.editBlog">Edit</button>
</div>
</div>
<div id="editView">
<div>
Title:
<input data-bind="value: Title" />
</div>
<div>
Post: <textarea data-bind="value: Post"></textarea>
</div>
<div>
<button id="update" data-bind="click: update">Update</button>
<button id="cancel" data-bind="click: cancel">Cancel</button>
</div>
</div>
</div>
</div>
We’ve removed the Select Button and instead have bound the click event of the anchor tag to the selectBlog action.
Next we have two divs in the ‘right-section’ div. One called ‘selectView’ and the other called ‘editView’. The ‘selectView’ uses labels for displaying bound data, whereas ‘editView’ uses input boxes for displaying bound data. The ‘selectView’ has an ‘Edit’ button that makes the ‘editView’ visible.
The ‘editView’ has two buttons, ‘Update’ and ‘Cancel’. The actions for these buttons is what we have to implement. The corresponding JavaScript changes in ko-blog.js is as follows:
selectBlog: function(blog)
{
viewModel.selectedBlog(this);
$(".right-section").show();
$("#selectView").fadeIn("slow");
if (!$("#editView").hidden)
{
$("#editView").fadeOut("slow");
}
},
editBlog: function(blog)
{
$("#selectView").fadeOut("slow");
$("#editView").fadeIn("slow");
},
We are using jQuery fadein and fadeout events to show/hide the appropriate divs depending on the button clicked.
Before we go ahead and update the KO View Model to use protected observables, let’s see the updated behavior.
- User selects the title to see the details of the post.
- Then they click Edit to bring up the Edit View.
- Next they update the Title, and instead of waiting for the Update button to be clicked, as soon as the user tabs out of the Title input box, the ViewModel gets updated. Cool!
Integrating Knockout.js with the Protected Observable Plugin
Now that we’ve see how the two way data-binding can affect UI updates in-appropriately, let’s bring in the protected observable.
First we add reference to the knockout.protectedobservable.js in our Index.cshtml. Next, we update the viewmodel ko-blog.js as follows
var viewModel =
{
…
editBlog: function (blog)
{
$("#selectView").fadeOut("slow");
$("#editView").fadeIn("slow");
},
updateBlog: function (blog)
{
viewModel.commitSelected(blog);
$("#editView").fadeOut("slow");
},
cancelEdit: function (blog)
{
$("#editView").fadeOut("slow");
},
commitSelected: function (blog)
{
for (var property in blog)
{
if (blog.hasOwnProperty(property) && blog[property].commit)
blog[property].commit();
}
},
newBlog: function ()
{
this.blogs.push(toKoObservable({
Title: "New " + this.blogs().length + 1,
Id: this.blogs().length + 1,
Post: "Post " + this.blogs().length,
IsNew: true
}));
}
}
…
function toKoObserable(blog)
{
return {
Id: ko.protectedObservable(blog.Id),
Title: ko.protectedObservable(blog.Title),
Post: ko.protectedObservable(blog.Post),
Comments: ko.protectedObservable(blog.Comments),
IsDirty: ko.protectedObservable(blog.IsDirty),
IsNew: ko.protectedObservable(blog.IsNew)
};
}
…
If we look closely, we’ve made the following changes/additions.
- The toKoObservable method that takes a JavaScript object and converts in into another with observable properties, now uses the protectedObservable plugin to create each property as Protected Observable.
- Next we have the updateBlog and cancelEdit functions in the view model. The updateBlog function is bound to the Update button whereas the cancelEdit is bound to the cancel function.
- The updateBlog function calls the commitSelected method, passing it the currently updated blog object. This method loops through all the properties in the blog object and calls commit() on each property, thus committing the entire object automatically.
- Finally it hides the Edit pane.
Now if we run the application, we’ll see the following behavior:
Following the arrows from top to bottom, we see that the first time when we make changes and hit cancel, there are no updates to the Title or the post. Second time around when we hit update, notice the title changes only after clicking the ‘Update’ button. Thus we now have control over when to accept a change into our View Model.
With control over the changes in place, we want finer grained control over what data is submitted to the server.
Tracking Changes
To update server with only what has changed, we need some kind of a ‘dirty’ flag implementation. Again, we leverage some of Ryan’s work from this JS Fiddle to see how we can achieve this.
ko.changeTracker = function (viewModel)
{
var self = this;
self.changes = ko.observableArray();
//add this change to the array of changes
function onPropertyChanged(property, source)
{
if (source.IsDirty)
{
source.IsDirty(true);
}
}
//track a single change
function trackChange(prop, source)
{
var value = source[prop];
if (ko.isObservable(value))
{
value.subscribe(function ()
{
onPropertyChanged(prop, source);
});
}
}
//expose a function to track changes for each property on an object
self.trackChanges = function (model)
{
for (var prop in model)
{
if (model.hasOwnProperty(prop))
{
trackChange(prop, model);
var underlying = ko.utils.unwrapObservable(model[prop]);
if (underlying instanceof Array)
{
ko.utils.arrayForEach(underlying, function (item)
{
self.trackChanges(item);
})
}
else if (typeof underlying === "object")
{
self.trackChanges(underlying);
}
}
}
}
}
The idea is simple, the change tracking module subscribes to the onPropertyChanged event for every property in the view model and looks for an ‘IsDirty’ flag. If this flag is present, it sets it to True.
The change tracker is initialized with the entire view model first time in the $(document).ready(…) as follows (in ko-blog.js)
$.ajax(
{
url: "/api/Blogs",
contentType: "text/json",
type: "GET",
success: function (data)
{
$.each(data, function (index)
{
viewModel.blogs.push(toKoObservable(data[index]));
});
var theTracker = new ko.changeTracker();
theTracker.trackChanges(viewModel);
ko.applyBindings(viewModel);
},
error: function (data)
{
alert("ERROR");
}
});
Updating the Save Mechanism
Once we have the change tracker in place, we update the ‘saveAll’ click function that’s invoked on click of the Save button.
$("#saveAll").click(function ()
{
var saveData = ko.toJS(viewModel.blogs);
var count = 0;
$.each(saveData, function (index)
{
var current = saveData[index];
if (current.IsDirty || current.IsNew)
{
count = count + 1;
var action = "PUT";
var stringyF = JSON.stringify(current);
var vUrl = "/api/Blogs?Id=" + current.Id;
if (current.IsNew)
{
action = "POST";
vUrl = "/api/Blogs";
}
$.ajax(
{
url: vUrl,
contentType: "application/json;charset=utf-8",
type: action,
data: JSON.stringify(current)
});
}
});
if (count == 0)
{
alert('No changes detected, nothing to save');
}
else
{
alert('Change detected in ' + count + ' items.', count);
populateViewModel();
}
});
Here we put a guard clause to check if the current blog entry is Dirty or New. If either, we send it off to the server and increment a count. Once the loop completes we tell the user how many changes were detected.
Hereafter we reload the view model using the populateViewModel(). This new method simply encapsulates the code we are calling directly in document ready event.
function populateViewModel()
{
$.ajax(
{
url: "/api/Blogs",
contentType: "text/json",
type: "GET",
success: function (data)
{
viewModel.blogs.removeAll();
$.each(data, function (index)
{
viewModel.blogs.push(toKoObservable(data[index]));
});
var myTracker = new ko.changeTracker();
myTracker.trackChanges(viewModel);
},
error: function (data)
{
alert("ERROR");
}
});
}
Thus our Document Ready function only needs to call populateViewModel() and then apply knockout bindings.
$(document).ready(function ()
{
…
populateViewModel();
ko.applyBindings(viewModel);
…
}
That’s it! Now if we change items or add new items, we send data to the server, else we get the message that no changes were detected and nothing is posted back to the server. Super cool, right?
Conclusion
We saw how we could plug in significant enhancements to KO using only a few lines of code. We were able to really pin point ViewModel changes giving us fine grained control over data transmission to server. This type of control becomes even more important considering mobile devices over constrained internet connections.
We heavily leaned on Ryan’s work, do checkout his site devoted to KO at www.knockmeout.com
Download the entire source code of this article (Github)
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of
DotNetCurry,
DNC Magazine for Developers,
SQLServerCurry and
DevCurry. He has also authored a couple of books
51 Recipes using jQuery with ASP.NET Controls and
The Absolutely Awesome jQuery CookBook.
Suprotim has received the prestigious Microsoft MVP award for Fifteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.
Get in touch with him on Twitter @suprotimagarwal or at LinkedIn