Introduction
Recently, I came across a great JavaScript framework KnockoutJS. It simplifies development of user interfaces by implementing data binding functionality. In this article, I want to briefly describe it and talk about problems in its usage in ASP.NET applications.
Prerequisites
To work with the sample from this article, you should download the latest version of KnockoutJS. Also, I have used Visual Studio 2010 to write my code. But I'm sure that everything will work with Visual Studio 2008.
Introduction to KnockoutJS
Let's start with a very simple example of KnockoutJS usage. Here is the code of the Web page:
<script type="text/javascript" src="Scripts/knockout-1.1.2.js"></script>
<table border="1">
<tbody><tr>
<td>
Change visibility of next row:
<input type="checkbox" data-bind="checked: isRowVisible" />
</td>
</tr>
<tr data-bind="visible: isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</tbody></table>
<script type="text/javascript">
var viewModel = { isRowVisible: ko.observable(false) };
ko.applyBindings(viewModel);
</script>
There are some things I should mention here:
- First of all, you should attach the knockout-1.1.2.js file.
- Then you should make bindings by applying
data-bind
attributes to any HTML element you like. I believe the syntax is clear. data-bind="visible: isRowVisible"
means that this element is visible if the isRowVisible
property is true. What is the isRowVisible
property for? Wait a moment. - And at last, there is the heart of KnockoutJS. In a script block, you should create the
viewmodel
. viewmodel
is a simple JavaScript object with properties. These properties will be used in bindings. isRowVisible
is a property of the viewmodel
. As you can see, the values of these properties are assigned with the help of the ko.observable
function. This function allows the system to track changes in the properties and send them into the bound HTML elements. - And the last thing is the call to
ko.applyBindings(viewModel)
. It makes all the magic work.
These are the basics of the usage of KnockoutJS. You may refer to the official documentation on the site to know more. Now I'd like to go to the usage of KnockoutJS in ASP.NET.
Binding to ASP.NET controls
In the previous example, I used the usual HTML input
element. It is time to test with an ASP.NET control. Let's try to use it with asp:Checkbox
. It appears the code:
<asp:CheckBox ID="chkChangeVisibility" runat="server" data-bind="checked: isRowVisible" />
does not work. The reason becomes obvious if we look at the generated HTML code:
<span data-bind="checked: isRowVisible">
<input id="chkChangeVisibility" type="checkbox" name="chkChangeVisibility" />
</span>
Our data-bind
attribute is not in the input
element. In order to place it in the input
element, we have to use the code-behind file. For example:
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] = "checked: isRowVisible";
}
Now everything works fine.
Persisting the View Model between postbacks
It is usual for an ASP.NET page to make postbacks to the server. If we make a postback on the page from our sample, then it will appear that after the postback, all our changes are lost. This is a usual problem with JavaScript-made changes. Our View Model will be loaded again from scratch and the initial state will be set again.
But of course, we'd like to save all our changes. The approach I suggest is very similar to the way ViewState is persisted in ASP.NET. We will store our View Model in a hidden field. Let's add a hidden field to our page:
<input type="hidden" runat="server" id="hViewStateStorage" />
Now we should write our View Model into this field. In most cases, the initial state of controls is defined on the server side. We'll use this technique too. On the server side, we'll create an object of the View Model, serialize it in JSON format, and place the JSON string in the hidden field:
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
As you can see, I have used an anonymous class here. It means that you don't even need to create a separate class for the View Model on the server side.
Another tempting possibility here is in changing the View Model while postback. As it is stored in a hidden field, you can extract the View Model from there, deserialize it, analyze and change its properties, serialize it, and put it in the hidden field again. In this case, you'll need to create a separate class for the View Model to deserialize the Model to.
Now we have to extract the View Model from the hidden field in the JavaScript code. Here is how you do it:
var stringViewModel = document.getElementById('<%=hViewStateStorage.ClientID %>').value;
var viewModel = ko.utils.parseJson(stringViewModel);
Here I have used the parseJson
function from the KnockoutJS library to convert the string representation to a JavaScript object.
But now we face a little problem. As I said, all properties of the View Model should be initialized using the ko.observable
function. Now this is not the case. The following code solves the problem:
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.observable(viewModel[propertyName]);
}
Now the View Model comes from the server perfectly. The only thing to do is to save it into the hidden field before postback. I have used jQuery to subscribe to the postback event:
$(document.forms[0]).submit(function () {
document.getElementById('<%=hViewStateStorage.ClientID %>').value =
ko.utils.stringifyJson(viewModel);
});
You may think this code will work. But it does not. After the first postback, everything stops working. It appears that our hidden field contains an object without any properties now:
<input name="hViewStateStorage" type="hidden"
id="hViewStateStorage" value="{}" />
What is the reason? It is in the function ko.observable
. In fact it returns the function, not an ordinal value. It means that all the properties of our View Model are functions now. So they are not serialized in JSON format. To return all properties to their "non-functional" state, we must use the ko.utils.unwrapObservable
function:
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.utils.unwrapObservable(viewModel[propertyName]);
}
Our goal is achieved. We implemented persistence of View Model between postbacks. Here is the full code for our page:
KnockoutJsSample.aspx:
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
<script type="text/javascript">
var stringViewModel =
document.getElementById('<%=hViewStateStorage.ClientID %>').value;
var viewModel = ko.utils.parseJson(stringViewModel);
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.observable(viewModel[propertyName]);
}
ko.applyBindings(viewModel);
$(document.forms[0]).submit(function () {
for (var propertyName in viewModel) {
viewModel[propertyName] = ko.utils.unwrapObservable(viewModel[propertyName]);
}
document.getElementById('<%=hViewStateStorage.ClientID %>').value =
ko.utils.stringifyJson(viewModel);
});
</script>
KnockoutJsSample.aspx.cs
public partial class KnockoutJsSample : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] = "checked: isRowVisible";
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
}
}
Using KnockoutJS in independent ASP.NET controls and pages
So far everything is great. But let's consider the following scenario. I'd like to use KnockoutJS in an ASP.NET control. And this control could be inserted into a page which uses KnockoutJS as well. Furthermore, I'd like to place several instances of this control into one page. And also, I may place on this page other controls which use KnockoutJS. I believe you can already see the problem. How can different View Models with probably the same names of properties work on one page?
Well, KnockoutJS has a mechanism to work with several View Models on the same page. Do you remember the ko.applyBindings
function? It may accept the second parameter, so call the context:
ko.applyBindings(viewModel, document.getElementById('someDivId'));
This context is a DOM element. In this case, elements will be bound to the View Model only inside this DOM element. So you can make several div
s on the page and assign different View Models to each of them. The problem is that these div
s can't be nested. In my opinion, it is a big limitation preventing free usage of KnockoutJS in ASP.NET. We may not be sure that one View Model will not interfere with another. Of course, we can design our pages and controls very carefully, trying to avoid nesting of controls with KnockoutJS, but in any case, this is a source of possible errors, I think.
The method I suggest is merging all View Models on the page into a single View Model. Something like this:
var mainViewModel = {
viewModelForPage: { someProperty: "someValue" },
viewModelForControl1: { anotherProperty: 1 },
viewModelForControl2: { anotherProperty: 2 },
};
In this case, we could reference the necessary properties like this:
<tr data-bind="visible: viewModelForControl2.anotherProperty">
There are several things to do here:
- How to create unique names of submodels inside the main View Model for each page\control?
- How to merge all submodels into the main View Model?
- How to implement persisting of submodels?
Let's solve these problems. First of all, we already have unique names in ASP.NET. I'm talking about the ClientID
property of each page\control. I suggest all our submodels will have names of ClientID
of the corresponding page\control. In this case, we can reference the properties of our submodles like this:
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
Unfortunately, this approach does not work with server-side controls (controls with the attribute runat="server"
). For these controls, you should set the data-bind
attribute in the code-behind file:
chkChangeVisibility.InputAttributes["data-bind"] =
string.Format("checked: {0}.isRowVisible", this.ClientID);
Considering that sometimes we must use InputAttributes
instead of Attributes
, this approach is unavoidable in any case.
Now let's look at system merging all our Models into a single one. If we want to have only one instance of something in our code, we must use Singleton pattern. But for the sake of simplicity, I'll just create a global variable in a JavaScript file which I'll attach to all our pages\controls:
KnockoutSupport.js:
var g_KnockoutRegulator = {
ViewModel: {},
LoadViewModel: function (clientId, storageFieldId) {
var stringViewModel = document.getElementById(storageFieldId).value;
var clientViewModel = ko.utils.parseJson(stringViewModel);
var partOfBigViewModel = {};
this.ViewModel[clientId] = partOfBigViewModel;
for (var propertyName in clientViewModel) {
this.ViewModel[clientId][propertyName] =
ko.observable(clientViewModel[propertyName]);
}
$(document.forms[0]).submit(function () {
var newViewModel = {};
for (var propertyName in partOfBigViewModel) {
newViewModel[propertyName] =
ko.utils.unwrapObservable(partOfBigViewModel[propertyName]);
}
document.getElementById(storageFieldId).value =
ko.utils.stringifyJson(newViewModel);
});
ko.applyBindings(this.ViewModel);
}
};
Let's take a closer look at this code. g_KnockoutRegulator
is a JavaScript object with a ViewModel
property. This property is our main View Model which is bound to all controls. The only function LoadViewModel
does all the magic. It gets the ClientID of the current page\control (clientId
) and the ClientID of the hidden storage field (storageFieldId
). Inside, it extracts the local View Model from the storage field and loads it into the main View Model as one of its properties (the first for
loop). Then it subscribes for the Submit event where it does the opposite operation, storing the local View Model into the storage field. And at last, it binds the main View Model to the controls.
The usage of the g_KnockoutRegulator
object is very simple:
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
Here are the full source codes of the page and control:
KnockoutJsSample.aspx:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="KnockoutJsSample.aspx.cs"
Inherits="KnockoutJsTest.KnockoutJsSample" %>
<%@ Register Src="~/UserControls/TestControl.ascx"
TagPrefix="uc" TagName="TestControl" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/KnockoutSupport.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<uc:TestControl runat="server" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
</form>
</body>
</html>
TestControl.ascx
<%@ Control Language="C#" AutoEventWireup="true"
CodeBehind="TestControl.ascx.cs"
Inherits="KnockoutJsTest.UserControls.TestControl" %>
<script src="Scripts/knockout-1.1.2.js" type="text/javascript"></script>
<script src="Scripts/jquery-1.4.1.min.js" type="text/javascript"></script>
<script src="Scripts/KnockoutSupport.js" type="text/javascript"></script>
<input type="hidden" runat="server" id="hViewStateStorage" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<script type="text/javascript">
g_KnockoutRegulator.LoadViewModel('<%= this.ClientID %>',
'<%=hViewStateStorage.ClientID %>');
</script>
As you can see, both the page and control use the same names of properties of their View Models. But they do not interfere with each other. This is exactly what we want.
Some optimization
We have created the main framework to work with KnockoutJS in ASP.NET. The only thing I don't like here is the call to ko.applyBindings
. It is called for every control using KnockoutJS. But it is obvious that one call would be enough. The only thing to consider is that this call must be made after all controls have loaded their local View Models into the main one. How can we achieve this? I'll modify the KnockoutSupport.js file like this:
var g_KnockoutRegulator = {
NumberOfLocalViewModels: 0,
ViewModel: {},
LoadViewModel: function (clientId, storageFieldId) {
...
this.NumberOfLocalViewModels--;
if (this.NumberOfLocalViewModels == 0) {
ko.applyBindings(this.ViewModel);
}
}
};
Here, the field NumberOfLocalViewModels
must be set to the correct number of pages\controls loading the local View Models. How do we get this number? I'll do it using the RegisterClientScriptBlock
and RegisterStartupScript
methods of the ClientScriptManager
object. All code registered using RegisterClientScriptBlock
is executed before all code registered using RegisterStartupScript
. So here is my helper class to register the necessary JavaScript code:
public class KnockoutJsHelper
{
public static void RegisterKnockoutScripts(Control control,
HtmlInputHidden storageField)
{
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("KnockoutJS"))
{
control.Page.ClientScript.RegisterClientScriptInclude("KnockoutJS",
control.Page.ResolveClientUrl(@"~\Scripts\knockout-1.1.2.js"));
}
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("jQuery"))
{
control.Page.ClientScript.RegisterClientScriptInclude("jQuery",
control.Page.ResolveClientUrl(@"~\Scripts\jquery-1.4.1.js"));
}
if (!control.Page.ClientScript.IsClientScriptIncludeRegistered("KnockoutRegulator"))
{
control.Page.ClientScript.RegisterClientScriptInclude("KnockoutRegulator",
control.Page.ResolveClientUrl(@"~\Scripts\KnockoutSupport.js"));
}
control.Page.ClientScript.RegisterClientScriptBlock(control.GetType(),
"IncreaseNumberOfViewModels" + control.ClientID,
"g_KnockoutRegulator.NumberOfLocalViewModels++;", true);
control.Page.ClientScript.RegisterStartupScript(control.GetType(),
"RegisterViewModelScripts" + control.ClientID,
string.Format("g_KnockoutRegulator.LoadViewModel('{0}', '{1}');",
control.ClientID, storageField.ClientID), true);
}
}
Using this class, we don't need references to .js files or a manual call to the g_KnockoutRegulator
object. The last two lines do the main work. The first line registers the script, increasing the number of local View Models to be loaded. The last line does the actual loading. Here is an example of a page with KnockoutJS using this KnockoutJsHelper
class:
KnockoutJsSample.aspx:
<%@ Page Language="C#" AutoEventWireup="true"
CodeBehind="KnockoutJsSample.aspx.cs"
Inherits="KnockoutJsTest.KnockoutJsSample" %>
<%@ Register Src="~/UserControls/TestControl.ascx"
TagPrefix="uc" TagName="TestControl" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<input type="hidden" runat="server" id="hViewStateStorage" />
<uc:TestControl runat="server" />
<table border="1">
<tr>
<td>
Change visibility of next row:
<asp:CheckBox ID="chkChangeVisibility" runat="server" />
</td>
</tr>
<tr data-bind="visible: <%= this.ClientID %>.isRowVisible">
<td>
This is a row which visibility can be changed.
</td>
</tr>
</table>
<input runat="server" type="submit" />
</form>
</body>
</html>
KnockoutJsSample.aspx.cs:
public partial class KnockoutJsSample : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
chkChangeVisibility.InputAttributes["data-bind"] =
string.Format("checked: {0}.isRowVisible", this.ClientID);
if (!IsPostBack)
{
var viewModel = new { isRowVisible = true };
var serializer = new JavaScriptSerializer { MaxJsonLength = int.MaxValue };
var json = serializer.Serialize(viewModel);
hViewStateStorage.Value = json;
}
}
protected override void OnPreRender(EventArgs e)
{
KnockoutJsHelper.RegisterKnockoutScripts(this, hViewStateStorage);
}
}
Conclusion
That is all I wanted to say about the usage of KnockoutJS with ASP.NET. I hope it will give you a good starting point. Thank you!
History
- 07.02.2011: Initial revision.