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.