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


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

KnockoutJS系列文章:

本文原創地址:http://www.cnblogs.com/landeanfen/p/5656307.html

一、第一個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" 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, {
            Name: {
                validators: {
                    notEmpty: {
                        message: '名稱不能為空!'
                    }
                }
            }
        });        
    });
    
</script>

那么就會在提交的時候自動進行驗證:

注意:驗證屬性Name對應的是input標簽的name屬性,所以要做驗證,這個name屬性必須設置正確。

最好附上增刪改的后台方法:

        [HttpPost]
        public JsonResult Add(Department oData)
        {
            DepartmentModel.Add(oData);
            return Json(new { }, JsonRequestBehavior.AllowGet);
        }


        [HttpPost]
        public JsonResult Update(Department oData)
        {
            DepartmentModel.Update(oData);
            return Json(new { }, JsonRequestBehavior.AllowGet);
        }


        [HttpPost]
        public JsonResult Delete(List<Department> oData)
        {
            DepartmentModel.Delete(oData);
            return Json(new { }, JsonRequestBehavior.AllowGet);
        }

至此,我們整個頁面的增刪改查效果就OK了,簡單看下效果:

 

三、總結

以上簡單封裝了bootstrapTable+ko的增刪改查業務,只是一個最初級的封裝。如果你需要將這些運用都你的項目中,可能還需要一些簡單的優化措施,比如:

1、如果單純是一個頁面的viewmodel,是否可以不用從后台的ActionResult里面返回,直接寫在View頁面里面感覺更好,省去了序列化和參數傳遞的問題。這點有待優化。

2、公共js里面不應該出現頁面元素的id,可以通過參數將頁面元素傳遞進來。

3、新增和編輯事件方法里面彈出框的部分有很多重復代碼,這部分的最好做法是將彈出框封裝成一個單獨的組件去調用,可以減少大部分的js代碼。

4、如果查詢條件以及編輯的屬性里面存在select下拉框元素,可能還需要封裝下拉框的datasourse等屬性,這一部分是非常常見的,等博主整理好demo后將這塊加進去。

 可能你要說,對於單表的增刪改查,實際項目中能有多少。確實,項目中大部分的業務是跨表的,但是,如果根據你的業務去建立對應的viewmodel,那么上述封裝還是能起到一定簡化代碼的作用。還是那句話,你的需求決定了你的封裝思路。如果你覺得本文能夠幫到你,可以打賞博主,也可以推薦下,博主一定繼續努力!

歡迎各位轉載,但是未經作者本人同意,轉載文章之后必須在文章頁面明顯位置給出作者和原文連接,否則保留追究法律責任的權利


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM