Wednesday, August 13, 2014

[KnockOut.js]Sample code to for beginners.

Goal

The blog is to create a working form using KnockOut.js, generic handler(ashx) and JQuery. This is strictly for beginners, who want to use KnockOut.js. I am not giving any solution in this blog, this is just a starting thread for the people who are still having trouble using KnockOut.

Steps

  1. Create a visual studio project. I have named the solution as Sample and project name is Sample.KnockOut.
  2. Tools > NuGet Package Manager > Package Manager Console > Run "Install-Package knockoutjs" (CLICK HERE)
  3. Tools > NuGet Package Manager > Package Manager Console > Run "Install-Package jQuery" (CLICK HERE)
  4. Add the files which are shown in the Solution Explorer image below and use the code which is mentioned in the blog.


OrderHandler.ashx.cs

#region System

using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Script.Serialization;
using System.Web.SessionState;
#endregion

namespace Sample.KnockOut.Ajax
{
    /// <summary>
    /// OrderHandler.ashx - Just processes data coming from default.aspx.
    /// </summary>
    public class OrderHandler : IHttpHandler, IRequiresSessionState
    {
        #region Properties

        /// <summary>
        /// Is Reusable
        /// </summary>
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Http Context
        /// </summary>
        private HttpContext Context { get; set; }

        /// <summary>
        /// Sample Entity List
        /// </summary>
        private IList<Entity.Order> OrderList
        {
            get
            {
                if (Context.Session["OrderList"] == null)
                {
                    Context.Session["OrderList"] = new List<Entity.Order>();
                }
                return (List<Entity.Order>)Context.Session["OrderList"];
            }
            set { Context.Session["OrderList"] = value; }
        }

        #endregion

        #region Process Request

        /// <summary>
        /// Process Request
        /// </summary>
        /// <param name="context"></param>
        public void ProcessRequest(HttpContext context)
        {
            // Set the context 
            Context = context;

            // Action which is passed to get the 
            var action = (context.Request["Process"] ?? string.Empty);

            // Check for action and settings
            switch (action)
            {
                case "GetOrder":
                    {
                        GetOrder();
                    }
                    break;
                case "AddUpdateItem":
                    {
                        AddUpdateItem();
                    }
                    break;
                case "DeleteOrder":
                    {
                        DeleteItem();
                    }
                    break;
                case "GetList":
                default:
                    {
                        GetList();
                    }
                    break;
            }
        }

        #endregion

        #region Actions

        /// <summary>
        /// Get List : Gets the list of 
        /// </summary>
        private void GetList()
        {
            // Sample Entity List
            OrderList = OrderList.OrderBy(a => a.OrderId).ToList();

            // Response the settings
            Context.Response.ContentType = "application/json";
            Context.Response.ContentEncoding = Encoding.UTF8;
            Context.Response.Write(new JavaScriptSerializer().Serialize(OrderList));
        }

        /// <summary>
        /// Get Order By Order Id
        /// </summary>
        private void GetOrder()
        {
            // Remove the item
            var orderId = Convert.ToInt32(Context.Request.Params["OrderId"] ?? "0");

            // Sample Entity List
            var order = OrderList.FirstOrDefault(a => a.OrderId == orderId);

            // Response the settings
            Context.Response.ContentType = "application/json";
            Context.Response.ContentEncoding = Encoding.UTF8;
            Context.Response.Write(new JavaScriptSerializer().Serialize(order));
        }

        /// <summary>
        /// Add/Update Item
        /// </summary>
        private void AddUpdateItem()
        {
            // Get the request
            var request = (new JavaScriptSerializer()).Deserialize<Entity.Order>(Context.Request.Params["Request"]);

            // Add Item
            if (request.OrderId == 0) 
            {
                // Get the max id
                var maxId = (OrderList.Any() ? OrderList.Max(a => a.OrderId) : 0);
                request.OrderId = maxId + 1;

                // Add to the session
                OrderList.Add(request);
            }
            else // Update Item
            {
                // remove the item
                OrderList = OrderList.Where(a => a.OrderId != request.OrderId).ToList();

                // Add to the session
                OrderList.Add(request);
            }

            // Returns the list
            GetList();
        }

        /// <summary>
        /// Delete Item
        /// </summary>
        private void DeleteItem()
        {
            // Remove the item
            var orderId = Convert.ToInt32(Context.Request.Params["OrderId"] ?? "0");

            // remove the item
            OrderList = OrderList.Where(a => a.OrderId != orderId).ToList();

            // Returns the list
            GetList();
        }

        #endregion
    }
}


Style.css

* { font-family: verdana; }
div{padding: 2px;}
input{ border: 1px solid #808080;}

.div-main { width: 600px; }
.div-sub-row { width: 600px; }
.div-left-box { width: 150px;text-align: right;float: left;}
.div-right-box { width: 395px;text-align: left;float: left;padding-left: 5px;}
.div-clear { clear: both; }

.div-order-counter{border: 1px solid #efefef;padding:5px;background: red;color: #fff;}

.div-main-grid{ width: 600px;font-size: 11px; }

.div-main-grid-column1{ float: left;text-align: center;width: 50px; }
.div-main-grid-column2{ float: left;text-align: center;width: 150px; }
.div-main-grid-column3{ float: left;text-align: center;width: 250px; }
.div-main-grid-column4{ float: left;text-align: center;width: 50px; }
.div-main-grid-column5{ float: left;text-align: center;width: 50px; }

.div-main-grid-header{ background: #1e90ff;color:#ffffff;height: 25px; }
.div-main-grid-item{ background: #efefef;color:#000;height: 25px; }
.div-main-grid-alternating-item{ background: #fff;color:#000;height: 25px; }


Entity.cs

#region System
using System;
using System.Collections;
using System.Collections.Generic;
#endregion

namespace Sample.KnockOut.Entity
{
    /// <summary>
    /// Order
    /// </summary>
    [Serializable]
    public class Order
    {
        public int OrderId { get; set; }

        public string OrderName { get; set; }

        public IList<OrderItem> OrderItems { get; set; }
    }

    /// <summary>
    /// Order Items
    /// </summary>
    [Serializable]
    public class OrderItem
    {
        public int Quantity { get; set; }

        public string ItemName { get; set; }
    }
}


Sample.js

/*
    Author : Maulik Dhorajia
    Description : Sample to show KnockOut.js usage. This will be helpful for beginners only.
*/

// Default settings for KnockOut.js
if (location.protocol != "data:") {
    $(window).bind('hashchange', function () {
        window.parent.handleChildIframeUrlChange(location.hash);
    });
}

// GLOBAL DECLARATIONS
var viewModel;

// Page Load
$(document).ready(function () {
    // Apply Binding - Which can help in applying binding.
    ApplyBindingHandlers();
    // Apply exterders
    ApplyNumericExtender();
    // Populate View Model
    PopulateViewModel();
});

// Apply Binding Handlers
function ApplyBindingHandlers() {
    ko.bindingHandlers.integerBoxSettings = {
        init: function (element, valueAccessor) {

            // Define the settings
            var $element = $(element);
            
            // Bind change event
            $element.blur(function () {
                if (parseInt($(this).val()) > 0) {
                    $(this).css("border", "1px solid green");
                    $(this).css("color", "green");
                }
                else if (parseInt($(this).val()) < 0) {
                    $(this).css("border", "1px solid red");
                    $(this).css("color", "red");
                }
                else if (parseInt($(this).val()) == 0) {
                    $(this).css("border", "1px solid #808080");
                    $(this).css("color", "black");
                }
            });
            
            $element.focus(function () {
                $(this).css("border", "1px solid #808080");
                $(this).css("color", "black");
            });
        }
    };
    
    // combine Items And Display - This will bind the items in the gird
    ko.bindingHandlers.combineItemsAndDisplay = {
        init: function (element, valueAccessor) {

            // Define the settings
            var $element = $(element);
            var value = ko.utils.unwrapObservable(valueAccessor());

            if (value != null && value.OrderItems.length > 0) {
                var html = "";
                for (var i = 0; i < value.OrderItems.length; i++) {
                    if (!(value.OrderItems[i].Quantity <= 0 || value.OrderItems[i].ItemName == "")) {
                        if (html != "") {
                            html += ", ";
                        }
                        html += value.OrderItems[i].Quantity.toString() + " - " + value.OrderItems[i].ItemName;
                    }
                }
                $element.html(html);
            }
        }
    };
    
    // Edit Item Settings
    ko.bindingHandlers.editItemSettings = {
        init: function (element, valueAccessor) {

            // Define the settings
            var $element = $(element);
            var value = ko.utils.unwrapObservable(valueAccessor());
            
            $element.click(function () {
                CallHandler("GetOrder", value.OrderId.toString());
            });
        }
    };
    
    // Delete Item Settings
    ko.bindingHandlers.deleteItemSettings = {
        init: function (element, valueAccessor) {

            // Define the settings
            var $element = $(element);
            var value = ko.utils.unwrapObservable(valueAccessor());
            
            $element.click(function () {
                if (confirm("Are you sure you want to delete the record?")) {
                    CallHandler("DeleteOrder", value.OrderId.toString());
                }
            });
        }
    };
}

// Applies the extenders
function ApplyNumericExtender() {

    // Validate numbers only
    ko.extenders.numeric = function (target, precision) {
        // create a writeable computed observable to intercept writes to our observable
        var result = ko.computed({
            read: target,  // always return the original observables value
            write: function (newValue) {
                var current = target(),
                    roundingMultiplier = Math.pow(10, precision),
                    newValueAsNum = isNaN(newValue) ? 0 : parseFloat(+newValue),
                    valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;

                // only write if it changed
                if (valueToWrite !== current) {
                    target(valueToWrite);
                } else {
                    // if the rounded value is the same, but a different value was written, force a notification for the current field
                    if (newValue !== current) {
                        target.notifySubscribers(valueToWrite);
                    }
                }
            }
        });

        // initialize with current value to make sure it is rounded appropriately
        result(target());

        // return the new computed observable
        return result;
    };
}

// OrderItem : Object
function OrderItem(quantity, itemName) {
    var self = this;
    self.Quantity = ko.observable(quantity).extend({ numeric: 0 }); // Extended to check if Quantity is string it will replace it with 0.
    self.ItemName = ko.observable(itemName);
}

// Order View Model
function OrderViewModel() {
    var self = this;
    self.OrderId = ko.observable(0);
    self.OrderName = ko.observable("");
    self.OrderItemList = ko.observableArray([new OrderItem(0, ""), new OrderItem(0, ""), new OrderItem(0, "")]);

    // This will be the result from json call
    self.OrderList = ko.observableArray([]);
    
    // Events
    self.SaveClick = function () {
        // Validation
        if (viewModel.OrderName() == "") {
            alert("Order name is required.");
            return;
        }

        var orderItems = new Array();
        for (var i = 0; i < viewModel.OrderItemList().length; i++) {
            orderItems.push({ Quantity: viewModel.OrderItemList()[i].Quantity(), ItemName: viewModel.OrderItemList()[i].ItemName() });
        }
        var request = { "OrderId": viewModel.OrderId(), "OrderName": viewModel.OrderName(), "OrderItems": orderItems };
        var postData = { Request: JSON.stringify(request) };
        CallHandler("AddUpdateItem", postData);
    };
    
    self.ClearClick = function () {
        ClearScreen();
    };
}

// Populates view model
function PopulateViewModel() {
    CallHandler("GetList", null);
}

// Call Handler
function CallHandler(callMethod, postData) {
    //Supports cors
    $.ajaxSetup({ cache: false });
    $.support.cors = true;
    if (callMethod == "GetOrder" || callMethod == "DeleteOrder") {
        postData = { OrderId: postData };
    }
    // Cart Proxy
    $.post("Ajax/OrderHandler.ashx?Process=" + callMethod.toString(), postData, function (response) {
        SuccessOnServiceCall(response, callMethod);
    }).fail(function (jqXhr, textStatus, errorThrown) {
        ErrorOnServiceCall(jqXhr, textStatus, errorThrown);
    });
}

// Bind the screen
function SuccessOnServiceCall(response, callMethod) {
    switch (callMethod) {
    case "GetList":
        {
            // DO nothing
        } break;
        case "GetOrder":
            {
                if (response != null) {
                    viewModel.OrderId(response.OrderId);
                    viewModel.OrderName(response.OrderName);
                    
                    for (var i = 0; i < response.OrderItems.length; i++) {
                        viewModel.OrderItemList()[i].Quantity(response.OrderItems[i].Quantity);
                        viewModel.OrderItemList()[i].ItemName(response.OrderItems[i].ItemName);
                    }
                }
            } break;
    case "AddUpdateItem":
        {
            ClearScreen();
            alert("Order saved successfully.");
        } break;
    case "DeleteItem":
        {
            ClearScreen();
            alert("Order deleted successfully.");
        } break;
    }
    
    // Apply the model if null
    if (viewModel == null) {
        // Apply the View Model
        viewModel = new OrderViewModel();
        RefreshOrderList(response);
        // This should be called only once.
        ko.applyBindings(viewModel, document.getElementById("knockout-main-form")); // document.getElementById => Should be the like this. Whatever controls are there in this element can only be use the script.   
    } else {
        if (callMethod != "GetOrder") {
            RefreshOrderList(response);
        }
    }
}

// Display on error message
function ErrorOnServiceCall(jqXhr, textStatus, errorThrown) {
    alert(JSON.stringify(jqXhr));
}

// Refresh Order List : VERY IMPORTANT DONT BIND THE WHOLE OBJECT IT WONT WORK :)
function RefreshOrderList(orders) {
    viewModel.OrderList([]);
    if (orders != null && orders.length > 0) {
        for (var i = 0; i < orders.length; i++) {
            viewModel.OrderList.push(ko.observable(orders[i]));
        }
    }
}

// Clear screen
function ClearScreen() {
    viewModel.OrderId(0);
    viewModel.OrderName("");
    for (var i = 0; i < viewModel.OrderItemList().length; i++) {
        viewModel.OrderItemList()[i].Quantity(0);
        viewModel.OrderItemList()[i].ItemName("");
    }
}


Default.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="Sample.KnockOut.Default" %>

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <!-- Title -->
    <title>KnockOut.js - Sample</title>
    <!-- Script -->
    <script type="text/javascript" src="Scripts/jquery-2.1.1.js"></script>
    <script type="text/javascript" src="Scripts/knockout-3.1.0.js"></script>
    <script type="text/javascript" src="Scripts/Sample.js"></script>
    <!-- Css -->
    <link rel="stylesheet" href="Css/Style.css" />
</head>
<body>
    <form id="form1" runat="server">
        <div id="knockout-main-form">
            <h3>KnockOut.js - Sample for observable object by KnockOut.js.</h3>
            <div id="divMain" class="div-main">
                <div class="div-sub-row">
                    <div class="div-left-box"><b>Order name :</b></div>
                    <div class="div-right-box">
                        <input type="text" data-bind="value: OrderName" maxlength="15" /><span data-bind="text: OrderId, visible: false" /></div>
                </div>
                <div class="div-clear"></div>
                <div class="div-sub-row">
                    <table>
                        <thead>
                            <tr>
                                <th>Quantity</th>
                                <th>Item Name</th>
                            </tr>
                        </thead>
                        <tbody data-bind="foreach: OrderItemList">
                            <tr>
                                <td>
                                    <input type="text" data-bind="value: Quantity, integerBoxSettings: $data" maxlength="3" style="width: 100px" /></td>
                                <td>
                                    <input type="text" data-bind="value: ItemName" maxlength="15" style="width: 250px" /></td>
                            </tr>
                        </tbody>
                    </table>
                </div>
                <div class="div-clear"></div>
                <div>
                    <input type="button" value="Save" data-bind="click: SaveClick" />&nbsp;<input type="button" value="Clear" data-bind="click: ClearClick" />
                </div>
                <div class="div-order-counter">
                    <span data-bind="text: 'There are '+ OrderList().length +' order(s) in memory.'"></span>
                </div>

                <div data-bind="visible: OrderList().length > 0" class="div-main-grid">
                    <div class="div-main-grid-header">
                        <div class="div-main-grid-column1">Id</div>
                        <div class="div-main-grid-column2">Name</div>
                        <div class="div-main-grid-column3">Items</div>
                        <div class="div-main-grid-column4">Edit</div>
                        <div class="div-main-grid-column5">Delete</div>
                    </div>
                </div>
                <div data-bind="visible: OrderList().length > 0, foreach: OrderList" class="div-main-grid">
                    <div data-bind="css: (($index() + 1) % 2 == 0 ? 'div-main-grid-alternating-item' : 'div-main-grid-item' )">
                        <div class="div-main-grid-column1" data-bind="text: OrderId"></div>
                        <div class="div-main-grid-column2" data-bind="text: OrderName"></div>
                        <div class="div-main-grid-column3" data-bind="combineItemsAndDisplay: $data"></div>
                        <div class="div-main-grid-column4"><a href="javascript:void(0);" data-bind="text: 'Edit', editItemSettings: $data"></a></div>
                        <div class="div-main-grid-column5"><a href="javascript:void(0);" data-bind="text: 'Delete', deleteItemSettings: $data"></a></div>
                    </div>
                </div>
            </div>
        </div>
    </form>
</body>
</html>


Final Out Come



Please let me know if this helped or not.