Introduction
Here I am going to explain, how to use knockout js with ASP.NET MVC 4 application and a basic
JavaScript pattern that helps us to write a more maintainable code. The example which I use here is most suitable for single-page applications. However, it is not limited to this, you can use it in any ASP.NET MVC application according to your needs.
Background
If we are working with ASP.NET MVC framework, then its obvious that we need to work much on
JavaScript (I can say jQuery, since we can see this in almost every project). According to my experience, for a traditional
ASP.NET developer working with JavaScript is always a nightmare. Also, it was really scary that there is no server-side controls and no viewstate in MVC application. Well, once I started to work in MVC application I explored
JavaScript more along with jQuery and found it extremely easy and solid framework for web development. Most of my recent projects were implemented as a single-page application. There by implementing whole UI stuffs using
JavaScript, jQuery, jQuery-UI, knockout and some other js libraries. Initial times I phased lot of issues though, now I'm really feeling very comfortable with the framework and love this approach. I would like to say many thanks to Douglas Crockford for his fantastic book
JavaScript Good Parts, which helped me a lot to understand JavaScript better.
Using the code
I'll give you a step-by-step walk-through of the implementation by dividing them into
two sections namely, Basic Steps and Advanced Steps. You can directly jump into Advanced Steps if you are not a beginner for ASP.NET MVC application.
Basic Steps:
- File -> New -> Projects -> Templates -> Visual C# -> Web -> ASP.NET MVC 4 Web Application -> Give a friendly name and click Ok.
- Choose
Basic
from the Select template menu. Select
View Engine as Razor and click OK.
- Download and add the knockout mapping library to your project. Alternatively, you can use this nuget command Install-Package Knockout.Mapping
- Right click on the Controllers folder -> Add -> Controller, give controller name as
PersonController
and click on Add button.
- Right click on the project -> Add -> New Folder, rename it as ViewModel.
- Right click on the folder ViewModel -> Add -> class, name it as PersonViewModel and make sure you have following code inside:
public class PersonViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime DateOfBirth { get; set; }
public int CountryId { get; set; }
public Country Country { get; set; }
}
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
}
- Come back to
PersonController
and paste the following code:
public ActionResult Index()
{
ViewBag.Countries = new List<Country>(){
new Country()
{
Id = 1,
Name = "India"
},
new Country()
{
Id = 2,
Name = "USA"
},
new Country()
{
Id = 3,
Name = "France"
}
};
var viewModel = new PersonViewModel()
{
Id = 1,
Name = "Naveen",
DateOfBirth = new DateTime(1990, 11, 21)
};
return View(viewModel);
}
[HttpPost]
public JsonResult SavePersonDetails(PersonViewModel viewModel)
{
return Json(new { });
}
Also, make sure that you included the reference to View-Model.
- Right click inside the
index
method and click on Add View
,
it will popup a window, leave the default options and click on Add
button. This will add a folder called Person
under Views
folder
and have a file name Index.cshtml.
- Open RouteConfig .cs file which resides under App_Start folder. Here set
controller
as Person
. Once you done the changes
your RoutConfig file should look like this:
public class RouteConfig
{
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Person", action = "Index", id = UrlParameter.Optional }
);
}
}
- Right click on Content folder -> Add -> New Item -> select Style Sheet -> name it as
Person.css
.
- Add a new folder under Scripts folder called
Application
and right click on it and -> Add -> New Item -> select Javascript File,
name it as Person.js
.
Advanced Steps
All we done in the basic steps is a preparation for our journey. Lets summarize what we have done so far. We added a new project by keeping Person as an entity in mind. Meaning, we created PersonViewModel
,
PersonController
, and an Index.cshtml file for the view. Also, we added
Person.css and Person.js for obvious reasons. We altered RouteConfig to make Person as the default root. Included knockout mapping library to the project (other libraries comes by default with MVC 4 template). We are done with preparation and before I continue with the steps, I would like to give some introduction about knockout.
Knockout:
Knockout is a
JavaScript library which helps us to keep our view-model and UI elements synced with each other. Well, this is not the only feature of knockout. Please check http://knockoutjs.com/ for more information. Since the context of this article is to show how to star with, I'll just concentrate on the basic UI binding.
Coming to data binding concept of knockout, all we need to do is add an additional data-bind
attribute to our html elements. Ex: If our ViewModel object is in Person.ViewModel
and we want to bind Name
property to a textbox
, then we need to have the following markup:
<input data-bind="value: Person.ViewModel.Name" type="text">
likewise we need to have data-bind attribute for all fields. Now if you change value in your object, it will be reflected in the UI and vice-versa.
This is the basic feature of knockout js. We will explore more features as we move on. I think its time to continue our journey. So, here is the continued steps:
- Since we need to add
data-bind
attribute to each UI elements, it is nice to have a html helper method for this. In order to do that add a folder
called Helper to the project and add a class file (HtmlExtensions.cs) with following contents:
public static class HtmlExtensions
{
public static IHtmlString ObservableControlFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, string controlType = ControlTypeConstants.TextBox, object htmlAttributes = null)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
string jsObjectName = null;
string generalWidth = null;
switch (metaData.ContainerType.Name)
{
case "PersonViewModel":
jsObjectName = "Person.ViewModel."; generalWidth = "width: 380px";
break;
default:
throw new Exception(string.Format("The container type {0} is not supported yet.", metaData.ContainerType.Name));
}
var propertyObject = jsObjectName + metaData.PropertyName;
TagBuilder controlBuilder = null;
switch (controlType)
{
case ControlTypeConstants.TextBox:
controlBuilder = new TagBuilder("input");
controlBuilder.Attributes.Add("type", "text");
controlBuilder.Attributes.Add("style", generalWidth);
break;
case ControlTypeConstants.Html5NumberInput:
controlBuilder = new TagBuilder("input");
controlBuilder.Attributes.Add("type", "number");
controlBuilder.Attributes.Add("style", generalWidth);
break;
case ControlTypeConstants.Html5UrlInput:
controlBuilder = new TagBuilder("input");
controlBuilder.Attributes.Add("type", "url");
controlBuilder.Attributes.Add("style", generalWidth);
break;
case ControlTypeConstants.TextArea:
controlBuilder = new TagBuilder("textarea");
controlBuilder.Attributes.Add("rows", "5");
break;
case ControlTypeConstants.DropDownList:
controlBuilder = new TagBuilder("select");
controlBuilder.Attributes.Add("style", generalWidth);
break;
case ControlTypeConstants.JqueryUIDateInput:
controlBuilder = new TagBuilder("input");
controlBuilder.Attributes.Add("type", "text");
controlBuilder.Attributes.Add("style", generalWidth);
controlBuilder.Attributes.Add("class", "dateInput");
controlBuilder.Attributes.Add("data-bind", "date: " + propertyObject); break;
default:
throw new Exception(string.Format("The control type {0} is not supported yet.", controlType));
}
controlBuilder.Attributes.Add("id", metaData.PropertyName);
controlBuilder.Attributes.Add("name", metaData.PropertyName);
if (!controlBuilder.Attributes.ContainsKey("data-bind"))
{
controlBuilder.Attributes.Add("data-bind", "value: " + propertyObject);
}
if (htmlAttributes != null)
{
controlBuilder.MergeAttributes(HtmlExtensions.AnonymousObjectToHtmlAttributes(htmlAttributes), true);
}
return MvcHtmlString.Create(controlBuilder.ToString());
}
private static RouteValueDictionary AnonymousObjectToHtmlAttributes(object htmlAttributes)
{
RouteValueDictionary result = new RouteValueDictionary();
if (htmlAttributes != null)
{
foreach (System.ComponentModel.PropertyDescriptor property in System.ComponentModel.TypeDescriptor.GetProperties(htmlAttributes))
{
result.Add(property.Name.Replace('_', '-'), property.GetValue(htmlAttributes));
}
}
return result;
}
}
Also, add another class file called ViewModelConstants.cs with the following contents:
public static class ControlTypeConstants
{
public const string TextBox = "TextBox";
public const string TextArea = "TextArea";
public const string CheckBox = "CheckBox";
public const string DropDownList = "DropDownList";
public const string Html5NumberInput = "Html5NumberInput";
public const string Html5UrlInput = "Html5UrlInput";
public const string Html5DateInput = "Html5DateInput";
public const string JqueryUIDateInput = "JqueryUIDateInput";
}
The ObservableControlFor
is a simple generic method which creates relevant html element with data-bind
attribute.
By default it creates textbox but we can pass various other types which is defined in the ControlTypeConstants
. Feel free to add your own,
if would need one. In that case all you need to do is add another constant to ControlTypeConstants
and extend the switch case in ObservableControlFor
method.
If you don't understand few of the things in the method above, nothing to worry you will understand as we move on.
- Open Index.cshtml file under Views/Person folder and paste the following code:
@model Mvc4withKnockoutJsWalkThrough.ViewModel.PersonViewModel
@using Mvc4withKnockoutJsWalkThrough.Helper
@section styles{
@Styles.Render("~/Content/themes/base/css")
<link href="~/Content/Person.css" rel="stylesheet" />
}
@section scripts{
@Scripts.Render("~/bundles/jqueryui")
<script src="~/Scripts/knockout-2.1.0.js"></script>
<script src="~/Scripts/knockout.mapping-latest.js"></script>
<script src="~/Scripts/Application/Person.js"></script>
<script type="text/javascript">
Person.SaveUrl = '@Url.Action("SavePersonDetails", "Person")';
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
Person.Countries = @Html.Raw(Json.Encode(ViewBag.Countries)); </script>
}
<form>
<div class="mainWrapper">
<table>
<tr>
<td>Id :
</td>
<td>
@Html.ObservableControlFor(model => model.Id, ControlTypeConstants.Html5NumberInput)
</td>
</tr>
<tr>
<td>Name :
</td>
<td>
@Html.ObservableControlFor(model => model.Name)
</td>
</tr>
<tr>
<td>Date Of Birth :
</td>
<td>
@Html.ObservableControlFor(model => model.DateOfBirth, ControlTypeConstants.JqueryUIDateInput)
</td>
</tr>
<tr>
<td>Country (Id will be assigned):
</td>
<td>
@Html.ObservableControlFor(model => model.CountryId, ControlTypeConstants.DropDownList,
new
{
data_bind = "options: Person.Countries, optionsCaption: 'Please Choose', optionsText: 'Name', optionsValue: 'Id', value: Person.ViewModel.CountryId"
})
</td>
</tr>
<tr>
<td>Country (Object will be assigned):
</td>
<td>
@Html.ObservableControlFor(model => model.CountryId, ControlTypeConstants.DropDownList,
new
{
data_bind = "options: Person.Countries, optionsCaption: 'Please Choose', optionsText: 'Name', value: Person.ViewModel.Country"
})
</td>
</tr>
</table>
</div>
<br />
<input id="Save" type="submit" value="Save" />
</form>
Some of you may get following error:
json does not exist in the current context
You can fix this by following the steps provided in this stackoverflow
answer. Also, you may need to replace Mvc4withKnockoutJsWalkThrough
with your respective namespace.
As you can see in the code, we are using the html helper which we created in previous step (12). Also, you might notice the script written in scripts
section. That is:
Person.SaveUrl = '@Url.Action("SavePersonDetails", "Person")';
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
Person.Countries = @Html.Raw(Json.Encode(ViewBag.Countries));
Here Person
is a javascript object (which we are going to create
in the Person.js file). Theoretically we can call it as namespace (as it is the purpose here).
In razor engine, there is a limitation that you cannot use its syntax in external javascript file. Hence, we assign razor evaluated values to the properties
of Person
object.
I'll explain what exactly the line ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
and @Html.Raw(Json.Encode(ViewBag.Countries));
will do in the coming points.
- You might noticed, we are using
styles
section in the Index.cshtml
page.
Hence, it is necessary to define this in relevant layout page. In our case it is _Layout.cshtml
. So, open this page (available under
Views/Shared folder) and just above the end of head
tag, add following line:
@RenderSection("styles", required: false)
Finally, your layout page should look like this:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Title</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@RenderSection("styles", required: false)
</head>
<body>
@RenderBody()
@Scripts.Render("~/bundles/jquery")
@RenderSection("scripts", required: false)
</body>
</html>
- Now it is time to write most awaited javascript code. Open Person.js file (available under Scripts/Application folder) and paste the following code:
var Person = {
PrepareKo: function () {
ko.bindingHandlers.date = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
element.onchange = function () {
var observable = valueAccessor();
observable(new Date(element.value));
}
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var observable = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(observable);
if ((typeof valueUnwrapped == 'string' || valueUnwrapped instanceof String) &&
valueUnwrapped.indexOf('/Date') === 0) {
var parsedDate = Person.ParseJsonDate(valueUnwrapped);
element.value = parsedDate.getMonth() + 1 + "/" +
parsedDate.getDate() + "/" + parsedDate.getFullYear();
observable(parsedDate);
}
}
};
},
ParseJsonDate: function (jsonDate) {
return new Date(parseInt(jsonDate.substr(6)));
},
BindUIwithViewModel: function (viewModel) {
ko.applyBindings(viewModel);
},
EvaluateJqueryUI: function () {
$('.dateInput').datepicker();
},
RegisterUIEventHandlers: function () {
$('#Save').click(function (e) {
if (document.forms[0].checkValidity()) {
e.preventDefault();
$.ajax({
type: "POST",
url: Person.SaveUrl,
data: ko.toJSON(Person.ViewModel),
contentType: 'application/json',
async: true,
beforeSend: function () {
},
success: function (result) {
},
complete: function () {
},
error: function (jqXHR, textStatus, errorThrown) {
}
});
}
});
},
};
$(document).ready(function () {
Person.PrepareKo();
Person.BindUIwithViewModel(Person.ViewModel);
Person.EvaluateJqueryUI();
Person.RegisterUIEventHandlers();
});
Here Person
is a namespace or you can call a core object which represents the person related operations. Before I explain what these methods do,
I would like to provide some more information regarding knockout.
Something more about knockout:
So far I explained how to bind viewmodel with UI elements but did not tell how to create viewmodel. In general you can create viewmodel like this:
var myViewModel = {
Name: ko.observable('Bob'),
Age: ko.observable(123),
Report: ko.observableArray([1,5,6,7,8])
};
and you can activate the knockout like this:
ko.applyBindings(myViewModel);
By seeing the above example, it seems like we need to call ko.observable
for each of our properties. But don't worry, there is an alternative for this. The knockout provides one
plug-in for this purpose, that is in the knockout.mapping-*
library. We have already added this file into our project in the step 3. Also, we used it once in the step 13, i.e. :
Person.ViewModel = ko.mapping.fromJS(@Html.Raw(Json.Encode(Model)));
ko.mapping.fromJS
creates appropriate view-model for us from the
JavaScript object provided by server. This way we have the view-model with us in Person.ViewModel
. Hence, all the properties of Person.ViewModel
is observable and we need to access it with function syntax.
Ex: We can retrieve the person name like Person.ViewModel.Name()
and set the value like Person.ViewModel.Name('New Name')
. As you noticed Person.ViweModel
is no more suitable for saving. Meaning, if you pass Person.ViewModel
directly to the server, it will not map it to relevant .net object. Hence, we need to get the plain
JavaScript object back from the ko. We can do that using ko.toJSON
function.
ko.toJSON(Person.ViewModel)
In step 13, we are also having below line:
Person.Countries = @Html.Raw(Json.Encode(ViewBag.Countries));
This line assigns the countries collection available in the ViewBag to our javascript object. You may notice a red underscore nearby semi-colon(;) making you feel that there is an error (Also, in Error List, you will see a warning Syntax error
). This is a bug in VS-2012 and you can ignore it without any worries.
Note: I used ViewBag to keep our demonstration simple instead of creating complex ViewModel. In real projects, I recommend to use ViewModel for these kind of data.
It
is time to explore the individual functions defined in Person
object. So, here we go with one by one:
PrepareKo: The purpose of this function is to set-up the ko or extend the default functionality. In the code pasted above, I'm creating my own binding handler to handle the date. This is required because of non-compatible JSON date format serialization done by .net. The explanation of what exactly I'm doing here is out of the scope of this article. Hence I'm omitting it. (If anyone interested, please let me know in comments. I'll be happy to explain).
Here is the sample usage of this binding handler:
<input data-bind="date: AnyDate" type="text">
In our code, we can see the usage in the following line of step 12:
controlBuilder.Attributes.Add("data-bind", "date: " + propertyObject);
ParseJsonDate
: This is a utility function to convert JSON date into
JavaScript date.
BindUIwithViewModel
: As the name suggests, this function binds the passed viewModel to UI elements.
EvaluateJqueryUI
: To write jQuery UI related operations. Currently, datepicker is evaluated.
RegisterUIEventHandlers
: This function is to register the event handlers for UI elements. Currently, click event is registered for an element; with the Id Save
. This save function first validates the page, prevents its default functionality and triggers an
AJAX request to the URL specified in
Person.SaveUrl
. As the URL is generated from the server using Url.Action
, it will be proper and we need not to worry about the domain name or virtual directory.
This is all about Person object. So we are ready with all ingredients and its time to cook That is, once the document is ready, we can call relevant functions one by one in the preferable order.
That
is it! we are done, run the project and see the result.
Points of Interest
We have built a page with 100s of basic UI elements and 7 rich elements i.e. Jqx Grids x 3 (Three Tabs with same no. of controls), which deals with a large
JSON data. The page works very smoothly and we are not seeing any performance issues. Coming to the code, it is something more complex than what I described here. The used
JavaScript pattern is similar to this thought, it was split into various files again (having trust on bundling and minification! ).
The final thing that every traditional
ASP.NET developer wondering is about zero server-side code (well, excluding razor in between) for UI manipulation purpose. For me theoretically it makes more sense to say, "Let client do its work. We are only to serve data".
History
- 25th November, 2013 - 1.1.0 - Updated with observable dropdownlist.
- 26th September, 2013 - 1.0.1 - Attached the sample project.
- 25th September, 2013 - 1.0.0 - Initial version.