1.1.1 摘要
在博文《Ember.js實現單頁面應用程序》中,我們介紹了使用Ember JS實現一個單頁應用程序 (SPA),這使我想起了幾年前寫過一個任務管理程序,通過選擇日期,然后編輯時間來增加任務信息。
當時,我們是使用ASP.NET和jQuery實現了任務管理程序的,通過ajax調用ASP.NET的Webservice方法來訪問數據庫。
今天,我們將通過任務管理程序的實現,來介紹使用ASP.NET Web API和Knockout JS的結合使用,想必許多人都有使用過任務管理程序,其中我覺得Google日歷是一個不錯的任務管理器
圖1 Google日歷
目錄
1.1.2 正文
通過圖1Google日歷,我們發現它使用一個Date Picker,讓用戶選擇編輯的日期、還有一個24小時的表格,當用戶點擊表上的一個時間區域就顯示一個彈出式窗口,讓用戶編輯任務的內容,現在大概了解了基本的界面設計了,接下來我們將通過ASP.NET Web API作為服務端,開放API讓Knockout JS調用接口獲取數據。
創建ASP.NET MVC 項目
首先,我們在VS2012中創建一個ASP.NET MVC 4 Web項目。
然后,我們打開Package Manager Console,添加Package引用,要使用的庫如下:
- PM> install-package jQuery
- PM> install-package KnockoutJS
- PM> install-package Microsoft.AspNet.Web.Optimization
- PM> update-package Micrsoft.AspNet.WebApi
- PM> install-package EntityFramework
圖2 ASP.NET MVC 4 Web Application
創建數據表
接着,我們在數據庫中添加表TaskDays和TaskDetails,TaskDays保存所有任務的日期,那么一個日期只有一行記錄保存該表中,它包含了Id(自增)和Day字段,TaskDetails保存不同時間短任務信息,它包含Id(自增)、Title、Details、Starts、Ends和ParentTaskId等字段,其中ParentTaskId保存任務的日期的Id值。
圖3 表TaskDays和TaskDetails
數據傳輸對象
前面,我們已經定義了數據表TaskDays和TaskDetails並且通過ParentTaskId建立了表之間的關系,接下來,我們將根表定義數據傳輸對象,具體定義如下:
/// <summary> /// Defines a DTO TaskCalendar. /// </summary> public class TaskDay { public TaskDay() { Tasks = new List<TaskDetail>(); } public int Id { get; set; } public DateTime Day { get; set; } public List<TaskDetail> Tasks { get; set; } } /// <summary> /// Defines a DTO TaskDetail. /// </summary> public class TaskDetail { public int Id { get; set; } public string Title { get; set; } public string Details { get; set; } public DateTime Starts { get; set; } public DateTime Ends { get; set; } [ForeignKey("ParentTaskId")] [ScriptIgnore] public TaskDay ParentTask { get; set; } public int ParentTaskId { get; set; } }
上面,我們定義了數據傳輸對象TaskDays和TaskDetails,在TaskDays類中,我們定義了一個List<TaskDetail>類型的字段並且在構造函數中實例化該字段,通過保持TaskDetail類型的強對象引用,從而建立起TaskDays和TaskDetails之間的聚合關系,也就是TaskDay和TaskDetails是一對多的關系。
創建控制器
這里我們的ASP.NET MVC程序作為服務端向客戶端開放API接口,所以我們創建控制器CalendarController並且提供數據庫操作方法,具體實現如下:
/// <summary> /// The server api controller. /// </summary> public class CalendarController : ApiController { /// <summary> /// Gets the task details. /// </summary> /// <param name="id">The identifier.</param> /// <returns>A list of task detail.</returns> /// /api/Calendar/GetTaskDetails?id [HttpGet] public List<TaskDetail> GetTaskDetails(DateTime id) { } /// <summary> /// Saves the task. /// </summary> /// <param name="taskDetail">The task detail.</param> /// <returns></returns> /// /api/Calendar/SaveTask?taskDetail [HttpPost] public bool SaveTask(TaskDetail taskDetail) { } /// <summary> /// Deletes the task. /// </summary> /// <param name="id">The identifier.</param> /// <returns></returns> /// /api/Calendar/DeleteTask?id [HttpDelete] public bool DeleteTask(int id) { } }
在控制器CalendarController中我們定義了三個方法分別是SaveTask()、DeleteTask()和GetTaskDetails(),想必大家一看都知道這三個方法的作用,沒錯就是傳統的增刪查API,但我們這里並沒有給出具體數據庫操作代碼,因為我們將使用Entity Framework替代傳統ADO.NET操作。
Entity Framework數據庫操作
接下來,我們定義類TaskDayRepository和TaskDetailRepository,它們使用Entity Framework對數據庫進行操作,具體定義如下:
/// <summary> /// Task day repository /// </summary> public class TaskDayRepository : ITaskDayRepository { readonly TaskCalendarContext _context = new TaskCalendarContext(); /// <summary> /// Gets all tasks. /// </summary> public IQueryable<TaskDay> All { get { return _context.TaskDays.Include("Tasks"); } } /// <summary> /// Alls the including tasks. /// </summary> /// <param name="includeProperties">The include properties.</param> /// <returns></returns> public IQueryable<TaskDay> AllIncluding(params Expression<Func<TaskDay, object>>[] includeProperties) { IQueryable<TaskDay> query = _context.TaskDays; foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } /// <summary> /// Finds the specified identifier. /// </summary> /// <param name="id">The identifier.</param> /// <returns></returns> public TaskDay Find(int id) { return _context.TaskDays.Find(id); } /// <summary> /// Inserts the or update. /// </summary> /// <param name="taskday">The taskday.</param> public void InsertOrUpdate(TaskDay taskday) { if (taskday.Id == default(int)) { _context.TaskDays.Add(taskday); } else { _context.Entry(taskday).State = EntityState.Modified; } } /// <summary> /// Saves this instance. /// </summary> public void Save() { _context.SaveChanges(); } /// <summary> /// Deletes the specified identifier. /// </summary> /// <param name="id">The identifier.</param> public void Delete(int id) { var taskDay = _context.TaskDays.Find(id); _context.TaskDays.Remove(taskDay); } public void Dispose() { _context.Dispose(); } } public interface ITaskDayRepository : IDisposable { IQueryable<TaskDay> All { get; } IQueryable<TaskDay> AllIncluding(params Expression<Func<TaskDay, object>>[] includeProperties); TaskDay Find(int id); void InsertOrUpdate(TaskDay taskday); void Delete(int id); void Save(); }
上面,我們定義類TaskDayRepository,它包含具體的數據庫操作方法:Save()、Delete()和Find(),TaskDetailRepository的實現和TaskDayRepository基本相似,所以我們很快就可以使用TaskDetailRepository,具體定義如下:
/// <summary> /// Task detail repository /// </summary> public class TaskDetailRepository : ITaskDetailRepository { readonly TaskCalendarContext _context = new TaskCalendarContext(); /// <summary> /// Gets all. /// </summary> public IQueryable<TaskDetail> All { get { return _context.TaskDetails; } } /// <summary> /// Alls the including task details. /// </summary> /// <param name="includeProperties">The include properties.</param> /// <returns></returns> public IQueryable<TaskDetail> AllIncluding(params Expression<Func<TaskDetail, object>>[] includeProperties) { IQueryable<TaskDetail> query = _context.TaskDetails; foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query; } /// <summary> /// Finds the specified identifier. /// </summary> /// <param name="id">The identifier.</param> /// <returns></returns> public TaskDetail Find(int id) { return _context.TaskDetails.Find(id); } /// <summary> /// Saves this instance. /// </summary> public void Save() { _context.SaveChanges(); } /// <summary> /// Inserts the or update. /// </summary> /// <param name="taskdetail">The taskdetail.</param> public void InsertOrUpdate(TaskDetail taskdetail) { if (default(int) == taskdetail.Id) { _context.TaskDetails.Add(taskdetail); } else { _context.Entry(taskdetail).State = EntityState.Modified; } } /// <summary> /// Deletes the specified identifier. /// </summary> /// <param name="id">The identifier.</param> public void Delete(int id) { var taskDetail = _context.TaskDetails.Find(id); _context.TaskDetails.Remove(taskDetail); } public void Dispose() { _context.Dispose(); } } public interface ITaskDetailRepository : IDisposable { IQueryable<TaskDetail> All { get; } IQueryable<TaskDetail> AllIncluding(params Expression<Func<TaskDetail, object>>[] includeProperties); TaskDetail Find(int id); void InsertOrUpdate(TaskDetail taskdetail); void Delete(int id); void Save(); }
上面我們通過Entity Framework實現了數據的操作,接下來,讓我們控制器CalendarController的API的方法吧!
/// <summary> /// The server api controller. /// </summary> public class CalendarController : ApiController { readonly ITaskDayRepository _taskDayRepository = new TaskDayRepository(); readonly TaskDetailRepository _taskDetailRepository = new TaskDetailRepository(); /// <summary> /// Gets the task details. /// </summary> /// <param name="id">The identifier.</param> /// <returns>A list of task detail.</returns> /// /api/Calendar/GetTaskDetails?id [HttpGet] public List<TaskDetail> GetTaskDetails(DateTime id) { var taskDay = _taskDayRepository.All.FirstOrDefault<TaskDay>(_ => _.Day == id); return taskDay != null ? taskDay.Tasks : new List<TaskDetail>(); } /// <summary> /// Saves the task. /// </summary> /// <param name="taskDetail">The task detail.</param> /// <returns></returns> /// /api/Calendar/SaveTask?taskDetail [HttpPost] public bool SaveTask(TaskDetail taskDetail) { var targetDay = new DateTime( taskDetail.Starts.Year, taskDetail.Starts.Month, taskDetail.Starts.Day); // Check new task or not. var day = _taskDayRepository.All.FirstOrDefault<TaskDay>(_ => _.Day == targetDay); if (null == day) { day = new TaskDay { Day = targetDay, Tasks = new List<TaskDetail>() }; _taskDayRepository.InsertOrUpdate(day); _taskDayRepository.Save(); taskDetail.ParentTaskId = day.Id; } else { taskDetail.ParentTaskId = day.Id; taskDetail.ParentTask = null; } _taskDetailRepository.InsertOrUpdate(taskDetail); _taskDetailRepository.Save(); return true; } /// <summary> /// Deletes the task. /// </summary> /// <param name="id">The identifier.</param> /// <returns></returns> /// /api/Calendar/DeleteTask?id [HttpDelete] public bool DeleteTask(int id) { try { _taskDetailRepository.Delete(id); _taskDetailRepository.Save(); return true; } catch (Exception) { return false; } } }
Knockout JS
上面,我們通過APS.NET MVC實現了服務端,接下來,我們通過Knockout JS實現客戶端訪問服務端,首先,我們在Script文件中創建day-calendar.js和day-calendar.knockout.bindinghandlers.js文件。
// The viem model type. var ViewModel = function () { var $this = this, d = new Date(); // Defines observable object, when the selectedDate value changed, will // change data bind in the view. $this.selectedDate = ko.observable(new Date(d.getFullYear(), d.getMonth(), d.getDate())); $this.selectedTaskDetails = ko.observable(new TaskDetails(d)); // A serial of observable object observableArray. $this.dateDetails = ko.observableArray(); $this.appointments = ko.observableArray(); // Init date details list. $this.initializeDateDetails = function () { $this.dateDetails.removeAll(); for (var i = 0; i < 24; i++) { var dt = $this.selectedDate(); $this.dateDetails.push({ count: i, TaskDetails: new GetTaskHolder(i, dt) }); } }; // Call api to get task details. $this.getTaskDetails = function (date) { var dt = new Date(date.getFullYear(), date.getMonth(), date.getDate()), uri = "/api/Calendar/GetTaskDetails"; // Deprecation Notice: The jqXHR.success(), jqXHR.error(), and jqXHR.complete() callbacks are deprecated as of jQuery 1.8. // To prepare your code for their eventual removal, use jqXHR.done(), jqXHR.fail(), and jqXHR.always() instead. // Reference: https://api.jquery.com/jQuery.ajax/ $.get(uri, 'id=' + dt.getFullYear() + '-' + (dt.getMonth() + 1) + '-' + dt.getDate() ).done(function(data) { $this.appointments.removeAll(); $(data).each(function(i, item) { $this.appointments.push(new Appointment(item, i)); }); }).error(function(data) { alert("Failed to retrieve tasks from server."); }); }; };
上面,我們定義了ViewModel類型,並且在其中定義了一系列的方法。
- selectedDate:獲取用戶在日歷控件中選擇的日期。
- selectedTaskDetails:獲取用戶選擇中TaskDetail對象。
- dateDetails:定義監控數組,它保存了24個時間對象。
- appointments:定義監控數組保存每個TaskDetail對象。
接下來,需要獲取用戶點擊日歷控件的操作,我們通過方法ko.bindingHandlers()自定義事件處理方法,具體定義如下:
// Binding event handler with date picker. ko.bindingHandlers.datepicker = { init: function (element, valueAccessor, allBindingsAccessor) { // initialize datepicker with some optional options var options = allBindingsAccessor().datepickerOptions || {}; $(element).datepicker(options); // when a user changes the date, update the view model ko.utils.registerEventHandler(element, "changeDate", function (event) { var value = valueAccessor(); // Determine if an object property is ko.observable if (ko.isObservable(value)) { value(event.date); } }); }, update: function (element, valueAccessor) { var widget = $(element).data("datepicker"); //when the view model is updated, update the widget if (widget) { widget.date = ko.utils.unwrapObservable(valueAccessor()); widget.setValue(); } } };
上面,我們定義了日歷控件的事件處理方法,當用戶選擇日歷中的日期時,我們獲取當前選擇的日期綁定到界面上,具體定義如下:
<!-- Selected time control --> <input id="selectStartDate" data-bind="datepicker: Starts" type="text" class="span12" />
上面,我們在Html元素中綁定了datepicker事件處理方法並且把Starts值顯示到input元素中。
圖4 日歷控件
接下來,我們定義Time picker事件處理方法,當用戶時間時獲取當前選擇的時間綁定到界面上,具體定義如下:
// Binding event handler with time picker. ko.bindingHandlers.timepicker = { init: function (element, valueAccessor, allBindingsAccessor) { //initialize timepicker var options = $(element).timepicker(); //when a user changes the date, update the view model ko.utils.registerEventHandler(element, "changeTime.timepicker", function (event) { var value = valueAccessor(); if (ko.isObservable(value)) { value(event.time.value); } }); }, update: function (element, valueAccessor) { var widget = $(element).data("timepicker"); //when the view model is updated, update the widget if (widget) { var time = ko.utils.unwrapObservable(valueAccessor()); widget.setTime(time); } } };
同樣,我們把時間值綁定頁面元素中,具體定義如下:
<!-- Time picker value--> <input id="selectStartTime" data-bind="timepicker: StartTime" class="span8" type="text" />
現在,我們已經實現獲取用戶的輸入,接下來需要把用戶輸入的任務信息數據保存到數據庫中,那么我們將通過$.ajax()方法調用API接口,首先我們在day-calendar.js文件中定義類型TaskDetails,具體定義如下:
// TaskDetails type. var TaskDetails = function (date) { var $this = this; $this.Id = ko.observable(); $this.ParentTask = ko.observable(); $this.Title = ko.observable("New Task"); $this.Details = ko.observable(); $this.Starts = ko.observable(new Date(new Date(date).setMinutes(0))); $this.Ends = ko.observable(new Date(new Date(date).setMinutes(59))); // Gets start time when starts changed. $this.StartTime = ko.computed({ read: function () { return $this.Starts().toLocaleTimeString("en-US"); }, write: function (value) { if (value) { var dt = new Date($this.Starts().toDateString() + " " + value); $this.Starts(new Date($this.Starts().getFullYear(), $this.Starts().getMonth(), $this.Starts().getDate(), dt.getHours(), dt.getMinutes())); } } }); // Gets end time when ends changed. $this.EndTime = ko.computed({ read: function () { return $this.Ends().toLocaleTimeString("en-US"); }, write: function (value) { if (value) { var dt = new Date($this.Ends().toDateString() + " " + value); $this.Ends(new Date($this.Ends().getFullYear(), $this.Ends().getMonth(), $this.Ends().getDate(), dt.getHours(), dt.getMinutes())); } } }); $this.btnVisibility = ko.computed(function () { if ($this.Id() > 0) { return "visible"; } else { return "hidden"; } }); $this.Save = function (data) { // http://knockoutjs.com/documentation/plugins-mapping.html var taskDetails = ko.mapping.toJS(data); taskDetails.Starts = taskDetails.Starts.toDateString(); taskDetails.Ends = taskDetails.Ends.toDateString(); $.ajax({ url: "/api/Calendar/SaveTask", type: "POST", contentType: "text/json", data: JSON.stringify(taskDetails) }).done(function () { $("#currentTaskModal").modal("toggle"); vm.getTaskDetails(vm.selectedDate()); }).error(function () { alert("Failed to Save Task"); }); }; $this.Delete = function (data) { $.ajax({ url: "/api/Calendar/" + data.Id(), type: "DELETE", }).done(function () { $("#currentTaskModal").modal("toggle"); vm.getTaskDetails(vm.selectedDate()); }).error(function () { alert("Failed to Delete Task"); }); }; $this.Cancel = function (data) { $("#currentTaskModal").modal("toggle"); }; };
我們在TaskDetails類型中定義方法Save()和Delete(),我們看到Save()方法通過$.ajax()調用接口“/api/Calendar/SaveTask” 保存數據,這里要注意的是我們把TaskDetails對象序列化成JSON格式數據,然后調用SaveTask()保存數據。
現在,我們已經實現了頁面綁定用戶的輸入,然后由Knockout JS訪問Web API接口,對數據庫進行操作;接着需要對程序界面進行調整,我們在項目中添加bootstrap-responsive.css和bootstrap.css文件引用,接下來,我們在的BundleConfig中指定需要加載的Javascript和CSS文件,具體定義如下:
/// <summary> /// Compress JS and CSS file. /// </summary> public class BundleConfig { /// <summary> /// Registers the bundles. /// </summary> /// <param name="bundles">The bundles.</param> public static void RegisterBundles(BundleCollection bundles) { bundles.Add(new ScriptBundle("~/bundles/jquery").Include( "~/Scripts/jquery-{version}.js")); bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( "~/Scripts/bootstrap.js", "~/Scripts/html5shiv.js")); bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( "~/Scripts/jquery.unobtrusive*", "~/Scripts/jquery.validate*")); bundles.Add(new ScriptBundle("~/bundles/knockout").Include( "~/Scripts/knockout-{version}.js")); bundles.Add(new StyleBundle("~/Styles/bootstrap/css").Include( "~/Content/bootstrap-responsive.css", "~/Content/bootstrap.css")); bundles.Add(new ScriptBundle("~/bundles/jquerydate").Include( "~/Scripts/datepicker/bootstrap-datepicker.js", //"~/Scripts/datepicker/locales/bootstrap-datepicker.zh-CN.js", "~/Scripts/timepicker/bootstrap-timepicker.min.js", "~/Scripts/moment.js")); bundles.Add(new ScriptBundle("~/bundles/app").Include( "~/Scripts/app/day-calendar*")); bundles.Add(new StyleBundle("~/Styles/jquerydate").Include( "~/Content/datepicker/datepicker.css", "~/Content/timepicker/bootstrap-timepicker.css")); } }
圖5 任務管理器Demo
1.1.3 總結
我們通過一個任務管理程序的實現介紹了Knockout JS和Web API的結合使用,服務端我們通過Entity Framework對數據進行操作,然后使用API控制器開放接口讓客戶端訪問,然后我們使用Knockout JS的數據綁定和事件處理方法實現了動態的頁面顯示,最后,我們使用了BootStrap CSS美化了一下程序界面。
本文僅僅是介紹了Knockout JS和Web API的結合使用,如果大家想進一步學習,參考如下鏈接。
參考
- https://github.com/dotnetcurry/ko-calendar-dncmag-08
- http://blogs.msdn.com/b/webdev/archive/2013/11/18/announcing-release-of-asp-net-and-web-tools-2013-1-for-visual-studio-2012.aspx
- http://code.tutsplus.com/tutorials/into-the-ring-with-knockout-js--net-21239
- http://code.tutsplus.com/series/into-the-ring-with-knockoutjs--net-22038
- http://www.asp.net/web-api/overview/creating-web-apis/using-web-api-with-entity-framework/using-web-api-with-entity-framework,-part-5