1. Preface
In the previous topic, I quickly introduced the knowledge points related to KnockoutJs and wrote some simple examples. I hope that through these examples, you can quickly get started with KnockoutJs. In order to let everyone clearly see the application of KnockoutJs in actual projects, this topic will introduce how to use WebApi+Bootstrap+KnockoutJs+Asp.net MVC to create a single-page web program. This model is also used in most companies' actual projects now.
2. SPA (single page) benefits
Before introducing the specific implementation, I feel it is necessary to introduce SPA in detail. SPA, the abbreviation of Single Page Web Application, is a web application that loads a single HTML page and updates the page dynamically when the user interacts with the application. The browser will load the required HTML, CSS and JavaScript at the beginning. All operations are completed on this page and are controlled by JavaScript.
The benefits of single-page programs are:
A better user experience allows users to experience the speed and smoothness of native apps in the Web app.
Separate the front and back end concerns, the front end is responsible for interface display, and the back end is responsible for data storage and computing, each performs its own duties and will not mix the logic of the front and back ends together.
To reduce the pressure on the server, the server only needs to generate data, regardless of display logic and page logic, and increase server throughput. The front-end written in Razor syntax in MVC requires the server to complete the synthesis of the page and then output it.
The same set of back-end programs can be used directly on multiple clients such as web interface, mobile phones, tablets, etc. without modification.
Of course, in addition to the advantages listed above, single-page programs also have their shortcomings:
Not conducive to SEO. If this is a management system, it will not affect it.
The initial loading time is relatively increased. Because all JS and CSS resources will be loaded at the first time, making the subsequent page smooth. For this, you can use Bundle in Asp.net MVC to bind files. For detailed use of Bundle, please refer to articles: http://www.VeVB.COM/article/84329.htm, http://www.VeVB.COM/article/84329.htm and http://www.VeVB.COM/article/82174.htm.
Navigation is not available. If you have to navigate, you must advance and retreat by yourself. For this, you can realize the forward and back functions by yourself to make up for it. In fact, this is what the mobile web pages do now, and now they still need to be navigation above. This can also be done for some enterprise backend management systems.
High development costs for developers. This is not a problem. Programmers need to keep learning to charge. Fortunately, some front-end frameworks are very easy to use.
3. Use Asp.net MVC+WebAPI+Bootstrap+KnockoutJS to implement SPA
The advantages and disadvantages of SPA were introduced in detail earlier. Next, let us use Asp.net MVC+WebAPI+BS+KO to implement a single-page program, so as to experience the smoothness of SPA and compare the effects of the pages made by the original Asp.net MVC+Razor.
1. Use VS2013 to create an Asp.net Web application project, check the MVC and WebAPI library. See the figure below for details:
2. Create corresponding warehouses and models. Here is a simple task management system. The specific model and warehousing code are as follows:
Task entity class implementation:
public enum TaskState { Active = 1, Completed =2 } /// <summary> /// Task entity//// </summary> public class Task { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } public DateTime CreationTime { get; set; } public DateTime FinishTime { get; set; } public string Owner { get; set; } public TaskState State { get; set; } public Task() { CreationTime = DateTime.Parse(DateTime.Now.ToLongDateString()); State = TaskState.Active; } }Task warehousing class implementation:
/// <summary> /// Here the warehouse uses sample data as a demonstration. The real project needs to be loaded dynamically from the database/// </summary> public class TaskRepository { #region Static Filed private static Lazy<TaskRepository> _taskRepository = new Lazy<TaskRepository>(() => new TaskRepository()); public static TaskRepository Current { get { return _taskRepository.Value; } } #endregion #region Fields private readonly List<Task> _tasks = new List<Task>() { new Task { Id =1, Name = "Create a SPA program", Description = "SPA(single page web application), The advantage of SPA is a small amount of bandwidth and a smooth experience", Owner = "Learning hard", FinishTime = DateTime.Parse(DateTime.Now.AddDays(1).ToString(CultureInfo.InvariantCulture)) }, new Task { Id =2, Name = "Learning KnockoutJs", Description = "KnockoutJs is an MVVM class library that supports two-way binding", Owner = "Tommy Li", FinishTime = DateTime.Parse(DateTime.Now.AddDays(2).ToString(CultureInfo.InvariantCulture)) }, new Task { Id =3, Name = "Learn AngularJS", Description = "AngularJs is an MVVM framework that integrates MVVM and MVC with one.", Owner = "Li Zhi", FinishTime = DateTime.Parse(DateTime.Now.AddDays(3).ToString(CultureInfo.InvariantCulture)) }, new Task { Id =4, Name = "Learn ASP.NET MVC website", Description = "Glimpse is a performance testing tool under .NET, which supports asp.net, asp.net mvc, EF, etc. The advantage is that it does not need to modify any code of the original project, and can output the execution time of each link of the code execution", Owner = "Tonny Li", FinishTime = DateTime.Parse(DateTime.Now.AddDays(4).ToString(CultureInfo.InvariantCulture)) }, }; #endregion #region Public Methods public IEnumerable<Task> GetAll() { return _tasks; } public Task Get(int id) { return _tasks.Find(p => p.Id == id); } public Task Add(Task item) { if (item == null) { throw new ArgumentNullException("item"); } item.Id = _tasks.Count + 1; _tasks.Add(item); return item; } public void Remove(int id) { _tasks.RemoveAll(p => p.Id == id); } public bool Update(Task item) { if (item == null) { throw new ArgumentNullException("item"); } var taskItem = Get(item.Id); if (taskItem == null) { return false; } _tasks.Remove(taskItem); _tasks.Add(item); return true; } #endregion }3. Add Bootstrap and KnockoutJs libraries through Nuget.
4. Implement back-end data services. Here the backend service is implemented using Asp.net WebAPI. The specific implementation code is as follows:
/// <summary> /// Task WebAPI, provide data services/// </summary> public class TasksController : ApiController { private readonly TaskRepository _taskRepository = TaskRepository.Current; public IEnumerable<Task> GetAll() { return _taskRepository.GetAll().OrderBy(a => a.Id); } public Task Get(int id) { var item = _taskRepository.Get(id); if (item == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } return item; } [Route("api/tasks/GetByState")] public IEnumerable<Task> GetByState(string state) { IEnumerable<Task> results = new List<Task>(); switch (state.ToLower()) { case "": case "all": results = _taskRepository.GetAll(); break; case "active": results = _taskRepository.GetAll().Where(t => t.State == TaskState.Active); break; case "completed": results = _taskRepository.GetAll().Where(t => t.State == TaskState.Completed); break; } results = results.OrderBy(t => t.Id); return results; } [HttpPost] public Task Create(Task item) { return _taskRepository.Add(item); } [HttpPut] public void Put(Task item) { if (!_taskRepository.Update(item)) { throw new HttpResponseException(HttpStatusCode.NotFound); } } public void Delete(int id) { _taskRepository.Remove(id); } }5. Use Asp.net MVC Bundle to package the resources. The corresponding BundleConfig implementation code is as follows:
/// <summary> /// Just add some missing CSS and JS files. Because some CSS and JS files have been added when creating the template /// </summary> public class BundleConfig { // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.validate*")); // Use the development version of Modernizr to develop with and learn from. Then, when you're // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( "~/Scripts/modernizr-*")); bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( "~/Scripts/bootstrap.js", "~/Scripts/bootstrap-datepicker.min.js")); bundles.Add(new StyleBundle("~/Content/css").Include( "~/Content/bootstrap.css", "~/Content/bootstrap-datepicker3.min.css", "~/Content/site.css")); bundles.Add(new ScriptBundle("~/bundles/knockout").Include( "~/Scripts/knockout-{version}.js", "~/Scripts/knockout.validation.min.js", "~/Scripts/knockout.mapping-latest.js")); bundles.Add(new ScriptBundle("~/bundles/app").Include( "~/Scripts/app/app.js")); } }6. Because we need to make the enum type appear as a string on the page. The enumeration is converted to a numeric type when serialized by default. Therefore, we need to make the following changes to the WebApiConfig class:
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services // Web API routing config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); // Make serialization use camel case style serialization property config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); // Serialize the string config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new StringEnumConverter()); } }Note: If the camel lowercase serialization is not used above, you should also make adjustments when binding data on the page. For example, when binding Name attribute, use Name capitalization directly. If you use the name method, it will prompt that there is no definition error in this attribute. Since JS uses camel lowercase style to name variables. Therefore, it is recommended that you use camel lowercase style for serialization. At this time, you can only use the form of "name" to bind. This is more in line with the JS code specifications.
7. Modify the corresponding Layout file and Index file content.
The specific code of the Layout file is as follows:
<!DOCTYPE html><html><head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title> Learninghard SPA Application</title> @Styles.Render("~/Content/css") @Scripts.Render("~/bundles/modernizr")</head> <body> <div> <div> <div> <p>Simple Task Management System</p> </div> <div> <ul> <li><a href="/">Home</a></li> </ul> </div> </div> </div> <div id="main"> @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - Learninghard SPA Application</p> </footer> </div> @Scripts.Render("~/bundles/jquery") @Scripts.Render("~/bundles/bootstrap") @Scripts.Render("~/bundles/knockout") @Scripts.Render("~/bundles/knockout") @Scripts.Render("~/bundles/app") </body></html> The Index page code is as follows: @{ ViewBag.Title = "Index"; Layout = "~/Views/Shared/_Layout.cshtml";}<div id="list" data-bind="if:canCreate"><h2>Tasks</h2><div> <table> <thead> <tr> <th>Number</th> <th>Name</th> <th>Description</th> <th>People in charge</th> <th>Creation time</th> <th>Completion time</th> <th>Statu</th> <th></th> </tr> </t> <tbody data-bind="foreach:tasks"> <tr> <td data-bind="text:id"></td> <td><a data-bind="text: name, click: handleCreateOrUpdate"></a></td> <td data-bind="text: description"></td> <td data-bind="text: owner"></td> <td data-bind="text: creationTime"></td> <td data-bind="text: finishTime"></td> <td data-bind="text: state"></td> <td><a data-bind="click:remove" href="javascript:void(0)">Remove</a></td> </tbody> </table></div><div> <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('all') }">All </a> | <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('active') }"> Active</a> | <a href="javascript:void(0)" data-bind="click: function(data, event){ setTaskList('completed') }"> Completed</a></div><div> <a href="javascript:void(0)" data-bind="click: handleCreateOrUpdate">Add task</a></div><div id="create" style="visibility: hidden"> <h2>Add task</h2> <br/> <div> <div> <label for="taskName">Name*</label> <div> <input type="text" data-bind="value: name" id="taskName" name="taskName" placeholder="name"> </div> </div> <div> <label for="taskDesc">Description</label> <div> <textarea data-bind="value: description" rows="3" id="taskDesc" name="taskDesc" placeholder="Description"></textarea> </div> </div> <div> <label for="taskOwner">Person in charge*</label> <div> <input id="taskOwner" name="taskOwner" data-bind="value: owner" placeholder="header"> </div> </div> <div> <label for="taskFinish">Estimated completion time*</label> <div> <input id="taskFinish" data-bind="value: finishTime" name="taskFinish"> </div> </div> <div> <label for="taskOwner">Status*</label> <div> <select id="taskState" data-bind="value: state"> <option>Active</option> <option>Completed</option> </select> </div> </div> <div> <div> <button data-bind="click:handleSaveClick">Save</button> <button data-bind="click: handleBackClick">Back</button> </div> </div> </div></div>8. Create the corresponding front-end script logic. Use JS code to request data and create corresponding ViewModel objects for front-end binding. The specific JS implementation code is as follows:
var taskListViewModel = { tasks: ko.observableArray(), canCreate:ko.observable(true)};var taskModel = function () { this.id = 0; this.name = ko.observable(); this.description = ko.observable(); this.finishTime = ko.observable(); this.owner = ko.observable(); this.state = ko.observable(); this.fromJS = function(data) { this.id = data.id; this.name(data.name); this.description(data.description); this.finishTime(data.finishTime); this.owner(data.owner); this.state(data.state); };};function getAllTasks() { sendAjaxRequest("GET", function (data) { taskListViewModel.tasks.removeAll(); for (var i = 0; i < data.length; i++) { taskListViewModel.tasks.push(data[i]); } }, 'GetByState', { 'state': 'all' });}function setTaskList(state) { sendAjaxRequest("GET", function(data) { taskListViewModel.tasks.removeAll(); for (var i = 0; i < data.length; i++) { taskListViewModel.tasks.push(data[i]); }},'GetByState',{ 'state': state });}function remove(item) { sendAjaxRequest("DELETE", function () { getAllTasks(); }, item.id);}var task = new taskModel();function handleCreateOrUpdate(item) { task.fromJS(item); initDatePicker(); taskListViewModel.canCreate(false); $('#create').css('visibility', 'visible');}function handleBackClick() { taskListViewModel.canCreate(true); $('#create').css('visibility', 'hidden');}function handleSaveClick(item) { if (item.id == undefined) { sendAjaxRequest("POST", function (newItem) { //newitem is the returned object. taskListViewModel.tasks.push(newItem); }, null, { name: item.name, description: item.description, finishTime: item.finishTime, owner: item.owner, state: item.state }); } else { sendAjaxRequest("PUT", function () { getAllTasks(); }, null, { id:item.id, name: item.name, description: item.description, finishTime: item.finishTime, owner: item.owner, state: item.state }); } taskListViewModel.canCreate(true); $('#create').css('visibility', 'hidden');}function sendAjaxRequest(httpMethod, callback, url, reqData) { $.ajax("/api/tasks" + (url ? "/" + url : ""), { type: httpMethod, success: callback, data: reqData });}var initDatePicker = function() { $('#create .datepicker').datepicker({ autoclose: true });};$('.nav').on('click', 'li', function() { $('.nav li.active').removeClass('active'); $(this).addClass('active');});$(document).ready(function () { getAllTasks(); // Use KnockoutJs to bind ko.applyBindings(taskListViewModel, $('#list').get(0)); ko.applyBindings(task, $('#create').get(0));});At this point, our single-page program has been developed. Next, let’s run it to see its effect.
From the above running result demonstration diagram, we can see that once the page is loaded, all operations seem to operate on one page, and it feels like the browser page is circling. Compared with the pages developed using Asp.net MVC + Razor before, do you feel the smoothness of SPA? Previously, the page developed using Asp.net MVC + Razor, you just need to request one page and you can feel the refresh of the entire page, which makes the user experience very bad.
4. Comparison with Razor development model
I believe everyone has seen the advantages of SPA from the results. Next, I think it is necessary to compare it with the traditional way of implementing web pages. There are two main differences from Razor development method:
1. When the page is rendered, the data is processed on the browser side. Not on the server. Allocate rendering pressure to the browser side of each user, thereby reducing the pressure on the website server. If it were Razor syntax, the front-end page binding statement should be as follows:
@Model IEnumerable<KnockoutJSSPA.Models.Task> @foreach (var item in Model){ <tr> <td>@item.Name</td> <td>@item.Description</td> </tr>}These are rendered by the Razor engine on the server side. This is also the reason why pages developed using Razor will see the pages circling. Because every time you switch a page, you need to request the server to render, and after the server renders, return the html to the client for display.
2. The bound data is dynamic. This means that changes in the data model will be reflected on the page immediately. This effect is attributed to the two-way binding mechanism implemented by KnockoutJs.
Using this method is also simple for program development. The Web API is only responsible for providing data, and the front-end page also reduces a lot of DOM operations. Because DOM operations are complicated and error-prone. This also means reducing implicit bugs in the program. Moreover, a back-end service can be used by mobile phones, web browsers and multiple platforms to avoid repeated development.
5. Summary
At this point, the introduction of this article will be introduced. This article mainly introduces the use of KnockoutJs to complete a SPA program. In fact, in actual work, the model of creating single-page programs is more based on AngularJS. Then there are many KnockoutJs, but KnockoutJs is just an MVVM framework, and its routing mechanism needs to be used with some other class libraries, such as the routing mechanism in Asp.net MVC here, you can also use the director.js front-end routing framework. Compared with KnockoutJs, AngularJs is an MVVM+MVC framework. So in the next topic, we will introduce how to use AngularJs to create a single page program (SPA).
Download all source codes in this article: SPAWithKnockoutJs
If you still want to study in depth, you can click here to study and attach 3 exciting topics to you:
Bootstrap learning tutorial
Bootstrap practical tutorial
Bootstrap plug-in usage tutorial
The above is all about this article, I hope it will be helpful to everyone's learning.