1. 程式人生 > >JS元件系列——BootstrapTable+KnockoutJS實現增刪改查解決方案(三):兩個Viewmodel搞定增刪改查

JS元件系列——BootstrapTable+KnockoutJS實現增刪改查解決方案(三):兩個Viewmodel搞定增刪改查

前言:之前博主分享過knockoutJS和BootstrapTable的一些基礎用法,都是寫基礎應用,根本談不上封裝,僅僅是避免了html控制元件的取值和賦值,遠遠沒有將MVVM的精妙展現出來。最近專案打算正式將ko用起來,於是乎對ko和bootstraptable做了一些封裝,在此分享出來供園友們參考。封裝思路參考部落格園大神蕭秦,如果園友們有更好的方法,歡迎討論。

KnockoutJS系列文章:

一、第一個viewmodel搞定查詢

demo的實現還是延續上次的部門管理功能。以下展開通過資料流向來說明。

1、後臺向View返回viewmodel的實現

        public
ActionResult Index() { var model = new { tableParams = new { url = "/Department/GetDepartment", //pageSize = 2, }, urls = new { delete
= "/Department/Delete", edit = "/Department/Edit", add = "/Department/Edit", }, queryCondition = new { name = "", des = "" } }; return
View(model); }

程式碼釋疑:這裡返回的model包含三個選項

  • tableParams:頁面表格初始化引數。由於js裡面定義了預設引數,所以這裡設定的引數是頁面特定的初始化引數。
  • urls:包含增刪改請求的url路徑。
  • queryCondition:頁面的查詢條件。

2、cshtml頁面程式碼

Index.cshtml頁面程式碼如下:

@{
    Layout = null;
}

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Index</title>

    <link href="~/Content/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-table/bootstrap-table.min.css" rel="stylesheet" />

    <script src="~/scripts/jquery-1.9.1.min.js"></script>
    <script src="~/Content/bootstrap/js/bootstrap.min.js"></script>
    <script src="~/Content/bootstrap-table/bootstrap-table.min.js"></script>
    <script src="~/Content/bootstrap-table/locale/bootstrap-table-zh-CN.js"></script>

    <script src="~/scripts/knockout/knockout-3.4.0.min.js"></script>
    <script src="~/scripts/knockout/extensions/knockout.mapping-latest.js"></script>
    <script src="~/scripts/extensions/knockout.index.js"></script>
    <script src="~/scripts/extensions/knockout.bootstraptable.js"></script><script type="text/javascript">
        $(function(){
            var data = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model));
            ko.bindingViewModel(data, document.getElementById("div_index"));
        });

    </script>
</head>
<body>
    <div id="div_index" class="panel-body" style="padding:0px;overflow-x:hidden;">
        <div class="panel panel-default">
            <div class="panel-heading">查詢條件</div>
            <div class="panel-body">
                <form id="formSearch" class="form-horizontal">
                    <div class="form-group">
                        <label class="control-label col-xs-1">部門名稱</label>
                        <div class="col-xs-3">
                            <input type="text" class="form-control" data-bind="value:queryCondition.name">
                        </div>
                        <label class="control-label col-xs-1">部門描述</label>
                        <div class="col-xs-3">
                            <input type="text" class="form-control" data-bind="value:queryCondition.des">
                        </div>
                        <div class="col-xs-4" style="text-align:right;">
                            <button type="button"data-bind="click:clearClick" class="btn">清空</button>
                            <button type="button"data-bind="click:queryClick" class="btn btn-primary">查詢</button>
                        </div>
                    </div>
                </form>
            </div>
        </div>
        <div id="toolbar" class="btn-group">
            <button data-bind="click:addClick" type="button" class="btn btn-default">
                <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>新增
            </button>
            <button data-bind="click:editClick" type="button" class="btn btn-default">
                <span class="glyphicon glyphicon-pencil" aria-hidden="true"></span>修改
            </button>
            <button data-bind="click:deleteClick" type="button" class="btn btn-default">
                <span class="glyphicon glyphicon-remove" aria-hidden="true"></span>刪除
            </button>
        </div>
        <table data-bind="bootstrapTable:bootstrapTable">
            <thead>
                <tr>
                    <th data-checkbox="true"></th>
                    <th data-field="Name">部門名稱</th>
                    <th data-field="Level">部門級別</th>
                    <th data-field="Des">描述</th>
                    <th data-field="strCreatetime">建立時間</th>
                </tr>
            </thead>
        </table>
    </div>
    
</body>
</html>

程式碼釋疑:和上篇一樣,需要引用JQuery、bootstrap、bootstraptable、knockout等相關檔案。這裡重點說明下兩個檔案:

  • knockout.index.js:封裝了查詢頁面相關的屬性和事件繫結。
  • knockout.bootstraptable.js:封裝了bootstrapTable的初始化和自定義knockout繫結的方法。

以上所有的頁面互動都封裝在了公共js裡面,這樣就不用在頁面上面寫大量的DOM元素取賦值、事件的繫結等重複程式碼,需要在本頁面寫的js只有以上兩句,是不是很easy。

3、JS封裝

重點來看看上面的說的兩個js檔案knockout.bootstraptable.js和knockout.index.js。

(1)knockout.bootstraptable.js

(function ($) {
    //向ko裡面新增一個bootstrapTableViewModel方法
    ko.bootstrapTableViewModel = function (options) {
        var that = this;

        this.default = {
            toolbar: '#toolbar',                //工具按鈕用哪個容器
            queryParams: function (param) {
                return { limit: param.limit, offset: param.offset };
            },//傳遞引數(*)
            pagination: true,                   //是否顯示分頁(*)
            sidePagination: "server",           //分頁方式:client客戶端分頁,server服務端分頁(*)
            pageNumber: 1,                      //初始化載入第一頁,預設第一頁
            pageSize: 10,                       //每頁的記錄行數(*)
            pageList: [10, 25, 50, 100],        //可供選擇的每頁的行數(*)
            method: 'get',
            search: true,                       //是否顯示錶格搜尋,此搜尋是客戶端搜尋,不會進服務端,所以,個人感覺意義不大
            strictSearch: true,
            showColumns: true,                  //是否顯示所有的列
            cache:false,
            showRefresh: true,                  //是否顯示重新整理按鈕
            minimumCountColumns: 2,             //最少允許的列數
            clickToSelect: true,                //是否啟用點選選中行
            showToggle: true,
        };
        this.params = $.extend({}, this.default, options || {});

        //得到選中的記錄
        this.getSelections = function () {
            var arrRes = that.bootstrapTable("getSelections")
            return arrRes;
        };

        //重新整理
        this.refresh = function () {
            that.bootstrapTable("refresh");
        };
        

    };

    //新增ko自定義繫結
    ko.bindingHandlers.bootstrapTable = {
        init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
            //這裡的oParam就是繫結的viewmodel
            var oViewModel = valueAccessor();
            var $ele = $(element).bootstrapTable(oViewModel.params);
            //給viewmodel新增bootstrapTable方法
            oViewModel.bootstrapTable = function () {
                return $ele.bootstrapTable.apply($ele, arguments);
            }
        },

        update: function (element, valueAccessor, allBindingsAccessor, viewModel) {

        }
    };
})(jQuery);

程式碼釋疑:上面程式碼主要做了兩件事

  1. 自定義了bootstrapTable初始化的ViewModel。
  2. 新增ko自定義繫結。

如果園友不理解自定義繫結的使用,可以看看博主的前兩篇博文(一)(二),有詳細介紹。

(2)knockout.index.js

(function ($) {
    ko.bindingViewModel = function (data, bindElement) {

        var self = this;

        this.queryCondition = ko.mapping.fromJS(data.queryCondition);
        this.defaultQueryParams = {
            queryParams: function (param) {
                var params = self.queryCondition;
                params.limit = param.limit;
                params.offset = param.offset;
                return params;
            }
        };

        var tableParams = $.extend({}, this.defaultQueryParams, data.tableParams || {});
        this.bootstrapTable = new ko.bootstrapTableViewModel(tableParams);
        
        //清空事件
        this.clearClick = function () {
            $.each(self.queryCondition, function (key, value) {
                //只有監控屬性才清空
                if (typeof (value) == "function") {
                    this(''); //value('');
                }
            });
            self.bootstrapTable.refresh();
        };

        //查詢事件
        this.queryClick = function () {
            self.bootstrapTable.refresh();
        };

        //新增事件
        this.addClick = function () {
            var dialog = $('<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"></div>');
            dialog.load(data.urls.edit, null, function () { });

            $("body").append(dialog);
            dialog.modal().on('hidden.bs.modal', function () {
                //關閉彈出框的時候清除繫結(這個清空包括清空繫結和清空註冊事件)
                ko.cleanNode(document.getElementById("formEdit"));
                dialog.remove();
                self.bootstrapTable.refresh();
            });
        };

        //編輯事件
        this.editClick = function () {
            var arrselectedData = self.bootstrapTable.getSelections();
            if (arrselectedData.length <= 0 || arrselectedData.length > 1) {
                alert("每次只能編輯一行");
                return;
            }
            var dialog = $('<div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"></div>');
            dialog.load(data.urls.edit, arrselectedData[0], function () { });

            $("body").append(dialog);
            dialog.modal().on('hidden.bs.modal', function () {
                //關閉彈出框的時候清除繫結(這個清空包括清空繫結和清空註冊事件)
                ko.cleanNode(document.getElementById("formEdit"));
                dialog.remove();
                self.bootstrapTable.refresh();
            });
        };

        //刪除事件
        this.deleteClick = function () {
            var arrselectedData = self.bootstrapTable.getSelections();
            if (!arrselectedData||arrselectedData.length<=0) {
                alert("請至少選擇一行");
                return;
            }
            $.ajax({
                url: data.urls.delete,
                type: "post",
                contentType: 'application/json',
                data: JSON.stringify(arrselectedData),
                success: function (data, status) {
                    alert(status);
                    self.bootstrapTable.refresh();
                }
            });
        };

        ko.applyBindings(self, bindElement);
    };
})(jQuery);

程式碼釋疑:這個js主要封裝了頁面元素的屬性和事件繫結,需要說明的幾個地方

  • this.queryCondition = ko.mapping.fromJS(data.queryCondition):這一句的作用是將後臺傳過來的查詢條件,從JSON資料轉換成監控屬性。只有執行了這一句,屬性和頁面元素才能雙向監控。
  • self.bootstrapTable.refresh():這一句的含義是重新整理表格資料,它實際上是呼叫的bootstrapTable的refresh方法,只不過博主在knockout.bootstraptable.js檔案裡面對它進行了簡單封裝。
  • dialog.load(data.urls.edit, null, function () { }):在新增和編輯的時候使用了jQuery的load()方法,這個方法的作用是請求這個url的頁面元素,並執行url對應頁面的js程式碼。此方法在動態引用js檔案並執行js檔案裡面程式碼這方面功能很強大。

最後附上後臺GetDepartment()方法對應的程式碼

        [HttpGet]
        public JsonResult GetDepartment(int limit, int offset, string name, string des)
        {
            var lstRes = DepartmentModel.GetData();
            if (!string.IsNullOrEmpty(name))
            {
                lstRes = lstRes.Where(x => x.Name.Contains(name)).ToList();
            }
            if (!string.IsNullOrEmpty(des))
            {
                lstRes = lstRes.Where(x => x.Des.Contains(des)).ToList();
            }
            lstRes.ForEach(x=> {
                x.strCreatetime = x.Createtime.ToString("yyyy-MM-dd HH:mm:ss");
            });
            var oRes = new
            {
                rows = lstRes.Skip(offset).Take(limit).ToList(),
                total = lstRes.Count
            };
            return Json(oRes, JsonRequestBehavior.AllowGet);
        }

至此,查詢頁面的查詢、清空功能即可實現。

 

你是否還有一個疑問:如果我們需要自定義bootstrapTable的事件怎麼辦?不能通過後臺的viewmodel傳過來吧?

確實,從後臺是無法傳遞js事件方法的,所以需要我們在前端自定義事件的處理方法,比如我們可以這樣:

<script type="text/javascript">
        $(function(){
            var data = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model));
            data.tableParams.onLoadSuccess = function(data){
          alert("載入成功事件"
);
       }; ko.bindingViewModel(data, document.getElementById(
"div_index")); }); </script>

二、第二個viewmodel搞定編輯

上面的一個viewmodel搞定了查詢和刪除的功能,但是新增和編輯還需要另一個viewmodel的支援。下面來看看編輯的封裝實現。

1、ActionResult的實現

通過上面查詢的程式碼我們可以知道,當用戶點選新增和編輯的時候,會請求另一個View檢視→/Department/Edit。下面來看看Edit檢視的實現

    public ActionResult Edit(Department model)
        {
            var oResModel = new
            {
                editModel = model,
                urls = new
                {
                    submit = model.id == 0 ? "/Department/Add" : "/Department/Update"
                }
            };
            return View(oResModel);
        }

程式碼釋疑:上述程式碼很簡單,就是向檢視頁面返回一個viewmodel,包含編輯的實體和提交的url。通過這個實體主鍵是否存在來判斷當前提交是新增實體還是編輯實體。

2、cshtml程式碼

Edit.cshtml程式碼如下:

<form id="formEdit">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="myModalLabel">操作</h4>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label for="txt_departmentname">部門名稱</label>
                    <input type="text" name="txt_departmentname" data-bind="value:editModel.Name" class="form-control" placeholder="部門名稱">
                </div>
                <div class="form-group">
                    <label for="txt_departmentlevel">部門級別</label>
                    <input type="text" name="txt_departmentlevel" data-bind="value:editModel.Level" class="form-control" placeholder="部門級別">
                </div>
                <div class="form-group">
                    <label for="txt_des">描述</label>
                    <input type="text" name="txt_des" data-bind="value:editModel.Des" class="form-control" placeholder="描述">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove" aria-hidden="true"></span>關閉</button>
                <button type="submit" class="btn btn-primary"><span class="glyphicon glyphicon-floppy-disk" aria-hidden="true"></span>儲存</button>
            </div>
        </div>
    </div>
</form>
<link href="~/Content/bootstrapValidator/css/bootstrapValidator.css" rel="stylesheet" />
<script src="~/Content/bootstrapValidator/js/bootstrapValidator.js"></script>
<script src="~/scripts/extensions/knockout.edit.js"></script>
<script type="text/javascript">
    $(function () {
     var editData = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model));
        ko.bindingEditViewModel(editData, {});        
    });
</script>

程式碼釋疑:由於我們加了驗證元件bootstrapValidator,所以需要引用相關js和css。knockout.edit.js這個檔案主要封裝了編輯頁面的屬性和事件繫結。重點來看看這個js的實現程式碼。

3、js封裝

knockout.edit.js程式碼:

(function ($) {
    ko.bindingEditViewModel = function (data, validatorFields) {

        var that = {};

        that.editModel = ko.mapping.fromJS(data.editModel);

        that.default = {
            message: '驗證不通過',
            fields: { },
            submitHandler: function (validator, form, submitButton) {
                var arrselectedData = ko.toJS(that.editModel);
                $.ajax({
                    url: data.urls.submit,
                    type: "post",
                    contentType: 'application/json',
                    data: JSON.stringify(arrselectedData),
                    success: function (data, status) {
                        alert(status);
                    }
                });
                $("#myModal").modal("hide");
            }
        };
        that.params = $.extend({}, that.default, {fields: validatorFields} || {});
        $('#formEdit').bootstrapValidator(that.params);
        ko.applyBindings(that, document.getElementById("formEdit"));
    };

})(jQuery);

程式碼釋疑:這個js主要封裝了編輯模型的屬性和提交的事件繫結。由於用到了bootstrapValidator驗證元件,所以需要表單提交。其實公共js裡面是不應該出現頁面id的,比如上面的“formEdit”和“myModal”,可以將此作為引數傳過來,這點有待優化。引數validatorFields表示驗證元件的驗證欄位,如果表單不需要驗證,則傳一個空的Json或者不傳都行。上文我們沒有做欄位驗證,其實一般來說,基礎表都會有一個或者幾個非空欄位,比如我們可以加上部門名稱的非空驗證。在Edit.cshtml頁面的程式碼改成這樣:

<form id="formEdit">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="myModalLabel">操作</h4>
            </div>
            <div class="modal-body">
                <div class="form-group">
                    <label for="txt_departmentname">部門名稱</label>
                    <input type="text" name="Name" data-bind="value:editModel.Name" class="form-control" placeholder="部門名稱">
                </div>
                <div class="form-group">
                    <label for="txt_departmentlevel">部門級別</label>
                    <input type="text" name="Level" data-bind="value:editModel.Level" class="form-control" placeholder="部門級別">
                </div>
                <div class="form-group">
                    <label for="txt_des">描述</label>
                    <input type="text" name="Des"<