In this tutorial, we will create an expense tracking application. For each expense, we will track the expense date, category, description and amount. We will display the list of expenses in a grid and next to it we will show a pie chart to breakdown the expenses by category. We will allow the user to add, edit, delete and view the expense records individually. To do this, the user can simply select an expense record from the grid by clicking on it, and a form entry will appear on top of the grid that will allow the user to do CRUD operations (create, read, update, delete.) Below is some screenshots of the final product.
Figure 1. DataTable displaying expenses along with a pie chart next to it.
Figure 2. When user clicks on an expense entry in the Grid, The dataform with that entry will appear on top of the grid.
Figure 3. User is able to edit the selected record by clicking on the edit button. Similarly, there are buttons for add and delete records. You may notice that the edit and delete buttons are only enabled when the user selects an entry from the grid. When the user saves the changes, it will be reflected immediately on the grid and chart.
Setup the project.
The project will be composed of two components:
- A server side component that will expose rest methods to handle saving and fetching the data. We will use ASP MVC to build this part. As for saving the data, we will simply use an in memory collection.
- A client side component that will contain both the view and view model logic. This will be done in HTML and Javascript. We will use several javascript frameworks along the way. Mainly, knockout js to provide the binding between our view and viewmodel. DataTables to provide grid functionality with paging and sorting. And finally jqPlot for the pie chart.
Step 1: Import resources:
Open Visual Studio and create a new ASP.NET MVC web application. When asked to select a template, choose the Empty template. I will call my solution DemoProject. Once the project is created, use nugget to install or update the following javascript frameworks:
Jquery, Jquery UI, json2, knockout js, knockout mapping, DataTables, jqPlot.
In addition, download the resources folder here (resources) and import the contents of it to the Scripts and Content folders. This includes the CSS files, images and other javascript frameworks that are needed for our project.
At this point your project structure should look like this:
Figure 4. after importing the resources for the project.
Step 2: server side:
The first thing we need to do is add a model. Right click the Models folder in your project and click Add new item. Choose a class from the list and call it Expense.cs. Modify its contents with this:
|
1 2 3 4 5 6 7 8 |
public class Expense { public int ID { get; set; } public double ExpenseAmount { get; set; } public string ExpenseDescription { get; set; } public DateTime ExpenseDate { get; set; } public int CategoryId { get; set; } } |
The next step is to create the controller. Right click on the Controllers folder and click on Add controller. Type HomeController as the name and click Add. Since we will store our data in memory, let’s create a static collection of Expense type. We will prepopulate the collection with some expenses for a starting point. Put the following inside the HomeController class:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private static List expenses = new List{ new Expense { ID = 1, ExpenseAmount = 3.99, ExpenseDate = new DateTime(2012, 10, 15), ExpenseDescription = "Apples", CategoryId = 1 }, new Expense { ID = 2, ExpenseAmount = 34.95, ExpenseDate = new DateTime(2012, 10, 16), ExpenseDescription = "Internet bill", CategoryId = 2 }, new Expense { ID = 3, ExpenseAmount = 60, ExpenseDate = new DateTime(2012, 10, 18), ExpenseDescription = "Jeans", CategoryId = 3 }, new Expense { ID = 4, ExpenseAmount = 375, ExpenseDate = new DateTime(2012, 10, 19), ExpenseDescription = "Ipad", CategoryId = 4 }, new Expense { ID = 5, ExpenseAmount = 26, ExpenseDate = new DateTime(2012, 10, 22), ExpenseDescription = "new keyboard", CategoryId = 4 }, new Expense { ID = 6, ExpenseAmount = 75, ExpenseDate = new DateTime(2012, 10, 25), ExpenseDescription = "webcam", CategoryId = 4 }, new Expense { ID = 7, ExpenseAmount = 15, ExpenseDate = new DateTime(2012, 10, 29), ExpenseDescription = "cellphone minutes", CategoryId = 2 }, new Expense { ID = 8, ExpenseAmount = 31.54, ExpenseDate = new DateTime(2012, 10, 8), ExpenseDescription = "T-shirt", CategoryId = 3 }, new Expense { ID = 9, ExpenseAmount = 7.85, ExpenseDate = new DateTime(2012, 10, 3), ExpenseDescription = "Pens", CategoryId = 6 }, new Expense { ID = 10, ExpenseAmount = 1.50, ExpenseDate = new DateTime(2012, 10, 16), ExpenseDescription = "Monster energy", CategoryId = 5 }, new Expense { ID = 11, ExpenseAmount = 5.00, ExpenseDate = new DateTime(2012, 10, 30), ExpenseDescription = "Subway Sandwich", CategoryId = 1 } }; |
If the compiler complains that the Expense type is not recognized, make sure to add the namespace of your model inside your Controller class:
|
1 |
using DemoProject.Models; |
We need another collection for the categories. Also we need to keep track of the ID of the last element in the expenses list (normally a database would handle this but since we are using a static list to store the data, we will need this). Copy the following code inside he homeController class:
|
1 2 3 4 5 6 7 8 9 10 11 |
private static Dictionary categories = new Dictionary { {1,"Food"}, {2,"Bills"}, {3,"Clothing"}, {4,"Gadget"}, {5,"Misc"}, {6,"Misc2"} }; private static int lastId = 11; |
Next, we will create the CRUD methods in the HomeController. First the method that will return the collection of expenses:
|
1 2 3 4 |
public ActionResult GetExpenses() { return Json(expenses, JsonRequestBehavior.AllowGet); } |
Notice that we return the data in Json format. This is important since we will call this method later from javascript.
The method for adding an expense is simple. It simply checks if all fields are populated and if so it will assign the next id to it and add it to the collection. It will return back the new id to the caller.
|
1 2 3 4 5 6 7 8 9 10 11 |
[HttpPost] public ActionResult AddExpense(Expense newExpense) { int newID = -1; if (newExpense.ExpenseAmount != 0 && newExpense.ExpenseDate != new DateTime() && newExpense.ExpenseDescription != null && newExpense.ExpenseDescription.Trim() != "") { newExpense.ID = newID = ++lastId; expenses.Add(newExpense); } return Json(newID); } |
Notice that this method can only be called using an HTTP Post request.
Similarly copy the following two methods for Add and Edit inside the HomeController:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
[HttpPost] public ActionResult EditExpense(Expense expense) { string result; Expense existing = expenses.Find(item => item.ID == expense.ID); if (existing != null) { if (expense.ExpenseAmount == 0 || expense.ExpenseDate == new DateTime() || expense.ExpenseDescription == null || expense.ExpenseDescription.Trim() == "") { result = "Record not valid. Try again."; } else { existing.ExpenseDate = expense.ExpenseDate; existing.ExpenseDescription = expense.ExpenseDescription; existing.ExpenseAmount = expense.ExpenseAmount; existing.CategoryId = expense.CategoryId; result = "success"; } } else { result = "record not found"; } return Json(result); } [HttpPost] public ActionResult DeleteExpense(Expense expense) { string result; Expense existing = expenses.Find(item => item.ID == expense.ID); if (existing != null) { expenses.Remove(existing); result = "success"; } else { result = "record not found"; } return Json(result); } |
Finally, we need one last method to provide the data for our chart. This is the same data as for our grid except it will be grouped by category and the expense amounts are summed for each category:
|
1 2 3 4 5 |
public ActionResult GetChartData() { var groupedExpenses = from e in expenses group e by e.CategoryId into g select new { category = categories[g.Key], amount = g.Sum(x => x.ExpenseAmount) }; return Json(groupedExpenses.ToList(), JsonRequestBehavior.AllowGet); } |
So now we are done with the server side component. The rest is all done on the client side.
Step 3: client side View:
Open Views > Shared > _Layout.cshtml file and replace its content with this:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<html> <head> <meta charset="utf-8" /> <title>@ViewBag.Title</title> <link href="@Url.Content("~/Content/themes/base/jquery-ui.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/DataTables-1.9.2/media/css/demo_page.css")" rel="stylesheet" type="text/css" /> <link href="@Url.Content("~/Content/DataTables-1.9.2/media/css/demo_table.css")" rel="stylesheet" type="text/css" /> <link rel="stylesheet" href="@Url.Content("~/Content/css/reset.css")" type="text/css"/> <link rel="stylesheet" href="@Url.Content("~/Content/css/grid.css")" type="text/css"/> <link rel="stylesheet" href="@Url.Content("~/Content/css/main.css")" type="text/css"/> <link rel="stylesheet" href="@Url.Content("~/Scripts/jqPlot/jquery.jqplot.css")" type="text/css" /> <script src="@Url.Content("~/Scripts/jquery-1.7.2.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/json2.min.js")" type="text/javascript"></script> <!--[if lt IE 9]><script language="javascript" type="text/javascript" src="@Url.Content("~/Scripts/jqPlot/excanvas.min.js")"></script><![endif]--> <script src="@Url.Content("~/Scripts/jqPlot/jquery.jqplot.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jqPlot/plugins/jqplot.pieRenderer.min.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/DataTables-1.9.2/media/js/jquery.dataTables.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout-2.1.0.debug.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout.mapping-latest.debug.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/cog.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/cog.utils.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout.bindings.dataTables.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/knockout.bindings.jqPlot.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/date.format.js")" type="text/javascript"></script> <script src="@Url.Content("~/Scripts/jquery-ui-1.8.20.min.js")" type="text/javascript"></script> @RenderSection("head", required: false) </head> <body> @RenderBody() </body> </html> |
This will reference all the necessary css and javascript files that we need to use. Next we need to create our main view. Right click the Views folder and click on Add New Folder. Call it Home. Right click the newly created Home folder and click on the Add View. Call the new view Index. Replace its content with the following HTML layout:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
@{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section head { <script type="text/javascript"> </script> } <div id="demo"> <div class="content-box"> <div class="box-header clear"> <h2>Expenses</h2> </div> <div class="box-body clear"> <div id="chart"></div> <div id="gridDiv"> <div id="divDataformHeader"> <div id="divDataformTitle">Add / Modify / Delete</div> <div id="divDataformButtons"><input id="addRecord" title="Add Record" type="button" /> <input id="editRecord" title="Edit Record" type="button" /> <input id="deleteRecord" title="Delete Record" type="button" /></div> </div> <div id="divDataformRead" class="dataformItem"> <table> <tbody> <tr class="title"> <td>Date</td> <td>Category</td> <td>Description</td> <td>Amount</td> </tr> <tr> <td id="readDateCell"></td> <td id="readCatagoryCell"></td> <td id="readDescCell"><span id="readDesc"> </span></td> <td id="readAmountCell"></td> </tr> </tbody> </table> </div> <div id="divDataformNew" class="dataformItem"> <table> <tbody> <tr class="title"> <td>Date</td> <td>Category</td> <td>Description</td> <td>Amount</td> </tr> <tr> <td><input id="newDate" class="entryField" type="text" maxlength="10" /></td> <td></td> <td><input id="newDesc" class="entryField" type="text" maxlength="30" /></td> <td><input id="newAmount" class="entryField" type="text" maxlength="10" /></td> </tr> <tr> <td class="confirmBtnContainer" colspan="4"> <div id="adderror"></div> <input id="cancelNewBtn" title="cancel" type="button" /> <input id="saveNewBtn" title="Add" type="button" /></td> </tr> </tbody> </table> </div> <div id="divDataformEdit" class="dataformItem"> <table> <tbody> <tr class="title"> <td>Date</td> <td>Category</td> <td>Description</td> <td>Amount</td> </tr> <tr> <td><input id="editDate" class="entryField" type="text" maxlength="10" /></td> <td></td> <td><input id="editDesc" class="entryField" type="text" maxlength="30" /></td> <td><input id="editAmount" class="entryField" type="text" maxlength="10" /></td> </tr> <tr> <td class="confirmBtnContainer" colspan="4"> <div id="editerror"></div> <input id="cancelEditBtn" title="cancel" type="button" /> <input id="saveEditBtn" title="Save" type="button" /></td> </tr> </tbody> </table> </div> <div id="loading" style="display: none;"> <img src="../../Content/images/ajax-loader.gif" alt="" /> Working ...</div> <div class="dataTables_wrapper"></div> <input id="refreshBtn" title="refresh data from server" type="button" /></div> </div> </div> </div> |
Step 4: Client Side ViewModel:
The next step is to create our view model that will be the glue that connects our model on the server side to our html view. We will start by creating a client side model that will mirror our model on the server side. Add the following code inside the javascript tag in index.cshtml:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var CategoriesCollection = [{ Text: "Food", Value: 1 }, { Text: "Bills", Value: 2 }, { Text: "Clothing", Value: 3 }, { Text: "Gadget", Value: 4 }, { Text: "Misc", Value: 5 }, { Text: "Misc2", Value: 6}]; function ExpenseEntry(Id, expenseDate, expenseDescription, expenseAmount, categoryId) { this.ID = Id; this.ExpenseDate = ko.observable(expenseDate); this.ExpenseDescription = ko.observable(expenseDescription); this.ExpenseAmount = ko.observable(expenseAmount); this.CategoryId = ko.observable(categoryId); this.Category = ko.computed(function () { if (this.CategoryId() != 0 && this.CategoryId() - 1 < CategoriesCollection.length) { return CategoriesCollection[this.CategoryId() - 1].Text; } else { return "none"; } }, this); } |
Notice the use of ko.observable to wrap our variables. This is the mechanism used by knockout to allow binding and change notification.
Next, we will create our view model. Copy the code below inside the javascript tag of index.chstml. An explanation of the code will follow:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
function ViewModel() { var vm = this; this.categories = CategoriesCollection; this.data = ko.observableArray(); this.chartData = ko.observableArray(); this.jsonData = ko.dependentObservable(function () { return ko.toJSON(this.data); }, this); this.canEdit = ko.observable(false); this.selectedEntry = new ExpenseEntry(0, "", "", "", 0); this.originalEntry = new ExpenseEntry(0, "", "", "", 0); this.newEntry = new ExpenseEntry(0, "", "", "", 0); this.mode = ko.observable("hidden"); this.errorMsg = ko.observable(""); this.beginEdit = function () { vm.copyEntry(vm.selectedEntry, vm.originalEntry); vm.clearErrors(); vm.mode("edit"); } this.endEdit = function () { $.ajax({ data: ko.toJSON(vm.selectedEntry), url: '/home/EditExpense', type: 'POST', contentType: 'application/json; charset=utf-8', success: function (resultMsg) { if (resultMsg == "success") { vm.data.remove(function (item) { return vm.selectedEntry.ID == item.ID }); var editedItem = new ExpenseEntry(0, "", "", "", 0); vm.copyEntry(vm.selectedEntry, editedItem); vm.data.push(editedItem); vm.originalEntry = new ExpenseEntry(0, "", "", "", 0); vm.clearErrors(); vm.mode("read"); vm.refreshChartData(); } else { vm.errorMsg(resultMsg); } }, error: function (e) { vm.errorMsg("Error: " + e.statusText); } }); } this.cancelEdit = function () { vm.copyEntry(vm.originalEntry, vm.selectedEntry); vm.clearErrors(); vm.mode("read"); } this.deleteEntry = function () { $.ajax({ data: ko.toJSON(vm.selectedEntry), url: '/home/DeleteExpense', type: 'POST', contentType: 'application/json; charset=utf-8', success: function (resultMsg) { if (resultMsg == "success") { vm.data.remove(function (item) { return vm.selectedEntry.ID == item.ID }); vm.resetEntry(vm.selectedEntry); vm.canEdit(false); vm.clearErrors(); vm.mode("hidden"); vm.refreshChartData(); } else { vm.errorMsg(resultMsg); } }, error: function (e) { vm.errorMsg("Error: " + e.statusText); } }); } this.beginNewEntry = function () { vm.resetEntry(vm.newEntry); vm.clearErrors(); vm.mode("new"); } this.endNewEntry = function () { $.ajax({ data: ko.toJSON(vm.newEntry), url: '/home/AddExpense', type: 'POST', contentType: 'application/json; charset=utf-8', success: function (insertID) { if (insertID == -1) { vm.errorMsg("error inserting new record"); } else { vm.newEntry.ID = insertID; var newItem = new ExpenseEntry(0, "", "", "", 0); vm.copyEntry(vm.newEntry, vm.selectedEntry); vm.copyEntry(vm.newEntry, newItem); vm.resetEntry(vm.newEntry); vm.data.push(newItem); vm.clearErrors(); vm.mode("read"); vm.refreshChartData(); } }, error: function (e) { vm.errorMsg("Error: " + e.statusText); } }); } this.cancelNewEntry = function () { if (vm.selectedEntry.ID == 0) { vm.mode("hidden"); vm.clearErrors(); } else { vm.mode("read"); vm.clearErrors(); } } this.refreshChartData = function () { $.ajax({ cache: false, url: '/home/GetChartData', type: 'GET', contentType: 'application/json; charset=utf-8', success: function (returnData) { vm.chartData.remove(function (item) { return true; }); for (var i = 0; i < returnData.length; i++) { var nitem = [returnData[i].category, returnData[i].amount]; vm.chartData.push(nitem); } }, error: function (e) { vm.errorMsg("Error: " + e.statusText); } }); } this.refreshData = function () { $.ajax({ cache: false, url: '/home/GetExpenses', type: 'GET', contentType: 'application/json; charset=utf-8', success: function (returnData) { vm.data.remove(function (item) { return true; }); for (var i = 0; i < returnData.length; i++) { var nitem = new ExpenseEntry(returnData[i].ID, new Date(parseInt(returnData[i].ExpenseDate.substr(6))).format("m/dd/yyyy"), returnData[i].ExpenseDescription, returnData[i].ExpenseAmount, returnData[i].CategoryId); vm.data.push(nitem); } vm.resetEntry(vm.selectedEntry); vm.canEdit(false); vm.clearErrors(); vm.mode("hidden"); }, error: function (e) { vm.errorMsg("Error: " + e.statusText); } }); } //helper functions this.setSelectedEntry = function (id, date, desc, amount, categoryId) { vm.selectedEntry.ID = id; vm.selectedEntry.ExpenseDate(date); vm.selectedEntry.ExpenseDescription(desc); vm.selectedEntry.ExpenseAmount(amount); vm.selectedEntry.CategoryId(categoryId); vm.copyEntry(vm.selectedEntry, vm.originalEntry); vm.clearErrors(); vm.mode("read"); } this.copyEntry = function (source, target) { target.ID = source.ID; target.ExpenseDate(source.ExpenseDate()); target.ExpenseDescription(source.ExpenseDescription()); target.ExpenseAmount(source.ExpenseAmount()); target.CategoryId(source.CategoryId()); } this.clearErrors = function () { vm.errorMsg(""); } this.resetEntry = function (entry) { var newItem = new ExpenseEntry(0, "", "", "", 0); vm.copyEntry(newItem, entry); } }; //end viewmodel |
Our view model has the following properties and method:
Properties:
Data : this is the collection of expense entries.
ChartData: expense entries grouped by categories and summed on expense amount.
Categories: json object of available categories and their ids.
selectedEntry: selected expense item from the grid.
originalEntry: original entry before user starts editing ( used to restore value upon cancel).
newEntry: the new record being added by the user.
Mode: whether we are in on of four modes: read, new, edit or hide.
errorMsg: holds the error messages when modifying or adding records.
canEdit: whether the edit and delete buttons are enabled or disabled.
Methods:
beginEdit: save original entry and change mode to edit.
endEdit: Try to save edited entry. If succeeded, update data with changes and refresh chart. Otherwise set error message.
cancelEdit: restore original entry and change mode to read.
deleteEntry: remove selected entry from data collection and refresh chart data.
beginNewEntry: set the mode to new.
endNewEntry: try to save new entry. If succeeded, add to data collection and change mode to read and refresh chart data. Else set error message.
CancelNewEntry: Change mode to either hidden or read depending if any entry is selected from the grid.
refreshData: refreshes data collection from server.
refreshChartData: refreshes chartdata collection from server.
helper functions:
setSelectedEntry: update the values of the selected Entry.
copyEntry: copy values from t one expense object to another.
clearErrors: clears the value of the erroMsg property.
resetEntry: clears the values of the passed expense object.
Step 5: wire up the bindings:
We are done with the view model. Now we need to bind our view model to the html view. The way to do this using knockout js is to use data-bind attributes as follows:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
$(document).ready(function () { $("#newDate").datepicker(); $("#editDate").datepicker(); $('#chart').attr('data-bind', 'piechart : chartData()'); $('#example').attr('data-bind', 'dataTable: { dataSource: data, columns: [{ 'bVisible' : false , mDataProp : 'ID', sTitle : 'Id'} ,{ mDataProp : 'ExpenseDate', sTitle : 'Date', sWidth: '100px'},{ mDataProp : 'CategoryId', sTitle : 'CategoryId', 'bVisible' : false},{ mDataProp : 'Category', sTitle : 'Category', sWidth: '100px'},{ mDataProp : 'ExpenseDescription', sTitle : 'Description', sWidth: '300px'},{ mDataProp : 'ExpenseAmount', sTitle : 'Amount', sWidth: '100px'}], sPaginationType : 'full_numbers', sDom : 'rt' }'); $('#adderror').attr('data-bind', 'text : errorMsg'); $('#editerror').attr('data-bind', 'text : errorMsg'); $('#readDate').attr('data-bind', 'text : selectedEntry.ExpenseDate'); $('#readDesc').attr('data-bind', 'text : selectedEntry.ExpenseDescription'); $('#readAmount').attr('data-bind', 'text : selectedEntry.ExpenseAmount'); $('#readCategory').attr('data-bind', 'text : selectedEntry.Category'); $('#editDate').attr('data-bind', 'value : selectedEntry.ExpenseDate'); $('#editDesc').attr('data-bind', 'value : selectedEntry.ExpenseDescription'); $('#editAmount').attr('data-bind', 'value : selectedEntry.ExpenseAmount'); $('#editCategory').attr('data-bind', 'options: categories, optionsCaption: 'Select...', optionsText: 'Text', optionsValue:'Value', value: selectedEntry.CategoryId'); $('#newDate').attr('data-bind', 'value : newEntry.ExpenseDate'); $('#newDesc').attr('data-bind', 'value : newEntry.ExpenseDescription'); $('#newAmount').attr('data-bind', 'value : newEntry.ExpenseAmount'); $('#newCategory').attr('data-bind', 'options: categories, optionsCaption: 'Select...', optionsText: 'Text', optionsValue:'Value', value: newEntry.CategoryId'); $('#divDataformRead').attr('data-bind', 'visible : mode() == 'read''); $('#divDataformNew').attr('data-bind', 'visible : mode() == 'new''); $('#divDataformEdit').attr('data-bind', 'visible : mode() == 'edit''); $('#addRecord').attr('data-bind', 'click : beginNewEntry'); $('#editRecord').attr('data-bind', 'click : beginEdit, enable : canEdit() == true'); $('#deleteRecord').attr('data-bind', 'click : deleteEntry, enable : canEdit() == true'); $('#cancelEditBtn').attr('data-bind', 'click : cancelEdit'); $('#saveEditBtn').attr('data-bind', 'click : endEdit'); $('#cancelNewBtn').attr('data-bind', 'click : cancelNewEntry'); $('#saveNewBtn').attr('data-bind', 'click : endNewEntry'); $('#refreshBtn').attr('data-bind', 'click : refreshData'); var vm1 = new ViewModel(); vm1.refreshData(); vm1.refreshChartData(); ko.applyBindings(vm1, $("#demo")[0]); //we need to get the otable object first var oTable = $.data(document, 'oTable'); $("#example tbody").click(function (event) { $(oTable.fnSettings().aoData).each(function () { $(this.nTr).removeClass('row_selected'); }); if (event.target.parentNode.localName && event.target.parentNode.localName == "tr") { $(event.target.parentNode).addClass('row_selected'); } var id = $(event.target.parentNode)[0].cells[0].innerText || $(event.target.parentNode)[0].cells[0].textContent; var date = $(event.target.parentNode)[0].cells[1].innerText || $(event.target.parentNode)[0].cells[1].textContent; var categoryId = $(event.target.parentNode)[0].cells[2].innerText || $(event.target.parentNode)[0].cells[2].textContent; var desc = $(event.target.parentNode)[0].cells[4].innerText || $(event.target.parentNode)[0].cells[4].textContent; var amount = $(event.target.parentNode)[0].cells[5].innerText || $(event.target.parentNode)[0].cells[5].textContent; vm1.setSelectedEntry(id, date, desc, amount, categoryId); vm1.canEdit(true); }); |
Knockout js by default provides binding handlers for most of html form elements. However for the DataTables and jqPlot there is no default binding. We already added custom binding for those when we added a reference to the knockout.bindings.dataTables.js and knockout.bindings.jqPlot.js which you downloaded from the resources folder.
The last function that we added below the bindings was adding a click event handler for the grid table. This is so we can get the expense item the user selects and then update the view model’s selected Entry. In theory, we should write a binding handler for the grid selection. For now, I’ll leave that as such since it works fine.
The last thing we need to do is to add a progress indicator whenever we are doing an ajax call. Add the following inside the javascript tags in index.cshtml:
|
1 2 3 4 5 6 7 8 9 10 |
$.ajaxSetup({ beforeSend: function () { // show gif here $("#loading").show(); }, complete: function () { // hide gif here $("#loading").hide(); } }); |
We are done. Run the application to see the result. Download the complete solution: here









Thanks for your posting. I did find one thing missing in the documentation that caused me a little time to determine. The DOM definition for your ExpenseTable is missing. Again, this is from the inline notes above. It might be correct in the document download.
For example, you only show this.
instead of
Sorry…that did not come through due to the HTML. Here is what I was saying was missing from the inline notes.
table cellpadding=”0″ cellspacing=”0″ border=”0″ class=”ExpenseTable” id=”example”
You have a great series on DataTable and Knockout, thank you.
One thing I am not able to find is how to handle filtering, paging and searching on server side.
I will have to treat a lot of data and I didn’t want to have everything on the client side.
Have you ever have to implement such a solution ?
Thank you !
hello
nice work.
is it posible to use localStorage to save grid data without any server side
i’m building something similar to this and i would appreciate a litle help with an advice about client side scripting
thank you
Hello Myer
Nice work, can you add more features like:
- Show the section for Add/Edit in popup box.
- Search section like in datatable grid if we have huge data.
- Grid support column filter.
Thank you.
Pingback: Exemple de pie chart avec KnockoutJS « Pragonas
How can we read data from database table for categories? right now it is hard coded. Can you post how to do that? Your code is awesome and it is great learning experience…thanks a bunch for that.
Hi nice job, I have seen that you have built a piechart binding. Recently I have done a similar one for D3JS. I have make it accept a “transformation” function which transforms any collcetion into a collection of a good shape for the chart. This can cleanup a bit your viewmodel. Check it out if you want: https://github.com/hoonzis/KoExtensions