http://www.cnblogs.com/xqin/archive/2013/06/06/3120887.html
前言
我写代码喜欢提取一些共通的东西出来,之前的一篇博客中说了如何用一个共通的viewModel和简洁的后台代码做查询页面,所有的查询页面都要对应一个数据录入的编辑及查看明细的页面,那么今天我们就来实现这个页面,同样我们也要使用一个共通的viewModel完成前台UI与JSON数据交互的处理,同样以超简洁的后台代码来处理保存。
需求分析
我们先弄明白我们要做怎么样一个编辑的页面。
1、最上面有一个共通的工具栏,有保存、撤消、审核、打印、还有上一条、下一条、第一条、最后一条的数据滚动按钮,还有一些其它按钮放在下拉按钮中。2、我们这个页面支持一个主表和从表一起保存,同一个事务,首先要有主表的录入
3、其次我们还要从表的录入grid,从表可以增删改,我们新增设计成从库中选择添加,当然也很容易实现直接新增一行。
4、然后我们可能主表中有些字段不常用,我们放在第二个tab页签中,如果还有从表还可以再增加页签
5、还有一个需求就是,我保存时,只保存改动过的东西,比如主表只改了合同名称,从表就修改了一行,那么我们处理应该要主表只更新一个字段,从表中只修改一条数据。如果没有值被修改时,保存按钮不响应。
技术实现
前端要实现
1、页面布局 2、绑定控件 3、UI与JSON数据交互的viewModel后台web api要实现
1、主表(增、改)及从表(增、删、改)在一个事务中保存好我们还是在我们的mms区域中做示例,还是选择一个跟我上一篇一样的[材料接收的业务]
上一篇中我们已经创建了材料接收的控件器RecieveController.cs,其中已经写了查询的页面Index及查询的api Get方法,现在我们先添加编辑的页面。
在mvc controller中添加Edit Actioinusing System;using System.Web.Mvc;using Zephyr.Core;using Zephyr.Models;using Zephyr.Web.Areas.Mms.Common;namespace Zephyr.Areas.Mms.Controllers{ public class ReceiveController : Controller { //查询页面 public ActionResult Index() { ... } //编辑页面 public ActionResult Edit(string id) { return View(); } }}
然后我们右击这个action,添加一个对应的view页面~/Views/Receive/Edit.cshtml
@{ ViewBag.Title = "Edit"; Layout = "~/Views/Shared/_Layout.cshtml";}@section scripts{ }单据编号单据日期经办人供应商库房收料日期供应类型付款方式合同名称原始票号金额备注
材料编码 材料名称 规格型号 材质 单位 验收数量 入库数量 入库单价 金额 备注 审批状态审批意见审批人审批日期编制日期编制人修改日期修改人
代码贴出来有换行变得很不整齐,没办法了,大家将就看吧。上面这估代码我还是要解释下,同样data-bind是knouckout的写法,easyui-xxx及data-optionis是easyui的写法。还有三段脚本,第一个是引入项目共通脚本,第二个是引入编辑页面共通的viewModel,第三个是继承共通的viewModel再加上本页面中的一些计算或验证之类并绑定viewModel到页面上。
我们再看看我们的编辑的共通viewModel,这段代码同样也就100行左右
/*** 模块名:mms viewModel* 程序名: mms.viewModel.edit.js* Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] **/var mms = mms || {};mms.viewModel = mms.viewModel || {}; mms.viewModel.edit = function (data) { var self = this; this.dataSource = data.dataSource; //下拉框的数据源 this.urls = data.urls; //api服务地址 this.resx = data.resx; //中文资源 this.scrollKeys = ko.mapping.fromJS(data.scrollKeys); //数据滚动按钮(上一条下一条) this.form = ko.mapping.fromJS(data.form||data.defaultForm); //表单数据 this.setting = data.setting; this.defaultRow = data.defaultRow; //默认grid行的值 this.defaultForm = data.defaultForm; //主表的默认值 this.grid = { size: { w: 6, h: 177 }, pagination: false, remoteSort: false, url: ko.observable(self.urls.getdetail + self.scrollKeys.current()) }; this.gridEdit = new com.editGridViewModel(self.grid); this.grid.onDblClickRow = self.gridEdit.begin; this.grid.onClickRow = self.gridEdit.ended; this.grid.toolbar = [{ text: '选择在库材料', iconCls: 'icon-search', handler: function () { mms.com.selectMaterial(self, { _xml: 'mms.material_dict' }); } }, '-', { text: '删除材料', iconCls: 'icon-remove', handler: self.gridEdit.deleterow }]; this.rejectClick = function () { ko.mapping.fromJS(data.form, {}, self.form); self.gridEdit.reject(); com.message('success', self.resx.rejected); }; this.firstClick = function () { self.scrollTo(self.scrollKeys.first()); }; this.previousClick = function () { self.scrollTo(self.scrollKeys.previous()); }; this.nextClick = function () { self.scrollTo(self.scrollKeys.next()); }; this.lastClick = function () { self.scrollTo(self.scrollKeys.last()); }; this.scrollTo = function (id) { if (id == self.scrollKeys.current()) return; com.setLocationHashId(id); com.ajax({ type: 'GET', url: self.urls.getmaster + id, success: function (d) { ko.mapping.fromJS(d, {}, self); ko.mapping.fromJS(d, {}, data); } }); self.grid.url(self.urls.getdetail + id); self.grid.datagrid('loaded'); }; this.saveClick = function () { self.gridEdit.ended(); //结束grid编辑状态 var post = { //传递到后台的数据 form: com.formChanges(self.form, data.form, self.setting.postFormKeys), list: self.gridEdit.getChanges(self.setting.postListFields) }; if ((self.gridEdit.ended() && com.formValidate()) && (post.form._changed || post.list._changed)) { com.ajax({ url: self.urls.edit, data: ko.toJSON(post), success: function (d) { com.message('success', self.resx.editSuccess); ko.mapping.fromJS(post.form, {}, data.form); //更新旧值 self.gridEdit.accept(); } }); } }; this.auditClick = function () { var updateArray = ['ApproveState', 'ApproveRemark']; mms.com.auditDialog(this.form, function (d) { com.ajax({ type: 'POST', url: self.urls.audit + self.scrollKeys.current(), data: JSON.stringify(d), success: function () { com.message('success', d.status == "passed" ? self.resx.auditPassed : self.resx.auditReject); if (data.form) for (var i in updateArray) data.form[updateArray[i]] = self.form[updateArray[i]](); } }); }); }; this.approveButton = { iconCls: ko.computed(function () { return self.form.ApproveState() == "passed" ? "icon-user-reject" : "icon-user-accept"; }), text: ko.computed(function () { return self.form.ApproveState() == "passed" ? "取消审核" : "审核"; }) }; this.printClick = function () { com.openTab('打印报表', '/report?p1=0002&p2=2012-1-1', 'icon-printer_color'); };};
这段代码利用了很多的ko的mapping组件去更新数据对象。需要了解下kouckoujs才比较好理解。
这个viewModel中上面也定义了很多变量,我基本都有注释,接下来this.grid是我赋给明细表格的属性,除了这些,我data-bind=”datagrid:grid”绑定时还有给它默认的属性。这里面对明细grid的增删改操作的对象this.gridEdit = new com.editGridViewModel(self.grid); 利用到我的另一个共通的grid编辑的viewModel。这里面已经实现了对easyui datagrid的操作,我这里了给大家共享下com.editGridViewModel = function (grid) { var self = this; this.begin = function (index, row) { if (index== undefined || typeof index === 'object') { row = grid.datagrid('getSelected'); index = grid.datagrid('getRowIndex', row); } self.editIndex = self.ended() ? index : self.editIndex; grid.datagrid('selectRow', self.editIndex).datagrid('beginEdit', self.editIndex); }; this.ended = function () { if (self.editIndex == undefined) return true; if (grid.datagrid('validateRow', self.editIndex)) { grid.datagrid('endEdit', self.editIndex); self.editIndex = undefined; return true; } grid.datagrid('selectRow', self.editIndex); return false; }; this.addnew = function (rowData) { if (self.ended()) { if (Object.prototype.toString.call(rowData) != '[object Object]') rowData = {}; rowData = $.extend({_isnew:true},rowData); grid.datagrid('appendRow', rowData); self.editIndex = grid.datagrid('getRows').length - 1; grid.datagrid('selectRow', self.editIndex); self.begin(self.editIndex, rowData); } }; this.deleterow = function () { var selectRow = grid.datagrid('getSelected'); if (selectRow) { var selectIndex = grid.datagrid('getRowIndex', selectRow); if (selectIndex == self.editIndex) { grid.datagrid('cancelEdit', self.editIndex); self.editIndex = undefined; } grid.datagrid('deleteRow', selectIndex); } }; this.reject = function () { grid.datagrid('rejectChanges'); }; this.accept = function () { grid.datagrid('acceptChanges'); var rows = grid.datagrid('getRows'); for (var i in rows) delete rows[i]._isnew; }; this.getChanges = function (include, ignore) { if (!include) include = [], ignore = true; var deleted = utils.filterProperties(grid.datagrid('getChanges', "deleted"), include, ignore), updated = utils.filterProperties(grid.datagrid('getChanges', "updated"), include, ignore), inserted = utils.filterProperties(grid.datagrid('getChanges', "inserted"), include, ignore); var changes = { deleted: deleted, inserted: utils.minusArray(inserted, deleted), updated: utils.minusArray(updated, deleted) }; changes._changed = (changes.deleted.length + changes.updated.length + changes.inserted.length)>0; return changes; }; this.isChangedAndValid = function () { return self.ended() && self.getChanges()._changed; };};
grid的编辑实现之后接下来就是一些按钮的实现。这里主要说一下保存按钮
this.saveClick = function () { … } 这里面第一句话就是调用grid编辑的对象去结束行编辑,然后取得传到后台的数据,post={form:xxx,list:xxxx},form是指主表的数据,而且过滤掉了未改变的字段,而list是指我明细grid的编辑的数据结果,可以看com.editGridViewModel中的getChanges的方法,它的结构应该是list={deleted:xxx,inserted:xxx,updated:xxx}; 然后我们还要判断主表输入验证是否通过,grid的输入验证是否通过,及它们是否有修改,满足条件才去ajax请求保存数据。前端就说到这里,那么我们的viewModel还需要一些参数,我们还是从后台mvc controller中返回。回过头再编辑Edit的action方法,传递我们viewModel中需要的参数
public ActionResult Edit(string id){ var userName = MmsHelper.GetUserName(); var currentProject = MmsHelper.GetCurrentProject(); var data = new ReceiveApiController().GetEditMaster(id); var codeService = new sys_codeService(); var model = new { form = data.form, scrollKeys = data.scrollKeys, urls = new { getdetail = "/api/mms/receive/getdetail/", //获取明细数据api
getmaster = "/api/mms/receive/geteditmaster/", //获取主表数据及数据滚动数据api edit = "/api/mms/receive/edit/", //数据保存api audit = "/api/mms/receive/audit/", //审核api getrowid = "/api/mms/receive/getnewrowid/" //获取新的明细数据的主键(日语叫采番) }, resx = new { rejected = "已撤消修改!", editSuccess = "保存成功!", auditPassed ="单据已通过审核!", auditReject = "单据已取消审核!" }, dataSource = new{ measureUnit = codeService.GetMeasureUnitListByType(), supplyType = codeService.GetValueTextListByType("SupplyType"), payKinds = codeService.GetValueTextListByType("PayType"), warehouseItems = new mms_warehouseService().GetWarehouseItems(currentProject) }, defaultForm = new mms_receive().Extend(new { //定义主表数据的默认值 BillNo = id, BillDate = DateTime.Now, DoPerson = userName, ReceiveDate = DateTime.Now, SupplyType = codeService.GetDefaultCode("SupplyType"), PayKind = codeService.GetDefaultCode("PayType"), }), defaultRow = new { //定义从表数据的默认值 CheckNum = 1, Num = 1, UnitPrice = 0, Money = 0, PrePay = 0 }, setting = new { postFormKeys = new string[] { "BillNo" }, //主表的主键 postListFields = new string[] { "BillNo", "RowId", //定义从表中哪些字段要传递到后台 "MaterialCode", "Unit", "CheckNum", "Num", "UnitPrice", "PrePay", "Money", "Remark" } } }; return View(model);}
上面定义的这些数据,就是我们共通的viewModel中需要的数据,根据这些数据我们的viewModel就能创建出不同的实例了。
接下来我们就开始实现Web Api服务,包括出现的urls当然的查询主表单条数据,查询明细数据,保存等。我们在ReceiveApiController中添加以下方法:public class ReceiveApiController : ApiController { // GET api/mms/send/geteditmaster 取得编辑页面中的主表数据及上一页下一页主键 public dynamic GetEditMaster(string id) { var projectCode = MmsHelper.GetCookies("CurrentProject"); var masterService = new mms_receiveService(); return new{ form = masterService.GetModel(ParamQuery.Instance().AndWhere("BillNo", id)), scrollKeys = masterService.ScrollKeys("BillNo", id, ParamQuery.Instance().AndWhere("ProjectCode", projectCode)) }; } // 地址:GET api/mms/send/getnewrowid 预取得新的明细表的行号 public string GetNewRowId(int id) { var service = new mms_receiveDetailService(); return service.GetNewKey("RowId", "maxplus",id); } // 地址:GET api/mms/send/getdetail 功能:取得收料单明细信息 public dynamic GetDetail(string id) { var query = RequestWrapper .InstanceFromRequest() .SetRequestData("BillNo",id) .LoadSettingXmlString(@""); var pQuery = query.ToParamQuery(); var ReceiveService = new mms_receiveService(); var result = ReceiveService.GetDynamicListWithPaging(pQuery); return result; } // 地址:POST api/mms/send 功能:保存收料单数据 [System.Web.Http.HttpPost] public void Edit(dynamic data) { var formWrapper = RequestWrapper.Instance().LoadSettingXmlString(@" mms_receiveDetail A left join mms_materialInfo B on B.MaterialCode = A.MaterialCode "); var listWrapper = RequestWrapper.Instance().LoadSettingXmlString(@" mms_receive
"); var service = new mms_receiveService(); var result = service.Edit(formWrapper, listWrapper, data); } } mms_receiveDetail
同样,这段代码也是利用我的框架中的RequestWrapper写出来的,我这里就不再解释RequestWrapper这个对象了,在我的上一篇博客中有解释过:
一个共通的viewModel搞定所有的分页查询一览及数据导出:我这里只说一下最后一个保存的方法:
传递到后台的data应该是这种结构 data={form:{a:’’,b:’’,…},list:{deleted: [{…},{…},…],inserted: [{},{},…],updated: [{},{},…]}}; 我再定义两个RequestWrapper对象配置这个保存操作,告诉框架我这些数据应该如果去保存。然后把这个配置信息及我的数据传给框架的共通方法去处理。这样,上面的Edit方法就简单的完成了这个主从表一起保存的复杂的处理。这样,我们的一个复杂的查看、编辑页面就完成了。可以跟上一个查询的页面连接在一起。在查询页面双击或编辑时会打开编辑页面的新的tab页。
效果展示
我们来看看这个页面:
从查询页面双击一行数据进入到这个页面数据验证
选择在库存材料
我们在主表中修改几个字段,从表中新增一条,删除一条,修改一条
点击保存
测试数据滚动 上一条 下一条 第一条 最后一条都没问题 就不截图了。
打印报表,我这里直接写死了一个测试报表在viewModel中,可以写成从后台传过来的
这个报表很强大,可以直接在线编辑,点击设计按钮进入
还有一些不常用的其它按钮我放在其它下拉按钮中。
审核按钮在查询与编辑页面中都有,审核之后,审核按钮变成取消审核按钮后述
和上一篇一样,我们有了共通的edit viewModel之后,就可以非常简洁的代码完成一个比较复杂的数据编辑页面了,这个页面是我参照ERP系统中的编辑页面完成的。开发一个新的编辑页面我们要做的只有:
1、前台页面布局修改一下 2、可以继承共通的viewModel,然后可能还要可以加上一些验证及页面中前台的一些处理。如果没有特殊的东西可以直接用共通的viewModel即可。 3、后台对应一个获取主表及数据滚动的数据、获取从表数据、及保存的Web Api服务,利用框架都只有几行代码,很简单。 这样就完成了。这个编辑页面和我之前写的查询页面都属性于典型的业务页面,基本上可以搞定企业信息化中的60%以上的页面了。写完这两篇,大家对我的框架应该有一定了解了,接下来准备写一些框架中的实现或者系统管理模块。因为在这种开发模式下前台后都有一定的工作量,我可能会分前台后台分开来写,谢谢大家的支持!