In this guide, you will learn how to create a Single-page application featuring jQWidgets. A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a more fluid user experience akin to a desktop application. In an SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page. Interaction with the single page application often involves dynamic communication with the web server behind the scenes.
The SPA featured in this guide makes use of the following technologies: ASP.NET MVC 4, Web API, Knockout.js and MongoDB.
We are using MongoDB database in our single-page application. MongoDB is a NoSQL database, storing data not in tables, but in JSON-style documents. Follow these steps to set the database correctly:
C:\mongodb
(or another location);C:\mongodb\bin
create a folder named data
and in it
- one called db
. There should now be the following path: C:\mongodb\bin\data\db
;C:\mongodb\bin\mongod.exe
PM> Install-Package mongocsharpdriver
You can download our jQWidgets SPA with Knockout and MongoDB example project from here. In the subsequent steps, we will "reverse-engineer" the code and examine each of its functionalities in detail. Since this is an MVC application, we will look into its Model (Step 3), Controller (Steps 4 and 5) and View (Steps 6 to 10).
Before you run the example, make sure you have set up MongoDB first (Steps 1.1 to 1.4). You do not need to install the C# driver, because it has already been included in the project.
The example has two views, which can be accessed from a single page - Timesheets and About. Timesheets shows the entries in our MongoDB database in a jqxDataTable instance and allows the basic CRUD functionalities - creating, reading, updating, and deleting of records. About shows information about the project and SPAs in general. Here is what the two SPA views look like:
We populate our database with timesheets, which have four propeties - FirstName,
LastName, Month and Year. The
timesheets model, Timesheet.cs
can be found in the project folder Models
. Here is the model code, bar the data annotations and JSON.NET
serialization attributes:
using System;using System.Collections.Generic;using System.ComponentModel.DataAnnotations;using System.Globalization;using System.Linq;using System.Web.Mvc;using Newtonsoft.Json;namespace MvcApplication.Models{public class Timesheet : Entity{public Timesheet(){Year = DateTime.Now.Year;}public string FirstName { get; set; }public string LastName { get; set; }public int Month { get; set; }public int Year { get; set; }public IEnumerable<SelectListItem> Months{get{var months = DateTimeFormatInfo.InvariantInfo.MonthNames.Select((monthName, index) => new SelectListItem{Value = (index + 1).ToString(),Text = monthName}).ToList();months.RemoveAt(12); // 13th item is emptyreturn months;}}}}
Note that the Timesheet class inherits Entity, whose mdel can be found in Models\Entity.cs
.
Here is the code of the timesheets controller, TimesheetsController.cs
,
sutiated in Controllers\Api
. It supports the HTTP verbs GET, POST,
PUT and DELETE via its methods.
using System;using System.Configuration;using System.Linq;using System.Net;using System.Net.Http;using System.Web.Http;using MongoDB.Bson;using MongoDB.Driver;using MvcApplication.Models;using MongoDB.Driver.Builders;namespace MvcApplication.Controllers.Api{public class TimesheetsController : ApiController{private readonly MongoDatabase _mongoDb;private readonly MongoCollection<Timesheet> _repository;public TimesheetsController(){var connectionString = ConfigurationManager.AppSettings["MongoDBTimesheets"];_mongoDb = MongoDatabase.Create(connectionString);_repository = _mongoDb.GetCollection<Timesheet>(typeof(Timesheet).Name);}// GET /api/timesheetspublic HttpResponseMessage Get(){var timesheets = _repository.FindAll().ToList();var response = Request.CreateResponse(HttpStatusCode.OK, timesheets);string uri = Url.Route(null, null);response.Headers.Location = new Uri(Request.RequestUri, uri);return response;}// GET /api/timesheets/4fd63a86f65e0a0e84e510de[HttpGet]public Timesheet Get(string id){var query = Query.EQ("_id", new ObjectId(id));return _repository.Find(query).Single();}// POST /api/timesheets[HttpPost]public HttpResponseMessage Post(Timesheet timesheet){_repository.Insert(timesheet);string uri = Url.Route(null, new { id = timesheet.Id }); // Where is the new timesheet?var response = Request.CreateResponse(HttpStatusCode.Created, timesheet);response.Headers.Location = new Uri(Request.RequestUri, uri);return response;}// PUT /api/timesheets[HttpPut]public HttpResponseMessage Put(Timesheet timesheet){var response = Request.CreateResponse(HttpStatusCode.OK, timesheet);_repository.Save(timesheet);string uri = Url.Route(null, new { id = timesheet.Id }); // Where is the modified timesheet?response.Headers.Location = new Uri(Request.RequestUri, uri);return response;}// DELETE /api/timesheets/4fd63a86f65e0a0e84e510depublic HttpResponseMessage Delete(params string[] ids){foreach (var id in ids){_repository.Remove(Query.EQ("_id", new ObjectId(id)));}return Request.CreateResponse(HttpStatusCode.NoContent);}}}
When an instance of TimesheetsController is created, it establishes a connection to MongoDB and creates a repository (MongoCollection) for the timesheets.
To demonstrate the Knockout binding of our SPA we need some initial data which to
bind. For that purpose, add the following code to the Index
method
of HomeController.cs
(in Controllers
):
var mongoDb = MongoDatabase.Create(ConfigurationManager.AppSettings["MongoDBTimesheets"]);var repository = mongoDb.GetCollection<Timesheet>(typeof(Timesheet).Name);var timesheets = new List<Timesheet>{new Timesheet { FirstName = "Martha", LastName = "Jones", Month = 0, Year = 2014 },new Timesheet { FirstName = "Rose", LastName = "Walker", Month = 5, Year = 2014 },new Timesheet { FirstName = "Amy", LastName = "Williams", Month = 3, Year = 2014 },new Timesheet { FirstName = "Lucie", LastName = "Queen", Month = 3, Year = 2014 }};foreach (var timesheet in timesheets)repository.Insert(timesheet);
After you run the application, the database should be populated with the four entries. Remove or comment out the database population code before the next run. Subsequent additions to the database will be done via the SPA's API (which will be examined in the next steps).
In Views\Shared
you can find the file _master.cshtml
,
which loads all needed external resources (styles and scripts - jQuery, Knockout,
jQWidgets, et al.) and renders the body of the page (through Index.cshtml
,
more on it - in Step 7). Here is the source code of _master.cshtml
:
?<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><title>jQWidgets Single-page Application (SPA)</title><meta name="viewport" content="width=device-width, initial-scale=1.0"><link href="@Url.Content("~/css/bootstrap.css")" rel="stylesheet" type="text/css" /><link href="@Url.Content("~/css/jqx.base.css")" rel="stylesheet" type="text/css" /><link href="@Url.Content("~/css/jqx.orange.css")" rel="stylesheet" type="text/css" /><link href="@Url.Content("~/css/site.css")" rel="stylesheet" type="text/css" /><link href="@Url.Content("~/css/bootstrap-responsive.css")" rel="stylesheet" type="text/css" /><script src="@Url.Content("~/js/jquery-1.8.2.min.js")" type="text/javascript"></script><script src="@Url.Content("~/js/knockout-3.0.0.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxcore.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxdata.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxdatatable.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxscrollbar.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxbuttons.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxmenu.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxwindow.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jqxknockout.js")" type="text/javascript"></script></head><body><div id="pageTitle">jQWidgets SPA</div><div>@RenderBody()</div><script src="@Url.Content("~/js/jquery.validate.min.js")" type="text/javascript"></script><script src="@Url.Content("~/js/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script></body></html>
In this and the next steps, we will take a look at the functionalities, implemented
in the file Index.cshtml
in Views\Home
.
To create an SPA effect, when you click on either Timesheets or About, the page is not reloaded. Instead, the views are just "switched" by hiding one of them and showing the other:
$('#jqxMenu').on('itemclick', function (event) {var item = $(event.target).text();if (item == "Timesheets" && $("#home").css("display") == "none"){$("#about").css("display", "none");$("#home").fadeIn();} else if (item == "About" && $("#about").css("display") == "none"){$("#home").css("display", "none");$("#about").fadeIn();}});
We will now focus solely on the Timesheets view as About contains only text.
First, in $(document).ready()
we apply the Knockout bindings of our
view model:
ko.applyBindings(viewModel);
Then we need to load all timesheets from the database. This is done by calling the
function loadTimesheets()
, which is implemented in the view model:
loadTimesheets: function () {var self = this;$.getJSON('@Url.RouteUrl("DefaultApi", new { httproute = "", controller = "timesheets" })',function (timesheets) {self.timesheets.removeAll();$.each(timesheets, function (index, item) {self.timesheets.push(new timesheet(item));});initializeDataTable();});},
Once loaded, the timesheets are bound to the table with id timesheets. For each timesheet, a new table row is added and each row displays the data of its corresponding timesheet object:
<table id="timesheets"><thead><tr><th>FirstName</th><th>LastName</th><th>Month</th><th>Year</th></tr></thead><tbody data-bind="foreach: viewModel.timesheets"><tr><td data-bind="text: firstname"></td><td data-bind="text: lastname"></td><td data-bind="text: month"></td><td data-bind="text: year"></td></tr></tbody></table>
And here is the definition of the timesheet object:
function timesheet(timesheet) {this.id = ko.observable(timesheet.id);this.firstname = ko.observable(timesheet.firstname);this.lastname = ko.observable(timesheet.lastname);this.month = ko.observable(timesheet.month);this.year = ko.observable(timesheet.year);this.update = function (timesheet) {this.id(timesheet.id);this.firstname(timesheet.firstname);this.lastname(timesheet.lastname);this.month(timesheet.month);this.year(timesheet.year);};}
Finally, when the timesheets are loaded, initializeDataTable()
is called,
which transforms the plain HTML table into a styled jqxDataTable (see the first
image above):
var initializeDataTable = function () {var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];$("#timesheets").jqxDataTable({theme: theme,width: "800px",altrows: true,sortable: true,selectionmode: "singleRow",columns: [{ text: 'First Name', dataField: 'FirstName', width: 250 },{ text: 'Last Name', dataField: 'LastName', width: 250 },{text: 'Month', dataField: 'Month', width: 200, cellsRenderer: function (row, column, value, rowData) {return months[parseInt(value) - 1];}},{ text: 'Year', dataField: 'Year', width: 100, align: 'right', cellsAlign: 'right' }]});$('#timesheets').on('rowSelect rowUnselect', function (event) {var selectedRows = $("#timesheets").jqxDataTable('getSelection');viewModel.rowSelection(selectedRows.length);viewModel.selectedRow(selectedRows[0]);});};
We will now look into the implementation of the "add timesheet" and "update timesheet" functionalities. By clicking the Add timesheet button, a modal window opens, containing a form for adding a new timesheet to the database:
If you select a row from the data table and click Update timesheet, the same window shows up, but this time the form is filled with the selected row data:
When we add/update information in the form and click Save, the
postTimesheet()
function is called:
postTimesheet: function (form) {form = $(form);if (!form.valid())return;var newRowData = this._getTimesheetFromFrom(form);var json = JSON.stringify(newRowData);var update = form.find("input[type='hidden'][id='id']").val();var httpVerb = !update ? "POST" : "PUT";var self = this;$.ajax({url: '@Url.RouteUrl("DefaultApi", new { httproute = "", controller = "timesheets" })',type: httpVerb,data: json,dataType: 'json',contentType: 'application/json; charset=utf-8',success: function (jsonObject) {if (update){var match = ko.utils.arrayFirst(self.timesheets(), function (item) {return jsonObject.id === item.id();});match.update(jsonObject);$("#timesheets").jqxDataTable('updateRow', viewModel.selectedRow().uid, { FirstName: jsonObject.firstname, LastName: jsonObject.lastname, Month: jsonObject.month, Year: jsonObject.year });}else{self.timesheets.push(new timesheet(jsonObject));// adds a new row to the data tablevar rowsCount = $("#timesheets").jqxDataTable("getRows");$("#timesheets").jqxDataTable('addRow', rowsCount.length, newRowData);}$("#timesheetWindow").jqxWindow("close");}});},
It determines whether this is a create or update operation and selects the appropriate
HTTP verb (POST or PUT). An Ajax call then updates the database accordingly. In
the success
callback function, the data table is updated to reflect
the database. Then the window is closed.
In this step we will take a look at the "delete timesheet" functionality of the
SPA. When you click on the Delete timesheet button, another modal
window opens, asking the user if he really wants to delete the timesheet. If Delete is clicked, the function deleteTimesheets()
is
called:
deleteTimesheets: function () {var index = getSelectedIndex(viewModel.selectedRow());var ids = new Array(this.timesheets()[index].id());var self = this;$.ajax({url: '@Url.RouteUrl("DefaultApi", new { httproute = "", controller = "timesheets" })',type: 'DELETE',data: ko.toJSON(ids),contentType: 'application/json; charset=utf-8',success: function () {$.each(ids, function (index, id) {var match = ko.utils.arrayFirst(self.timesheets(), function (item) {return id === item.id();});self.timesheets.remove(match);});$("#timesheets").jqxDataTable('deleteRow', index);$("#timesheetDelete").jqxWindow("close");}});}
It makes an Ajax call, which deletes the selected timesheet from the database. On
success
, the timesheet is removed from the data table as well.
The jQWidgets SPA with Knockout and MongoDB presents the user with all CRUD functionalities (create, read, update, delete) and exemplifies a single-page application, where the database is accessed and updated via Ajax calls and the page does not need to be reloaded to make changes visible to the user.