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,那么上述封装还是能起到一定简化代码的作用。还是那句话,你的需求决定了你的封装思路。如果你觉得本文能够帮到你,可以打赏博主,也可以推荐下,博主一定继续努力!

欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利

posted @ 2016-07-14 17:37  懒得安分  阅读(10284)  评论(23编辑  收藏  举报