Editable HTML Table with CRUD operations using KnockoutJS and ASP.NET WEB API

Posted by: Mahesh Sabnis , on 5/5/2014, in Category ASP.NET
Views: 141076
Abstract: Create Editable HTML Table in ASP.NET MVC for CRUD operations using KnockoutJS Framework and ASP.NET WEB API

New generation of web applications developed using ASP.NET and ASP.NET MVC can provide an excellent user experience with the help of client-side frameworks and libraries like jQuery, Knockout.js. Recently I came across a requirement where we had to implement inline editing and CRUD operations on Tabular data.

The requirement was to display all records in an HTML table, and allow users to add a new row as well as Edit and Delete records. In essence, the functionality was similar to like what we used to do earlier in ASP.NET GridView, but only this time it had to be done in an ASP.NET MVC application, preferably without un-necessary postbacks.

In this sample today, we’ll look at how we can implement this behaviour in an ASP.NET MVC 5 application.

 

 

The Sample Web API and Knockout Application

Step 1: Open VS 2013 and create a new MVC 5 application using the Empty project template and name it as ‘MVC5_Editable_Table’.

Once the solution is ready, add the jQuery and Knockout libraries using NuGet package manager. You can also install them from the Package Manager Console using the following commands

PM> install-package jquery –version 1.9.1
PM> install-package knockoutjs

Step 2: In the App_Data folder, add a new SQL Server Database, name it as Application.mdf. Open this Database in the Server Explorer by double clicking on it and add a new table called EmployeeInfo in it. The script is as below:

CREATE TABLE [dbo].[EmployeeInfo] (
    [EmpNo]       INT          IDENTITY (1, 1) NOT NULL,
    [EmpName]     VARCHAR (50) NOT NULL,
    [DeptName]    VARCHAR (50) NOT NULL,
    [Designation] VARCHAR (50) NOT NULL,
    [Salary]      DECIMAL (18) NOT NULL,
    PRIMARY KEY CLUSTERED ([EmpNo] ASC)
);

Step 3: Build the project. In this project, add a new ADO.NET EF in the Models folder name it as ‘ApplicationEntities.edmx’.

- In the wizard, select the Application.mdf database as created in Step 1.
- Select the EmployeeInfo table created in Step 2.
- You can keep all other defaults as is.

After completion of the wizard, the following mapping will be displayed:

ef-model

Step 4: Build the project. In the Controllers folder, add a new API Controller based upon the ADO.NET EF entity just added in the Step 3. Name it as ‘EmployeeInfoAPIController’. Here you will get all the methods mapped to HTTP GET, POST, PUT and DELETE.

Step 5: Add a new Empty MVC Controller in the Controllers folder of name EmployeeInfo. Here you will get an Index action method. Add a new (Index) view in the application using the Index action method - Index.cshtml.

Step 6: In the Index.cshtml add the following styles:

<style type="text/css">
table {
  width:700px;
  border:double;
}
th {
  width:100px;
}
td {
  border:double;
  width:100px;
}
input {
  width:100px;
}
< /style>

Refer the Script libraries for jQuery and Knockout as shown below in the view:

<script src="~/Scripts/jquery-2.0.0.min.js"></script>
< script src="~/Scripts/knockout-2.2.1.js"></script>

In the view, add the following script

<script type="text/javascript">
var self = this;     

//S1:Boolean to check wheather the operation is for Edit and New Record
var IsNewRecord = false;
self.Employees = ko.observableArray([]);
loadEmployees();

//S2:Method to Load all Employees by making call to WEB API GET method
function loadEmployees() {
  $.ajax({
   type: "GET",
   url: "api/EmployeeInfoAPI"
  }).done(function (data)
  {
   alert("Success");
   self.Employees(data);
  }).fail(function (err)
  {
   alert(err.status + " <--------------->");
  });
};
alert("Loading Data");

//S3:The Employee Object
function Employee(eno, ename, dname, desig, sal) {
  return {
   EmpNo: ko.observable(eno),
   EmpName: ko.observable(ename),
   DeptName: ko.observable(dname),
   Designation: ko.observable(desig),
   Salary: ko.observable(sal)
  }
};

//S4:The ViewModel where the Templates are initialized
var EmpViewModel = {
  readonlyTemplate:ko.observable("readonlyTemplate"),
  editTemplate:ko.observable()
};

//S5:Method to decide the Current Template (readonlyTemplate or editTemplate)
EmpViewModel.currentTemplate = function (tmpl) {
  return tmpl === this.editTemplate() ? 'editTemplate' :
   this.readonlyTemplate();
}.bind(EmpViewModel);

//S6:Method to create a new Blank entry When the Add New Record button is clicked
EmpViewModel.addnewRecord = function () {
  alert("Add Called");
  self.Employees.push(new Employee(0, "", "", "", 0.0));
  IsNewRecord = true; //Set the Check for the New Record
};

//S7:Method to Save the Record (This is used for Edit and Add New Record)
EmpViewModel.saveEmployee = function (d) {
  var Emp = {};
  Emp.EmpNo = d.EmpNo;
  Emp.EmpName = d.EmpName;
  Emp.DeptName = d.DeptName;
  Emp.Designation = d.Designation;
  Emp.Salary = d.Salary;

  //Edit the Record
  if (IsNewRecord === false) {
$.ajax({
   type: "PUT",
   url: "api/EmployeeInfoAPI/" + Emp.EmpNo,
   data: Emp
  }).done(function (data)
  {
   alert("Record Updated Successfully " + data.status);
   EmpViewModel.reset();
  }).fail(function (err)
  {
   alert("Error Occured, Please Reload the Page and Try Again " + err.status);
   EmpViewModel.reset();
  });
}

//The New Record
if (IsNewRecord === true)
{
  IsNewRecord = false;
  $.ajax({
   type: "POST",
   url: "api/EmployeeInfoAPI",
   data: Emp
  }).done(function (data)
  {
   alert("Record Added Successfully " + data.status);
   EmpViewModel.reset();
   loadEmployees();
  }).fail(function (err)
  {
   alert("Error Occures, Please Reload the Page and Try Again " + err.status);
   EmpViewModel.reset();
  });
});
}
};

//S8:Method to Delete the Record
EmpViewModel.deleteEmployee = function (d) {
$.ajax({
  type: "DELETE",
  url: "api/EmployeeInfoAPI/" + d.EmpNo
}).done(function (data)
{
  alert("Record Deleted Successfully " + data.status);
  EmpViewModel.reset();
  loadEmployees();
}).fail(function (err)
{
  alert("Error Occures, Please Reload the Page and Try Again " + err.status);
  EmpViewModel.reset();
});
};

//S9:Method to Reset the template
EmpViewModel.reset = function (t) {
this.editTemplate("readonlyTemplate");
};
ko.applyBindings(EmpViewModel);
< /script>

The above script performs the following operations:

  • S1 defines the Flag IsNewrecord that checks whether the current action is for New Record or for Editing an existing record
  • S2 describes the loadEmployees method, makes an AJAX call to a WEB API service that reads and fetches all the Employee records. Once these are received on the client side, they are added to the Employees observableArray
  • S3 defines the client side Employee object and its fields. This is used to PUSH the new record to the Employees observableArray
  • S4 defines the EmpViewModel which used to initialize templates. The initial value is set for the readonlyTemplate
  • S5 The currentTemplate method is used to maintain the current template type (i.e. readOnly or Editable)
  • S6: The addnewrecord method is used to push new Employee record in the Employees observableArray. This method also sets the IsNewRecord flag to true
  • S7: The saveEmployee method is used to either save New or Updated employee rows. This makes either an HTTP PUT or POST call to the WEB API based on the IsNewRecord flag value
  • S8: The deleteEmployee method is used to delete a record using and AJAX call to WEB API using HTTP DELETE verb
  • S9: The reset method is used to reset the template to readonlyTemplate. This removes the Edit effect from the UI


Step 7: Now we’ll add the following HTML markup to our Index.cshtml:

The Html input button which is bind with the addnewRecord function

<input type="button" value="Add New Record" data-bind="click: function () { EmpViewModel.addnewRecord(); }"/>

The Html Table is bound with the currentTemplate method and iterates through the Employees observableArray.

< table>
< thead>
  <tr>
   <th>EmpNo</th>
   <th>EmpName</th>
   <th>DeptName</th>
   <th>Designation</th>
   <th>Salary</th>
   <th></th>
   <th>
   </th>
  </tr>
< /thead>
< tbody data-bind="template: { name: currentTemplate, foreach: Employees }"></tbody>
< /table>

Step 8: The HTML template which will be used to show data either in Read-Only form or in Editable form uses the following markup:

<script type="text/html" id="readonlyTemplate">
  @*  <table>*@
< tr>
  <td>
   <span data-bind="text: EmpNo"></span>
  </td>
  <td>
   <span data-bind="text: EmpName"></span>
  </td>
  <td>
   <span data-bind="text: DeptName"></span>
  </td>
  <td>
   <span data-bind="text: Designation"></span>
  </td>
  <td>
   <span data-bind="text: Salary"></span>
  </td>
  <td>
   <input type="button" value="Edit" data-bind="click: function () { EmpViewModel.editTemplate($data);}"/>
  </td>
  <td>
   <input type="button" value="delete" data-bind="click: function () { EmpViewModel.deleteEmployee($data); }"/>
  </td>
< /tr>
@* </table>*@
< /script>


<script type="text/html" id="editTemplate">
@* <table>*@
< tr>
  <td>
   <input type="text" data-bind="value: $data.EmpNo"  id="txteno" disabled="disabled"/>
  </td>
  <td>
   <input type="text" data-bind="value: $data.EmpName"  id="txtename"/>
  </td>
  <td>
   <input type="text" data-bind="value: $data.DeptName"  id="txtdname"/>
  </td>
  <td>
   <input type="text" data-bind="value: $data.Designation"  id="txtdesig"/>
  </td>
  <td>
   <input type="text" data-bind="value: $data.Salary"  id="txtsal"/>
  </td>
  <td>
   <input type="button" value="Save" data-bind="click: EmpViewModel.saveEmployee"/>
  </td>
  <td>
   <input type="button" value="Cancel" data-bind="click: function () { EmpViewModel.reset(); }"/>
  </td>
< /tr>
@*</table>*@
< /script>

The above two templates are bound with the Employee objects’ properties. The readonlyTemplate will be displayed in the HTML table by default. This template contains ‘Save’ and ‘Delete’ button.

When the end-user clicks on the Save button for a specific row, the readonlyTemplate is replaced by editTemplate. Now the end-user can update the employee details and when the Save button is clicked, the record will be posted to the server.

That wraps our implementation, now let’s see how it looks.

The WebAPI, Knockout and MVC Demo

Run the application, you will be prompted with alert boxes which will intimate you about the ‘Loading Data’ and then ‘Success’. Press OK you will find the Table populated as below:

data-on-first-time-load

Click on ‘Add new Record’, you will find the new Row added at the bottom as below:

mvc-new-record

Now click on the ‘Edit’ button for the new added row, it will be editable as shown here:

edit-new-row

Here add values for EmpName, DeptName, Designation and Salary and click on Save the Record will be added and the result will be shown as below:

new-record-added

Note: In the screenshot, you might have observed that the EmpNo is not in a sequence. This is because I was experimenting with Edit and Delete. In your case, you should see a proper sequence.

Similarly when you click on the Edit of the existing record, it will be converted into editable form as below: e.g. click on record with EmpNo 7, the result will be as shown here:

webapi-edit-record

And you can change values of the row and click on ‘Save’, the record will get updated. Similarly you can test Delete as well.

Conclusion

We saw how to achieve client side interactivity for doing CRUD operations, inline, on Tabular Data. This is a common requirement in LoB applications and as we saw it’s rather easy to achieve using jQuery and Knockout JS.

Download the entire source code of this article (Github)

This article has been editorially reviewed by Suprotim Agarwal.

Absolutely Awesome Book on C# and .NET

C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.

We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).

Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.

Click here to Explore the Table of Contents or Download Sample Chapters!

What Others Are Reading!
Was this article worth reading? Share it with fellow developers too. Thanks!
Share on LinkedIn
Share on Google+

Author
Mahesh Sabnis is a DotNetCurry author and a Microsoft MVP having over two decades of experience in IT education and development. He is a Microsoft Certified Trainer (MCT) since 2005 and has conducted various Corporate Training programs for .NET Technologies (all versions), and Front-end technologies like Angular and React. Follow him on twitter @maheshdotnet or connect with him on LinkedIn


Page copy protected against web site content infringement 	by Copyscape




Feedback - Leave us some adulation, criticism and everything in between!
Comment posted by tom on Thursday, May 8, 2014 3:35 AM
Didn't find Employees how this attribute is bound
Comment posted by James on Friday, May 16, 2014 4:13 PM
When adding a new employee, then click the cancel button, the code has remove the newly added employe pushed in when clicking on "Add new employee" button.
Comment posted by aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa on Monday, May 19, 2014 4:33 AM
aaaaaaaaaaaaa
Comment posted by vijay on Thursday, June 12, 2014 4:53 AM
Hi,

please upload controller code also
Comment posted by MORAL on Thursday, June 12, 2014 6:37 AM
HELLO FANTASTIC WANNA ADD GRAND TOTAL OF SALARY DOWN THE LAST ROW ? HOWZ ITS  POSSIBLE ?
Comment posted by SRIRAM on Monday, June 30, 2014 12:43 PM
I have serious issue with the Knockout script as follows.
My function are not executing in sequence manner. my objective is to set the drop-down values in the EDIT mode. so some times its showing pointing it to right data some times its setting 0 vales. here is my code.
function tblUBRAMDetails(data) {
    this.BURAMIdPK = ko.observable(data.BURAMIdPK)
    this.BUIDFK = ko.observable(data.BUIDFK)
    this.Asof = ko.observable(data.Asof)
    this.CRMP = ko.observable(data.CRMP)
    this.Underwriting = ko.observable(data.Underwriting)
    this.RiskMonitoring = ko.observable(data.RiskMonitoring)
    this.RiskIdentification = ko.observable(data.RiskIdentification)
    this.Criticizedassetsmgmt = ko.observable(data.Criticizedassetsmgmt)
    this.LevelofRisk = ko.observable(data.LevelofRisk)
    this.TrendofRisk = ko.observable(data.TrendofRisk)
    this.Processdesing = ko.observable(data.Processdesing)
    this.ProcessEffective = ko.observable(data.ProcessEffective)
    this.QAComments = ko.observable(data.QAComments)
    this.EPMID = ko.observable(data.EPMID)
    this.EPMStatus = ko.observable(data.EPMStatus)
    this.CCEID = ko.observable(data.CCEID)
    this.CCEStatus = ko.observable(data.CCEStatus)
    this.CCEComments = ko.observable(data.CCEComments)
    this.IsProcessStart = ko.observable(data.IsProcessStart)
}
function BURAMVIEW() {
    var tabs = $("#tabs").tabs({
        select: function (e, i) {
            selected_tab = i.index;
        }
    });
    var self = this;
    self.BuRAMs = ko.observableArray([]);
    self.CRMP = ko.observableArray();
    self.UNDER = ko.observableArray();
    self.RM = ko.observableArray();
    self.RID = ko.observableArray();
    self.CRTICIZED = ko.observableArray();
    self.PROCESSDESIGN = ko.observableArray();
    self.PROCESSEFFECT = ko.observableArray();
    self.GETUBRAMDTLS = ko.observableArray([]);


    self.LEVELOFRISK = ko.observableArray();
    self.TRENDOFRISK = ko.observableArray();
    self.EPMSTATUS = ko.observableArray();
    self.CCESTATUS = ko.observableArray();
    self.TRENDOFRISK = ko.observableArray();
    self.LEVELOFRISK = ko.observableArray();
    self.MANGERS = ko.observableArray();

    //For control flow.
    self.LOGINID = ko.observable();
    self.LOGINROLEID = ko.observable();
    self.vv = ko.observable();

    $.ajax({
        type: "POST",
        data: {},
        url: "../BURAMPages/CreateEditUBBURAM.aspx/GetAllBusinessUnit",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function (response) {
            if (response != "") {
                //                    var vResults = $.map(response.d, function (item) { return new tblBu(item) });
                //                    self.BuRAMs(vResults);
                self.BuRAMs(response.d);
            }
        }
    });
    $.ajax({
        type: "POST",
        data: ko.toJSON({ scode: "CH6" }),
        url: "../AQReviewPages/LineCard.aspx/GetAllDivisions",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        success: function (response) {
            if (response != "") {
                self.CRMP(response.d);
                self.UNDER(response.d);
                self.RM(response.d);
                self.RID(response.d);
                self.CRTICIZED(response.d);
                self.PROCESSDESIGN(response.d);
                self.PROCESSEFFECT(response.d);
                //end Disable fields..
            }
        }
    });  self.LoginDetails = function () {
        $.ajax({
            type: "POST",
            data: {},
            url: "../BURAMPages/CreateEditUBBURAM.aspx/GetLoginDetails",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function (response) {
                if (response != "") {
                    self.LOGINID(response.d[0].UserPK);
                    self.LOGINROLEID(response.d[0].LoginUserRoleId);
                    self.GetControlFlow(0);
                }
            }
        });
    }

    //Adding empty values for the first time
    self.LoginDetails();
    self.GETUBRAMDTLS(GetEmptyUBRAM());
    self.EditUBDetails = function () {

        $.ajax({
            type: "POST",
            data: ko.toJSON({ ipBuidPK: vBuidPK }),
            url: "../BURAMPages/CreateEditUBBURAM.aspx/GetBUDetailsByID",
            contentType: "application/json; charset=utf-8",
            dataType: "json",
            success: function (results) {
                if (results.d.length > 0) {
                    var vResults = $.map(results.d, function (item) { return new tblUBRAMDetails(item) });
                    self.GETUBRAMDTLS(vResults);
                    self.GetControlFlow(self.GETUBRAMDTLS()[0].EPMID())
                }
                else { self.GETUBRAMDTLS(GetEmptyUBRAM()); }
            },
            error: function (err) {
                alert(err.responseText + " - " + err.statusText);
            }
        });

    }
    self.GetControlFlow = function (vEMPID) {
        //For Exminers...
        if (self.LOGINROLEID() == 6 && self.LOGINID() != vEMPID) {
            $("#ddlepm").prop("disabled", true);
            $("#ddlepmstatus").prop("disabled", true);
            $("#ddlccestatus").prop("disabled", true);
        }
        //For CCE
        else if (self.LOGINROLEID() == 5) {
            $("#ddlepm").prop("disabled", true);
            $("#ddlepmstatus").prop("disabled", true);
            $("#ddlccestatus").prop("disabled", false);
        }
        //For Admin
        else if (self.LOGINROLEID() == 1) {
            $("#ddlepm").prop("disabled", false);
            $("#ddlepmstatus").prop("disabled", false);
            $("#ddlccestatus").prop("disabled", false);
        }
        else if (vEMPID > 0 && LOGINROLEID == vEMPID) {
            $("#ddlepm").prop("disabled", true);
            $("#ddlepmstatus").prop("disabled", false);
            $("#ddlccestatus").prop("disabled", true);
        }
        else if (self.LOGINROLEID() == 3) {
            $("#ddlepm").prop("disabled", true);
            $("#ddlepmstatus").prop("disabled", false);
            $("#ddlccestatus").prop("disabled", true);
        }
    }


}; //End View..




$(document).ready(function () {
    $("#txtasof").datepicker();
    var vBURAMView = new BURAMVIEW();
    vBuidPK = 1;
    vBURAMView.EditUBDetails();
    ko.applyBindings(vBURAMView);
});

I am really frustrated.Please help me out.
Thanks,
Sriram
Comment posted by Ahmed on Tuesday, July 8, 2014 3:38 AM
Nine Aticle....!!
Comment posted by larry on Thursday, July 17, 2014 4:50 AM
Can I add an Employee and the row for the added employee is already in edit Tamplate?
Comment posted by sathya on Saturday, August 2, 2014 6:37 AM
i am tried grid sorting in asp.net... but that is not working... how can i use the  editable tables instead of grid.. how can i bind that database by using the editable tables...

with regards,
sathya.P
Comment posted by aaa on Friday, November 7, 2014 5:00 AM
ssssxxxx