Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC 4 with Knockout Js

0.00/5 (No votes)
24 Nov 2013 13  
Tips to make Asp.Net MVC4 UI development easier with the flavor of knockout js.

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: 

  1. File -> New -> Projects -> Templates -> Visual C# -> Web -> ASP.NET MVC 4 Web Application -> Give a friendly name and click Ok.
  2. Choose Basic from the Select template menu. Select View Engine as Razor and click OK. 
  3. Download and add the  knockout mapping library to your project.  Alternatively, you can use this nuget command Install-Package Knockout.Mapping 
  4. Right click on the Controllers folder ->  Add -> Controller, give controller name as PersonController and click on Add button.
  5. Right click on the project -> Add ->  New Folder, rename it as ViewModel.
  6. Right click on the folder ViewModel -> Add -> class, name it as PersonViewModel and make sure you have following code inside:  
  7.   public class PersonViewModel
        {
            public int Id { get; set; }
            public string Name { get; set; }
            public DateTime DateOfBirth { get; set; }
    
            // NOTE: For demonstration purpose included both Id as well as strong reference. In real projects, we need to use either of one (Id is preferred in case of performance).
            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; }
        }  
    
  8. Come back to PersonController and paste the following code:
  9. public ActionResult Index()
    {
        // NOTE: We should have a wrapper ViewModel instead of ViewBag or ViewData. This is used here to keep the demonstration simple.
        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)
    {
        // TODO: Save logic goes here.
    
        return Json(new { });
    } 

    Also, make sure that you included the reference to View-Model. 

  10. 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
  11. 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: 
  12. 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 }
            );
        }
    }  
  13. Right click on Content folder -> Add -> New Item -> select Style Sheet -> name it as  Person.css
  14. 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:  

  1. 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:
  2. public static class HtmlExtensions
    {
        /// <summary>
        /// To create an observable HTML Control.
        /// </summary>
        /// <typeparam name="TModel">The model object</typeparam>
        /// <typeparam name="TProperty">The property name</typeparam>
        /// <param name="htmlHelper">The <see cref="HtmlHelper<T>"/></param>
        /// <param name="expression">The property expression</param>
        /// <param name="controlType">The <see cref="ControlTypeConstants"/></param>
        /// <param name="htmlAttributes">The html attributes</param>
        /// <returns>Returns computed HTML string.</returns>
        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;
            // This will be useful, if the same extension has to share with multiple pages (i.e. each with different view models).
            switch (metaData.ContainerType.Name)
            {
                case "PersonViewModel":
                    jsObjectName = "Person.ViewModel."; // Where Person is the Javascript object name (namespace in theory). 
                    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;
            // Various control type creation.
            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); // date is the customized knockout binding handler. Check PrepareKo method of Person.
                    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);
            // Check data-bind already exists, add if not.
            if (!controlBuilder.Attributes.ContainsKey("data-bind"))
            {
                controlBuilder.Attributes.Add("data-bind", "value: " + propertyObject);
            }
            // Merge provided custom html attributes. This overrides the previously defined attributes, if any.
            if (htmlAttributes != null)
            {
                controlBuilder.MergeAttributes(HtmlExtensions.AnonymousObjectToHtmlAttributes(htmlAttributes), true);
            }
            return MvcHtmlString.Create(controlBuilder.ToString());
        }
        /// <summary>
        /// To convert '_' into '-'.
        /// </summary>
        /// <param name="htmlAttributes">The html attributes.</param>
        /// <returns>Returns converted <see cref="RouteValueDictionary"/>.</returns>
        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.

  3. Open Index.cshtml file under Views/Person folder and paste the following code:
  4. @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)); // This is required because, we are holding the collection in ViewBag. If it is wrapped in ViewModel, this line is not required.
        </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. 

  1. 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:  
  2. @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>
  3. Now it is time to write most awaited javascript code. Open Person.js file (available under Scripts/Application folder) and paste the following code: 
  4. 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) {
     
                // Check whether the form is valid. Note: Remove this check, if you are not using HTML5
                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 () {
                            // Display loading image
                        },
                        success: function (result) {
                            // Handle the response here.
                        },
                        complete: function () {
                            // Hide loading image.
                        },
                        error: function (jqXHR, textStatus, errorThrown) {
                            // Handle error.
                        }
                    });
     
                }
     
            });
     
        },
     
    };
     
    $(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 Smile | <img src=  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. 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here