Knockout JS實現任務管理應用程序

1.1.1 摘要

在博文《Ember.js實現單頁面應用程序》中,咱們介紹了使用Ember JS實現一個單頁應用程序 (SPA),這使我想起了幾年前寫過一個任務管理程序,經過選擇日期,而後編輯時間來增長任務信息。css

當時,咱們是使用ASP.NET和jQuery實現了任務管理程序的,經過ajax調用ASP.NET的Webservice方法來訪問數據庫。html

今天,咱們將經過任務管理程序的實現,來介紹使用ASP.NET Web API和Knockout JS的結合使用,想必許多人都有使用過任務管理程序,其中我以爲Google日曆是一個不錯的任務管理器html5

taskcalendar1

圖1 Google日曆jquery

目錄

1.1.2 正文

經過圖1Google日曆,咱們發現它使用一個Date Picker,讓用戶選擇編輯的日期、還有一個24小時的表格,當用戶點擊表上的一個時間區域就顯示一個彈出式窗口,讓用戶編輯任務的內容,如今大概瞭解了基本的界面設計了,接下來咱們將經過ASP.NET Web API做爲服務端,開放API讓Knockout JS調用接口獲取數據。git

建立ASP.NET MVC 項目

首先,咱們在VS2012中建立一個ASP.NET MVC 4 Web項目。github

而後,咱們打開Package Manager Console,添加Package引用,要使用的庫以下:web

  • 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

taskcalendar2

圖2 ASP.NET MVC 4 Web Applicationajax

建立數據表

接着,咱們在數據庫中添加表TaskDays和TaskDetails,TaskDays保存全部任務的日期,那麼一個日期只有一行記錄保存該表中,它包含了Id(自增)和Day字段,TaskDetails保存不一樣時間短任務信息,它包含Id(自增)、Title、Details、Starts、Ends和ParentTaskId等字段,其中ParentTaskId保存任務的日期的Id值。數據庫

 taskcalendar4

 taskcalendar5

 taskcalendar3

圖3 表TaskDays和TaskDetailsjson

數據傳輸對象

前面,咱們已經定義了數據表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元素中。

taskcalendar6

圖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"));
        }
    }

 

taskcalendar7

圖5 任務管理器Demo

 

1.1.3 總結

咱們經過一個任務管理程序的實現介紹了Knockout JS和Web API的結合使用,服務端咱們經過Entity Framework對數據進行操做,而後使用API控制器開放接口讓客戶端訪問,而後咱們使用Knockout JS的數據綁定和事件處理方法實現了動態的頁面顯示,最後,咱們使用了BootStrap CSS美化了一下程序界面。

本文僅僅是介紹了Knockout JS和Web API的結合使用,若是你們想進一步學習,參考以下連接。

參考

相關文章
相關標籤/搜索