Contents
Create WebServices Project
* Create a new project named WebServices
– select Empty template
– select both MVC and Web API features
Model
* Create model class named Reservation with three properties (id, name, location):
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace WebServices.Models { public class Reservation { public int ReservationId { get; set; } public string ClientName { get; set; } public string Location { get; set; } } }
* Create a Helper class named ReservationRespository to act as temp data storage. Normally this could be a DAO object.
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace WebServices.Models { public class ReservationRespository { private static ReservationRespository repo = new ReservationRespository(); public static ReservationRespository Current { get { return repo; } } private List<Reservation> data = new List<Reservation> { new Reservation { ReservationId = 1, ClientName = "Adam", Location = "Board Room"}, new Reservation { ReservationId = 2, ClientName = "Jacqui", Location = "Lecture Hall"}, new Reservation { ReservationId = 3, ClientName = "Russell", Location = "Meeting Room 1"}, }; public IEnumerable<Reservation> GetAll() { return data; } public Reservation Get(int id) { return data.Where(r => r.ReservationId == id).FirstOrDefault(); } public Reservation Add(Reservation item) { item.ReservationId = data.Count + 1; data.Add(item); return item; } public void Remove(int id) { Reservation item = Get(id); if (item != null) { data.Remove(item); } } public bool Update(Reservation item) { Reservation storedItem = Get(item.ReservationId); if (storedItem != null) { storedItem.ClientName = item.ClientName; storedItem.Location = item.Location; return true; } else { return false; } } } }
Install jQuery, Bootstrap, and Knockout Packages
* Install from Tools > NuGet Package Manager:
Install-Package jquery –version 1.10.2 Install-Package bootstrap –version 3.0.0 Install-Package knockoutjs –version 3.0.0
Controller
* Add a controller named HomeController
* Implement CRUD actions:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using WebServices.Models; namespace WebServices.Controllers { public class HomeController : Controller { private ReservationRespository repo = ReservationRespository.Current; public ViewResult Index() { return View(repo.GetAll()); } public ActionResult Add(Reservation item) { if (ModelState.IsValid) { repo.Add(item); return RedirectToAction("Index"); } else { return View("Index"); } } public ActionResult Remove(int id) { repo.Remove(id); return RedirectToAction("Index"); } public ActionResult Update(Reservation item) { if (ModelState.IsValid && repo.Update(item)) { return RedirectToAction("Index"); } else { return View("Index"); } } } }
Views
Layout View
* Add a MVC5 layout file called _Layout.cshtml in Views/Shared folder
* The layout page defines two sections named:
– Scripts
– Body
* It also includes Bootstrap CSS files
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> <link href="~/Content/bootstrap.css" rel="stylesheet" /> <link href="~/Content/bootstrap.min.css" rel="stylesheet" /> @RenderSection("Scripts") </head> <body> @RenderSection("Body") </body> </html>
Index View
* Create a view named Index in Views > Home folder.
* It references two partial views using the @Html.Partial() function:
– Summary
– Editor
@using WebServices.Models; @model IEnumerable<Reservation> @{ ViewBag.Title = "Reservations"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section Scripts { } @section Body { <div id="summary" class="section panel panel-primary"> @Html.Partial("Summary", Model) </div> <div id="editor" class="section panel panel-primary"> @Html.Partial("Editor", new Reservation()) </div> }
Summary Partial View
* Create Summary partial view in View > Home directory. Select List as template and make sure to check the Create as partial view option.
* Edit the page to loop through and display the reservation items:
@model IEnumerable<WebServices.Models.Reservation> <div class="panel-heading">Reservation Summary</div> <div class="panel-body"> <table class="table table-striped table-condensed"> <thead> <tr> <th>ID</th> <th>Name</th> <th>Location</th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td>@item.ReservationId</td> <td>@item.ClientName</td> <td>@item.Location</td> <td> @Html.ActionLink( "Remove", "Remove", new { id = item.ReservationId }, new { @class = "btn btn-xs btn-primary"} ) </td> </tr> } </tbody> </table> </div>
Editor Partial View
* Similarly create a partial view named Editor
* Edit the view to include reservation update form:
@model WebServices.Models.Reservation <div class="panel-heading"> Create Reservation </div> <div class="panel-body"> @using(Html.BeginForm("Add", "Home")) { <div class="form-group"> <label>Client Name</label> @Html.TextBoxFor(m => m.ClientName, new {@class="form-control"}) </div> <div class="form-group"> <label>Location</label> @Html.TextBoxFor(m => m.Location, new { @class="form-control"}) </div> <button type="submit" class="btn btn-primary">Save</button> } </div>
Test Index Page
Web API
* Uses ApiController:
– Action methods return model object instead of ActionResult object
– Returned model object is encoded in JSON format
– Action methods are selected based on the HTTP method (GET, POST, DELETE, PUT) used in the request
– Action methods can also be manually mapped with C# attributes (note the System.Web.Http namespace:
System.Web.Http.HttpGet
System.Web.Http.HttpPost
System.Web.Http.HttpPut
System.Web.Http.HttpDelete
For example:
[HttpPost] public Reservation CreateReservation(Reservation item) { return repo.Add(item); }
Create API Controller
* Right click Controllers folder and create a controller named WebController
* Extends new controller with ApiController
using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http; using WebServices.Models; namespace WebServices.Controllers { public class WebController : ApiController { private ReservationRespository repo = ReservationRespository.Current; public IEnumerable<Reservation> GetAllReservations() { return repo.GetAll(); } public Reservation GetReservation(int id) { return repo.Get(id); } public Reservation PostReservation(Reservation item) { return repo.Add(item); } public bool PutReservation(Reservation item) { return repo.Update(item); } public void DeleteReservation(int id) { repo.Remove(id); } } }
* Web API routing rules are defined in App_Start > WebApiConfig.cs
– it does not use action parameter in the route template
using System; using System.Collections.Generic; using System.Linq; using System.Web.Http; namespace WebServices { public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routes config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } }
Test Drive Web API
* Start project and point browser to
–/api/web
* You get json data with all reservation items as reply:
[{"ReservationId":1,"ClientName":"Adam","Location":"Board Room"},{"ReservationId":2,"ClientName":"Jacqui","Location":"Lecture Hall"},{"ReservationId":3,"ClientName":"Russell","Location":"Meeting Room 1"}]
* If you point to URL to include reservation id:
–/api/web/3
* You get the specific reservation item:
{"ReservationId":3,"ClientName":"Russell","Location":"Meeting Room 1"}
Single Page App (SPA) with Knockout
SPA vs MVC
* MVC uses model data in HTML pages, SPA does not
– SPA uses JSON from Web API calls
* SPA has Ajax behavior, MVC does not
* For SPA, data is processed by the browser, while MVC at server
Add Knockout Support
* Note you can also use AngularJS which is another more comprehensive alternative JavaScript framework.
Add Knockout and jQuery Java Script Files
* Add to _Layout.cshtml page
<script src="~/Scripts/jquery-1.10.2.js"></script> <script src="~/Scripts/knockout-3.0.0.js"></script>
Add Knockout Based Model Object
* aka observable objects
* Define a JavaScript model object as Knockout observable objects:
var model = { reservations: ko.observableArray() };
Base jQuery Method: sendAjaxRequest
function sendAjaxRequest(httpMethod, callback, url) { $.ajax("/api/web" + (url ? "/" + url : ""), { type : httpMethod, success : callback } ); }
Define JavaScript CRUD Methods
* Define JavaScript CRUD methods using base sendAjaxRequest method and model object:
function getAllItems() { sendAjaxRequest( "GET", function (data) { model.reservations.removeAll(); for (var i = 0; i < data.length; i++) { model.reservations.push(data[i]); } } ); } // Two ajax calls /*function removeItem(item) { sendAjaxRequest( "DELETE", function () { getAllItems(); // Second ajax call }, item.ReservationId ); }*/ // Repalce second ajax call function removeItem(item) { console.log("removeItem: " + item.ReservationId); sendAjaxRequest( "DELETE", function () { for (var i = 0; i < model.reservations().length; i++) { if (model.reservations()[i].ReservationId == item.ReservationId) { model.reservations.remove(model.reservations()[i]); break; } } }, item.ReservationId ); }
Bind Model to HTML Element with knockout
* Knockout uses data-bind=”type: expression” attributes to bind model object to HTML elements:
<table class="table table-striped table-condensed"> <thead> <tr><th>ID</th><th>Name</th><th>Location</th><th></th></tr> </thead> <tbody data-bind="foreach: model.reservations"> <tr> <td data-bind="text: ReservationsId"></td> <td data-bind="text: ClientName"></td> <td data-bind="text: Location"></td> <td> <button class="btn btn-xs btn-primary" data-bind="click: removeItem">Remove</button> </td> </tr> </tbody> </table>
Start Knockout Binding
$(document).ready( function () { getAllItems(); ko.applyBindings(model); } );
Complete SPA.cshtml File
@using WebServices.Models @{ ViewBag.Title = "SPA"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section Scripts { <script> var model = { reservations: ko.observableArray() }; function sendAjaxRequest(httpMethod, callback, url) { $.ajax("/api/web" + (url ? "/" + url : ""), { type: httpMethod, success: callback } ); } function getAllItems() { sendAjaxRequest( "GET", function (data) { model.reservations.removeAll(); for (var i = 0; i < data.length; i++) { model.reservations.push(data[i]); } } ); } function removeItem(item) { console.log("removeItem: " + item.ReservationId); sendAjaxRequest( "DELETE", function () { for (var i = 0; i < model.reservations().length; i++) { if (model.reservations()[i].ReservationId == item.ReservationId) { model.reservations.remove(model.reservations()[i]); break; } } }, item.ReservationId ); } $(document).ready( function () { getAllItems(); ko.applyBindings(model); } ); </script> } @section Body { <div id="summary" class="section panel panel-primary"> <div class="panel-heading">Reservation Summary</div> <div class="panel-body"> <table class="table table-striped table-condensed"> <thead> <tr><th>ID</th><th>Name</th><th>Location</th><th></th></tr> </thead> <tbody data-bind="foreach: model.reservations"> <tr> <td data-bind="text: ReservationId"></td> <td data-bind="text: ClientName"></td> <td data-bind="text: Location"></td> <td> <button class="btn btn-xs btn-primary" data-bind="click: removeItem"> Remove </button> </td> </tr> </tbody> </table> </div> </div> }
Test SPA Page
* Add action method to HomeController to route Home/SPA to SPA.cshtml page:
public ViewResult SPA() { return View("SPA"); }
* Debug SPA page:
Add Create Support
Modify JavaScript
* Extend model object to include displaySummary flag field which is used to show/hide summary section:
var model = { reservations: ko.observableArray(), editor: { name: ko.observable(""), location: ko.observable("") }, displaySummary: ko.observable(true) };
* Modify base ajax send function to include post data:
function sendAjaxRequest(httpMethod, callback, url, reqData) { $.ajax("/api/web" + (url ? "/" + url : ""), { type: httpMethod, success: callback, data: reqData } ); }
* Hide Summary section when Create button is clicked:
function handleCreateClick() { model.displaySummary(false); }
* Collect post values and show Summary section when Save button is clicked.
function handleEditorClick() { sendAjaxRequest( "POST", function (newItem) { model.reservations.push(newItem); model.displaySummary(true); }, null, { ClientName: model.editor.name, Location: model.editor.location } ) }
Add Create HTML Code
* Add HTML code to collect client name and location values as well as a Create button
* Use Knockout if: and ifnot: functions to show/hide sections
<div id="editor" class="section panel panel-primary" data-bind="ifnot: model.displaySummary"> <div class="panel-heading"> Create Reservation </div> <div class="panel-body"> <div class="form-group"> <label>Client Name</label> <input class="form-control" data-bind="value: model.editor.name" /> </div> <div class="form-group"> <label>Location</label> <input class="form-control" data-bind="value: model.editor.location" /> </div> <button class="btn btn-primary" data-bind="click: handleEditorClick"> Save </button> </div> </div>
Final Page
@using WebServices.Models @{ ViewBag.Title = "SPA"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section Scripts { <script> var model = { reservations: ko.observableArray(), editor: { name: ko.observable(""), location: ko.observable("") }, displaySummary: ko.observable(true) }; function sendAjaxRequest(httpMethod, callback, url, reqData) { $.ajax("/api/web" + (url ? "/" + url : ""), { type: httpMethod, success: callback, data: reqData } ); } function getAllItems() { sendAjaxRequest( "GET", function (data) { model.reservations.removeAll(); for (var i = 0; i < data.length; i++) { model.reservations.push(data[i]); } }, null ); } function removeItem(item) { sendAjaxRequest( "DELETE", function () { for (var i = 0; i < model.reservations().length; i++) { if (model.reservations()[i].ReservationId == item.ReservationId) { model.reservations.remove(model.reservations()[i]); break; } } }, item.ReservationId, null ); } function handleCreateClick() { model.displaySummary(false); } function handleEditorClick() { sendAjaxRequest( "POST", function (newItem) { model.reservations.push(newItem); model.displaySummary(true); }, null, { ClientName: model.editor.name, Location: model.editor.location } ) } $(document).ready( function () { getAllItems(); ko.applyBindings(model); } ); </script> } @section Body { <div id="summary" class="section panel panel-primary" data-bind="if: model.displaySummary"> <div class="panel-heading">Reservation Summary</div> <div class="panel-body"> <table class="table table-striped table-condensed"> <thead> <tr><th>ID</th><th>Name</th><th>Location</th><th></th></tr> </thead> <tbody data-bind="foreach: model.reservations"> <tr> <td data-bind="text: ReservationId"></td> <td data-bind="text: ClientName"></td> <td data-bind="text: Location"></td> <td> <button class="btn btn-xs btn-primary" data-bind="click: removeItem"> Remove </button> </td> </tr> </tbody> </table> <button class="btn btn-primary" data-bind="click: handleCreateClick">Create</button> </div> </div> <div id="editor" class="section panel panel-primary" data-bind="ifnot: model.displaySummary"> <div class="panel-heading"> Create Reservation </div> <div class="panel-body"> <div class="form-group"> <label>Client Name</label> <input class="form-control" data-bind="value: model.editor.name" /> </div> <div class="form-group"> <label>Location</label> <input class="form-control" data-bind="value: model.editor.location" /> </div> <button class="btn btn-primary" data-bind="click: handleEditorClick"> Save </button> </div> </div> }
Test
* Start page:
* Click Create button:
* Click Save: