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

Building ASP.NET applications using Knockout.js with a server side defined view model

Rate me:
Please Sign up or sign in to vote.
4.91/5 (9 votes)
1 Nov 2012CPOL5 min read 35.4K   976   41   12
Let's build a simple framework that allows to have the power of knockout.js with no JavaScript coding.

Introduction 

Knockout.js is an open source javascript library that allows to apply the Model View ViewModel architectural pattern (MVVM) to web pages. It's a great library that simplifies the development of complex pages with many user interactions.

The only problem is that more code has to be written on the client side, and everybody knows that coding on the client side is much more difficult and time consuming than coding server side (less intellisense support, no compile time check of errors, etc...) 

This small application will show how to define the model, together with it's main functions, in the server side code (C#) without losing the power of client side programming. In this way the code will be more structured and there will be more compile time check on its consistency before deploying it. 

In this example we demonstrate we can build a complex responsive interface without writing a single javascript line of code. Of course moving some processing on the client side with computed fields and functions would improve performance, but when you don't have big models the difference can be really small.

Background

Before reading this article it's important you are familiar with Knockout.js library. For more information about it see the official web site

Using the code 

The sample application is a Visual Studio 2010 web application  that displays an order with its details displayed in paged style.  You can perform some basic operations on the order (change data, edit main data, change prices, quantities, delivery status) and save it. Order data is saved in an xml file Order.xml which is stored in the root directory.   

The page layout is coded in the KnockoutServerSideViewModel.ascx user control. This user control loads the model from the xml file and stores it in an hidden field automatically created by the user control base class (BaseKOUserControl). Another view model  is defined in the DateTime.ascx user control, just to demonstrate the use of more user controls run by a separate view model in the same page.  

From KnockoutServerSideViewModel.ascx:  

ASP.NET
<%@ Control Language="C#" AutoEventWireup="true" 
	      CodeBehind="KnockoutServerSideViewModel.ascx.cs"
	      Inherits="KnockoutServerSideViewModel.Web.KnockoutServerSideViewModel" %>
<%@ Register src="Pager.ascx" tagname="Pager" tagprefix="uc1" %>
<table id="bindingArea" border="1" cellpadding="10">
    <tr>
        <td>
            First name:
        </td>
        <td>
            <span data-bind="text: FirstName, visible: !MainEditing()" ></span>
            <input data-bind="value: FirstName, visible: MainEditing()" ></input>
        </td>
    </tr>
    <tr>
        <td>
            Last name:
        </td>
        <td>
            <span data-bind="text: LastName, visible: !MainEditing() " ></span>
            <input data-bind="value: LastName, visible: MainEditing()" ></input>
        </td>
    </tr>
    <tr>
        <td>
            Date:
        </td>
        <td>
            <span data-bind="text: LastSavedTime" ></span>
        </td>
    </tr>
    <tr data-bind="visible:TimesSaved()>0">
        <td>
            Saved:
        </td>
        <td>
            <span data-bind="text:TimesSaved"></span>&nbsp;time
              <span data-bind="visible:TimesSaved()>1">s</span>
        </td>
    </tr>
     <tr>
        <td colspan="2">
            <input type="button" value="Edit main data" 
              data-bind="visible: !MainEditing(), click: c$.bind($data, 'MainEdit')" ></input>
            <input type="button" value="Close main data" 
              data-bind="visible: MainEditing(), click: c$.bind($data, 'MainClose') " ></input>
        </td>
    </tr>
    <tr>
        <td colspan="3">
            <input type="button" value="Save" 
               data-bind="click: c$.bind($data, 'Save')" />
            <input type="button" value="Save and close" 
              data-bind="click: c$.bind($data, 'SaveAndClose')" />
        </td>
    </tr>
    <tr>
        <td colspan="2">
            <table border="1" cellpadding="5">
                <tr>
                    <td colspan="10">
                        <uc1:Pager ID="Pager2" runat="server" />
                    </td>
                </tr>
                <tr>
                    <td>
                        Code
                    </td>
                    <td>
                        Name
                    </td>
                    <td>
                        Quantity
                    </td>
                    <td>
                        Price
                    </td>
                    <td>
                        Total
                    </td>
                    <td>
                        Delivered
                    </td>
                </tr>
                
                <tbody data-bind="foreach: Details">
                    <tr>
                        <td>
                            <span data-bind="text: Key" />
                        </td>
                        <td>
                            <span data-bind="text: Name, visible: !Editing()"></span>
                            <input type="text" data-bind="value: Name, visible: Editing()" />
                        </td>
                        <td>
                            <span data-bind="text: Quantity, visible: !Editing()"></span>
                            <select data-bind="value: Quantity, visible: Editing()">
                            <option value="1">1</option>
                            <option value="2">2</option>
                            <option value="3">3</option>
                            <option value="4">4</option>
                            <option value="5">5</option>
                            </select>
                        </td>
                        <td>
                            <span data-bind="text: UnitPrice, visible: !Editing()"></span>
                            <input type="text" data-bind="value: UnitPrice, visible: Editing()"></input>
                            <span data-bind="visible: PriceNotValid()" style="color: Red">Price not valid</span>
                        </td>
                        <td>
                            <span data-bind="text: Total" />
                        </td>
                        <td>
                            <span data-bind="text: Delivered" />
                        </td>
                        <td>
                            <input type="button" value="Set delivered" 
                               data-bind="click: $root.c$.bind($data, 'SetDelivered', $data.Key()), visible: Delivered() == false" />
                            <input type="button" value="Set not delivered" 
                               data-bind="click: $root.c$.bind($data, 'SetNotDelivered', $data.Key()), visible: Delivered() == true" />
                            <input type="button" value="Increase price (2 €)" 
                               data-bind="click: $root.c$.bind($data, 'AddTwoEuro', $data.Key())" />
                            <br />
                            <input type="button" value="Edit" 
                               data-bind="click: $root.c$.bind($data, 'Edit', $data.Key()), visible: !Editing()" />
                            <input type="button" value="Close" 
                               data-bind="click: $root.c$.bind($data, 'Close', $data.Key()), visible: Editing()" />
                            <input type="button" value="Delete" 
                               data-bind="click: $root.c$.bind($data, 'Delete', $data.Key())" />
                        </td>
                    </tr>
                </tbody>
                
                <tr>
                    <td colspan="10">
                        <uc1:Pager ID="Pager1" runat="server" />
                    </td>
                </tr>
                
            </table>
        </td>
    </tr>
</table>

From KnockoutServerSideViewModel.ascx.cs

C#
public partial class KnockoutServerSideViewModel : BaseKOUserControl
{
    protected override void OnInit(EventArgs e)
    {
        ViewModel = OrderViewModel.GetPagedOrder(1,10);
        base.OnInit(e);
    }
}

From BaseKOUserControl.cs:  

C#
private BaseViewModel _ViewModel;
public BaseViewModel ViewModel
{
    get
    {
        return _ViewModel;
    }
    set
    {
        _ViewModel = value;
        _ViewModel.ModelClass = string.Format("{0}.{1}, {2}", 
           _ViewModel.GetType().Namespace, _ViewModel.GetType().Name, 
           _ViewModel.GetType().Assembly.GetName().FullName);
    }
}

protected override void Render(System.Web.UI.HtmlTextWriter writer)
{
    writer.WriteLine(string.Format("<span id=\"sp{0}\">", this.ClientID));
    base.Render(writer);
    writer.WriteLine(string.Format("</span>"));
    writer.WriteLine(string.Format("<script type=\"text/javascript\">" + 
      "\n$().ready(function () {{ var model = setModelFunction(\"{0}\", " + 
      "\"{1}\"); model.init(); ko.applyBindings(model.viewModel, " + 
      "document.getElementById(\"sp{0}\")); }});\n</script>", 
      this.ClientID, this.ControlName));
    writer.WriteLine(string.Format("<input type=\"hidden\" id " + 
      "= \"hd{0}\" value=\"{1}\" />", 
      this.ClientID, HttpUtility.HtmlEncode(Utilities.ConvertToJson(ViewModel))));
}

The Render method is overridden in order to add the necessary javascript code to start the knockout binding. An hidden field containing the model (serialized in JSON) is added at the end of the user control, and this is surrounded by a span that  allows to have partial knock out binding, so you can have more user controls with server side binding in the same page and they will not disturb one each other.    

The ViewModel object inherits the BaseViewModel, which is a simple class defined like this:  

C#
public class BaseViewModel
{
   public BaseModel() { }
   public string Function { get; set; }
   public string ModelClass { get; set; }
   public string Argument { get; set; }
   public string RedirectUrl { get; set; } 
}

The ModelClass property contains the complete .NET description (including assembly and namespaces) of the class  used as ViewModel, and it's set at the beginning of the definition of the ViewModel, during the loading of the page:   

C#
_ViewModel.ModelClass = string.Format("{0}.{1}, {2}", 
           _ViewModel.GetType().Namespace, _ViewModel.GetType().Name, _ViewModel.GetType().Assembly.GetName().FullName);

This is important as the ViewModel instance is passed from the page to server side processing by means of a web service, and knowing the class description allows to deserialize it in the correct type.

The RedirectUrl propery is used to tell the page to redirect to a new url after a call to a server side function (see method SaveAndClose() for an example).

The web service is located in the default.aspx page: 

C#
 [WebMethod]
public static string CallModelFunction(string jsmodel, string className)
{
    Type t = System.Type.GetType(className);
    BaseModel model = Utilities.ConvertFromJson(jsmodel, t) as BaseModel;
    t.InvokeMember(model.Function, System.Reflection.BindingFlags.InvokeMethod, System.Type.DefaultBinder, model, null);
    return Utilities.ConvertToJson(model);
} 

The BaseViewModel class defines a property named Function which contains the function to be called server side. It is supposed that this function exists as a non static public member of the model class (in the example the model class is OrderViewModel). For example, the Save() method of the model is defined as a non static public member of the  OrderViewModel class:   

C#
public void Save()
{
    this.TimesSaved++;
    this.LastSavedTime = DateTime.Now.ToLongDateString() + " " + DateTime.Now.ToLongTimeString();
    PersistModel();
}

All the functions in the model can be defined as void as the responsibility of returning the updated model to the page is demanded to the service.   

In order to correctly define the viewModel in the page (we are on client side now) we used the ko.mapping plugin which takes a json string and deserialize it in a model with observable fields. 

This is the KnockOutBaseManager.js files that contains all the needed functionality to map the model (contained in the viewModel hidden field) and apply the bindings to the page. It also contains the CallModelFunction function that manages the communication with the server side application (it passes the serialized model to the service and maps the returned model to the page). The c$ function can be placed in the knockout binding to call any server side function in the view model. It also accepts an additional argument that is placed in the Argument property of the view model class (for example, to identify in which row we pressed a button) 

JavaScript
 function callFunction(functionName, viewModel) {
    viewModel.Function(functionName);
    $.ajax({
        type: "POST",
        url: "/Default.aspx/CallModelFunction",
        data: ko.toJSON({ jsmodel: ko.toJSON(viewModel), className: viewModel.ModelClass() }),
        contentType: "application/json",
        success: function (result) {
            ko.mapping.fromJSON(result, viewModel);
            if (viewModel.RedirectUrl() != "" && viewModel.RedirectUrl() != null) {
                document.location.href = viewModel.RedirectUrl();
            }
        }
    });
}

function setModelFunction(area, name) {

    var model = function (area) {
        var serviceUrl = "/Default.aspx/";
        //var proxy = new ServiceProxy(serviceUrl);
        var viewModel = ko.mapping.fromJSON($("#hd" + area).val());
        
        viewModel.c$ = function (functionName) {
            var argument = arguments[1];
            if (typeof argument == 'string' || typeof argument == 'number') {
                viewModel.Argument(argument);
            }
            callFunction(functionName, viewModel);
        }
        var init = function () {
            if (typeof window["setup_" + name] == 'function') {
                window["setup_" + name](viewModel);
                //setup(viewModel);
            }
        };
        return {
            init: init,
            viewModel: viewModel
        };
    } (area);
    return model;
}

The BaseViewModel class contains a property named Argument that is used to pass any custom value that is needed by the server side function to do it's job. For example, in the AddTwoEuro function it is used to pass the index of the detail on which we want to apply the price increase (of course you could use it also to pass the increase value).   

The model definition is located in the Order.cs file. Server side code is really not optimized and it's not very elegant, but the purpose of the example was not to build an elegant Data Access Layer so I decided not to lose too much time on it (If you feel offended by it, just accept my sincere apologize Smile | <img src= ) 

So, let's summarize the steps that you have to follow if you want to define a new user control with its own model: 

  1. Create a user control that inherits BaseKOUserControl. Its name will be the name of the class, or you can change it by setting the value of the Control name property in the Page_Init method. 
  2. Define a viewModel class that inherits from BaseViewModel.  
  3. Define the ViewModel property in the Page_Load method   
  4. Create the html code with bindings in the usercontrol ASCX file    
  5. Add the necessary functions to the view model class.
  6. Run it!     

This tool was created with the invaluable help of the "Banco del Mutuo Soccorso", a progressive rock band of the 70s Smile | <img src= 

Ciao!       

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)
Italy Italy
Paolo Costa is a software developer with long experience on any kind of .NET application. He lives in Italy and works in Switzerland for a credit card payment acquiring company.

Comments and Discussions

 
QuestionIt is a nice article to help to learn Knockout, but it is not a right way to use this in practical, right? Pin
wyx20003-Mar-14 11:29
wyx20003-Mar-14 11:29 
AnswerRe: It is a nice article to help to learn Knockout, but it is not a right way to use this in practical, right? Pin
Paolo Costa16-Oct-14 9:13
Paolo Costa16-Oct-14 9:13 
Questionupload file to folder and save file name in the database Pin
ravikhoda17-Dec-13 1:57
professionalravikhoda17-Dec-13 1:57 
Questionmy vote is 5 and above Pin
zensim11-Apr-13 4:10
zensim11-Apr-13 4:10 
QuestionExcellent article Pin
zensim11-Apr-13 4:09
zensim11-Apr-13 4:09 
GeneralMy vote of 5 Pin
KEVIN.HENG.SHI5-Nov-12 2:31
KEVIN.HENG.SHI5-Nov-12 2:31 
Questionvery good. Pin
Quentin in SA29-Oct-12 18:13
Quentin in SA29-Oct-12 18:13 
Thanks for sharing.
GeneralMy vote of 5 Pin
Sanjay K. Gupta29-Oct-12 3:41
professionalSanjay K. Gupta29-Oct-12 3:41 
GeneralRe: My vote of 5 Pin
Paolo Costa29-Oct-12 4:51
Paolo Costa29-Oct-12 4:51 
GeneralRe: My vote of 5 Pin
Sanjay K. Gupta29-Oct-12 5:48
professionalSanjay K. Gupta29-Oct-12 5:48 
GeneralRe: My vote of 5 Pin
Paolo Costa30-Oct-12 1:22
Paolo Costa30-Oct-12 1:22 
GeneralRe: My vote of 5 Pin
Paolo Costa8-Nov-12 6:38
Paolo Costa8-Nov-12 6:38 

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.