Click here to Skip to main content
15,867,704 members
Articles / Web Development / ASP.NET

Using KnockoutJS in your ASP.NET applications

Rate me:
Please Sign up or sign in to vote.
4.94/5 (9 votes)
6 Feb 2011CPOL9 min read 88.6K   2.1K   41   9
How to use the KnockoutJS library in your ASP.NET applications.

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:

ASP.NET
<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:

  1. First of all, you should attach the knockout-1.1.2.js file.
  2. 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.
  3. 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.
  4. 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.NET
<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:

HTML
<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:

C#
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:

ASP.NET
<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:

C#
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:

JavaScript
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:

JavaScript
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:

JavaScript
$(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:

HTML
<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:

JavaScript
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:
ASP.NET
<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
C#
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:

JavaScript
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 divs on the page and assign different View Models to each of them. The problem is that these divs 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:

JavaScript
var mainViewModel = {
    viewModelForPage: { someProperty: "someValue" },
    viewModelForControl1: { anotherProperty: 1 },
    viewModelForControl2: { anotherProperty: 2 },
};

In this case, we could reference the necessary properties like this:

ASP.NET
<tr data-bind="visible: viewModelForControl2.anotherProperty">

There are several things to do here:

  1. How to create unique names of submodels inside the main View Model for each page\control?
  2. How to merge all submodels into the main View Model?
  3. 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:

ASP.NET
<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:

C#
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:
JavaScript
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:

JavaScript
<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:
ASP.NET
<%@ 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
ASP.NET
<%@ 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:

JavaScript
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:

C#
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:
ASP.NET
<%@ 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:
C#
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finstek
China China
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionSuggestion for persisting into HiddenField Pin
Andreas Niedermair2-Apr-13 20:15
Andreas Niedermair2-Apr-13 20:15 
QuestionjQuery, submit() and ASP.NET Pin
peter.morlion13-Mar-13 0:23
peter.morlion13-Mar-13 0:23 
AnswerRe: jQuery, submit() and ASP.NET Pin
peter.morlion19-Mar-13 5:16
peter.morlion19-Mar-13 5:16 
QuestionExcellent Pin
chauhan.munish18-Feb-13 0:50
chauhan.munish18-Feb-13 0:50 
QuestionNice Pin
englishchan22-Oct-12 21:14
englishchan22-Oct-12 21:14 
GeneralMy vote of 5 Pin
Howard Richards4-Sep-12 5:59
Howard Richards4-Sep-12 5:59 
QuestionInteresting! Pin
markus folius23-Aug-11 3:58
markus folius23-Aug-11 3:58 
AnswerRe: Interesting! Pin
Ivan Yakimov23-Aug-11 21:47
professionalIvan Yakimov23-Aug-11 21:47 
GeneralRe: Interesting! Pin
chauhan.munish18-Feb-13 0:52
chauhan.munish18-Feb-13 0:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.