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
- Create a visual studio project. I have named the solution as Sample and project name is Sample.KnockOut.
- Tools > NuGet Package Manager > Package Manager Console > Run "Install-Package knockoutjs" (CLICK HERE)
- Tools > NuGet Package Manager > Package Manager Console > Run "Install-Package jQuery" (CLICK HERE)
- 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" /> <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.