In this article, We will implement CRUD (Create, Read, update and delete) operations with Knockout.js, ASP.NET Web API, Entity Framework 5.0 Database First Approach. For Add or Edit operation, A modal dialog having WYSIWYG Html editor is used. User can enter information and submit it. It should look like the image below:
Creating Project:
Open VS2012
File > New Project > ASP.NET MVC 4 Web Application > Enter name & Path > OK
Select Web API as template > OK
DB Structure:
Consider the following table structure:
Models:
Right Click on Model Folder > Add > New Item > Ado.Net Entity Data Model > Enter Name > Add
Generate from database > Next
Select Database > Enter Entity Name (DBEntities) > Next
Select Comments Table > Enter Model Name > Finish
Web API Controller:
Right Click on Controllers folder > Add > Controller
Enter Name "CommentController" > Select Template "API Controller with Read/Write Actions ...", Select Model (Comment) and Data Context Class (DBEntities)> Add
HTML Editor:
For simplicity, We are going to use same app to consume Web API.
We are going to use jHtmlArea Editor built on top of jQuery.
To install jHtmlArea, run the following command in the Package Manager Console
Install-Package jHtmlAreaClient-Side JavaScript:
If you see default SPA(Single Page Application) template, there are three layers:
1. todo.datacontext.js: responsible for AJAX requests to the Web API controllers.
2. todo.model.js: Defines the client side models.
3. todo.viewmodel.js: Defines the view model.
There is one more file:
4. todo.bindings.js: for knockout custom bindings
To implement all above layers, add a new script file say 'Comment.js' and reference it in the view.
1. Creating Client Side Model:
// Model
function Comment(data) {
var self = this;
data = data || {};
// Persisted properties
self.CommentID = data.CommentID;
self.Name = ko.observable(data.Name || "");
self.Email = ko.observable(data.Email || "");
self.HTMLComment = ko.observable(data.HTMLComment || "");
self.error = ko.observable();
//persist edits to real values on accept
self.acceptChanges = function () {
self.HTMLComment($('.htmlEditor').htmlarea('toHtmlString'));
dataContext.saveComment(self);
window.commentViewModel.selectedComment(null);
};
//reset to originals on cancel
self.cancelChanges = function () {
window.commentViewModel.selectedComment(null);
};
self.deleteComment = function () {
if (confirm('Do you really want to delete this comment?')) {
dataContext.deleteComment(self).done(function () {
window.commentViewModel.comments.remove(self);
});
}
}
}
In above model, three methods are implemented to save comment(acceptChanges), to cancel the changes(cancelChanges) and to delete comment(deleteComment).
We will add a class 'htmlEditor' to comment textarea and here it is used to get user entered value.
2. Data Context Layer:
// DataContext
var commentApiUrl = '/api/comment';
var dataContext = {
saveComment: function (comment) {
var InsertMode = comment.CommentID ? false : true;
comment.error(null);
if (InsertMode) {
var options = {
dataType: "json",
contentType: "application/json",
cache: false,
type: 'POST',
data: ko.toJSON(comment)
};
return $.ajax(commentApiUrl, options)
.done(function (result) {
comment.CommentID = result.CommentID;
window.commentViewModel.comments.push(comment);
})
.fail(function () {
alert('Error on adding comment');
});
}
else {
var options = {
dataType: "json",
contentType: "application/json",
cache: false,
type: 'PUT',
data: ko.toJSON(comment)
};
return $.ajax(commentApiUrl+"/" + comment.CommentID, options)
.fail(function () {
comment.error('Error on adding comment');
});
}
},
getComments: function (commentObservable, errorObservable) {
var options = {
dataType: "json",
contentType: "application/json",
cache: false,
type: 'GET'
};
return $.ajax(commentApiUrl, options)
.done(getSucceeded)
.fail(getFailed);
function getSucceeded(data) {
var mappedComments = $.map(data, function (list) { return new Comment(list); });
commentObservable(mappedComments);
}
function getFailed() {
errorObservable("Error retrieving comments.");
}
},
deleteComment: function (comment) {
var options = {
dataType: "json",
contentType: "application/json",
cache: false,
type: 'DELETE'
};
return $.ajax(commentApiUrl + "/" + comment.CommentID, options)
.fail(function () {
comment.error('Error on adding comment');
});
}
}
It handles AJAX calls to the Web API controllers to get all comments and add, edit or delete comment.
3. View Model Layer:
//ViewModel
window.commentViewModel = (function () {
var comments = ko.observableArray(),
error = ko.observable(),
addComment = function () {
selectedComment(new Comment());
loadEditor();
},
selectedComment = ko.observable(''),
editComment = function (comment) {
selectedComment(comment);
loadEditor();
};
dataContext.getComments(comments, error);
function loadEditor() {
$(".htmlEditor").htmlarea({
// Override/Specify the Toolbar buttons to show
toolbar: [
["bold", "italic", "h1", "link", //"image",
"orderedList", "unorderedList", "horizontalrule", {
// Add a custom Toolbar Button
css: "justifyleft",
text: "Code Sample <pre><code>",
action: function (btn) {
// 'this' = jHtmlArea object
this.pasteHTML('<pre><code>' + this.getSelectedHTML() + '</code></pre>');
}
}]
],
toolbarText: $.extend({}, jHtmlArea.defaultOptions.toolbarText, {
h1: "Heading 1", horizontalrule: "Insert Horizontal Rule"
})
});
}
return {
comments: comments,
error: error,
addComment: addComment,
selectedComment: selectedComment,
editComment: editComment
}
})();
In above viewmodel, loadEditor method is used to initialize jHtmlArea editor and a custom toolbar button is added to insert the code.
4. Custom Bindings:
To initialize jquery UI dialog we use Knockout custom binding.
//custom binding to initialize a jQuery UI dialog
ko.bindingHandlers.jqDialog = {
init: function (element, valueAccessor) {
var options = ko.utils.unwrapObservable(valueAccessor()) || {};
//handle disposal
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).dialog("destroy");
});
$(element).dialog(options);
}
};
//custom binding handler that opens/closes the dialog
ko.bindingHandlers.openDialog = {
update: function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
if (value) {
$(element).dialog("open");
} else {
$(element).dialog("close");
}
}
}
5. Activating Knockout:
to bind the view model to the view
ko.applyBindings(window.commentViewModel);
View:
First reference jHtmlArea css in the head tag.
@Styles.Render("~/Content/css")
@Styles.Render("~/Content/themes/base/css")
@Scripts.Render("~/bundles/modernizr")
<link href="~/Content/jHtmlArea/jHtmlArea.css" rel="stylesheet" />
For "Add Comment" button
<a type="button" class="linkButton"
data-bind="click: addComment">Add Comment</a>
To display all existing comments with Edit Delete options:
<p class="error" data-bind="text: error"></p>
<div style="" data-bind="foreach: { data: comments }, visible: comments().length > 0">
<div class="content">
<p class="error" data-bind="text: error"></p>
<span class="cname" data-bind="text: Name"></span>- <span class="cemail" data-bind=" text: Email"></span>
<div data-bind="html: HTMLComment"></div>
<a data-bind="click: $root.editComment" href="javascript:void(0)">Edit</a> |
<a data-bind="click: deleteComment" href="javascript:void(0)">Delete</a>
</div>
</div>
To display Comment Form in jQuery UI dialog for Add, Edit operations:
<div id="dialog" title="Comment"
data-bind="jqDialog: { 'autoOpen': false, 'resizable': false, 'modal': true, 'width': '60%' }, template: { 'name': 'descTmpl', 'data': selectedComment, if: selectedComment }, openDialog: selectedComment">
</div>
<script id="descTmpl" type="text/html">
<p>
Name:
<input type="text" data-bind="value: Name" />
</p>
<p>
Email:
<input type="text" data-bind="value: Email" />
</p>
<p>
Comment:
<textarea id="dialogEditor" rows="10" style="width: 100%" class="htmlEditor" data-bind="value: HTMLComment"></textarea>
</p>
<input type="button" class="linkButton" value="OK" data-bind="click: acceptChanges" />
<input type="button" class="linkButton" value="Cancel" data-bind="click: cancelChanges" />
</script>
Make sure to add jHtmlArea and Comment.js script references.
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/jqueryui")
<script src="~/Scripts/knockout-2.2.0.js"></script>
<script src="~/Scripts/jHtmlArea-0.7.5.min.js"></script>
<script src="~/Scripts/Comment.js"></script>
Conclusion:
We have implemented CRUD operations with knockout.js and asp.net web api. The main tricky part is to load html editor in jquery UI dialog on Add,Edit operations. You can download source code from GitHub. The next step is to validate html input to prevent XSS attack, Check out my next post for this.
Hope, you enjoy it.