前言
大家看標題就明白了我想寫什么了,在做企業信息化系統中可能大家寫的最多的一種頁面就是查詢頁面了。其實每個查詢頁面,除了條件不太一樣,數據不太一樣,其它的其實都差不多。所以我就想提取一些共通的東西出來,再寫查詢時只要引入我共通的東西,再加上極少的代碼就能完成。我個人比較崇尚代碼簡潔干凈,有不合理的地方歡迎大家指出。
這篇文章主要介紹兩個重點:1、前台viewModel的實現。2、后台服務端如何簡潔的處理查詢請求。
需求分析
查詢頁面要有哪些功能呢
1、有條件部輸入查詢條件(這個不打算做成共通的,因為共通的查詢拼接條件那種都很不好用)
2、在條件的右邊放置:a查詢按鈕:根據條件查詢數據,b清空按鈕:清空條件並查詢
4、因為這是個一覽頁面、還可能有新增數據、修改數據的跳轉按鈕,以及刪除數據、審核數據、及導出數據的功能,所以還要一個工具欄放置這些按鈕
技術實現
前端要實現的就是
1、頁面布局
2、綁定每個輸入控件及數據列表控件
3、每個按鈕操作對應的腳本
后台web api要實現的包括
1、根據前台傳過來的每個字段的值定義為什么查詢(equal、like、greater…這個本來我是放在前台定義的,考慮到安全性問題,移到后台),值為空是否要不加為條件。
2、根據這些定義及分頁信息、排序信息查詢我需要的數據。
3、返回數據到前台
我的項目代碼都在對應的區域中,比如我們在Mms項目加一個查詢
首先這對應一個頁面,所以在Controller中創建一個空的mvc controller,取名RecieveController.cs
using 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() { return View(); } }
然后在Views創建一個Receive文件夾,添加一個Index.cshtml的Razor頁面
@{ ViewBag.Title = "title"; Layout = "~/Views/Shared/_Layout.cshtml"; } @section scripts{ <script src="~/Areas/Mms/ViewModels/mms.com.js"></script> <script src="~/Areas/Mms/ViewModels/mms.viewModel.search.js"></script> <script type="text/javascript"> var data = @Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model)); var viewModel = mms.viewModel.search; ko.bindingViewModel(new viewModel(data)); </script> } <div class="z-toolbar"> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-arrow_refresh" title="刷新" data-bind="click:refreshClick">刷新</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-add" title="新增" data-bind="click:addClick" >新增</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-edit" title="編輯" data-bind="click:editClick" >編輯</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-cross" title="刪除" data-bind="click:deleteClick" >刪除</a> <a href="#" plain="true" class="easyui-linkbutton" icon="icon-user-accept" title="審核" data-bind="click:auditClick" >審核</a> <a href="#" plain="true" class="easyui-splitbutton" data-options="menu:'#dropdown',iconCls:'icon-download'" >導出</a> </div> <div id="dropdown" style="width:100px; display:none;"> <div data-options="iconCls:'icon-ext-xls'" suffix="xls" data-bind="click:downloadClick">Excel2003 </div> <div data-options="iconCls:'icon-page_excel'" suffix="xlsx" data-bind="click:downloadClick">Excel2007 </div> <div data-options="iconCls:'icon-ext-doc'" suffix="doc" data-bind="click:downloadClick">Word2003 </div> </div> <div id="condition" class="container_12" style="position:relative;"> <div class="grid_1 lbl">收料單號</div> <div class="grid_2 val"><input type="text" data-bind="value:form.BillNo" class="z-txt easyui-autocomplete" data-options="url:'/api/mms/receive/getbillno'"/></div> <div class="grid_1 lbl">項目名稱</div> <div class="grid_2 val"><input type="text" data-bind="value:form.ProjectName" class="z-txt easyui-autocomplete" data-options="url:'/api/mms/project/getprojectname'"/></div> <div class="grid_1 lbl">供應商</div> <div class="grid_2 val"><input type="text" data-bind="value:form.SupplierName" class="z-txt easyui-autocomplete" data-options="url:'/api/mms/merchant/getnames'"/></div> <div class="clear"></div> <div class="grid_1 lbl">倉庫</div> <div class="grid_2 val"><input type="text" data-bind="datasource:dataSource.warehouseItems ,comboboxValue:form.WarehouseCode" class="z-txt easyui-combobox" data-options="showblank:true"/></div> <div class="grid_1 lbl">材料類別</div> <div class="grid_2 val"><input type="text" data-bind="lookupValue:form.MaterialType" class="z-txt easyui-lookup" data-options="lookupType:'materialtype',parentField:'pid'"/></div> <div class="grid_1 lbl">發貨日期</div> <div class="grid_2 val"><input type="text" data-bind="value:form.ReceiveDate" class="z-txt easyui-daterange"/></div> <div class="clear"></div> <div class="prefix_9" style="position:absolute;top:5px;height:0;"> <a id="a_search" href="#" class="buttonHuge button-blue" data-bind="click:searchClick" style="margin:0 15px;">查詢</a> <a id="a_reset" href="#" class="buttonHuge button-blue" data-bind="click:clearClick">清空</a> </div> </div> <table id="gridlist" data-bind="datagrid:grid"> <thead> <tr> <th field="BillNo" sortable="true" align="left" width="90" >收料單號 </th> <th field="ProjectName" sortable="true" align="left" width="80" >項目名稱 </th> <th field="SupplierName" sortable="true" align="left" width="150" >供應商 </th> <th field="ContractCode" sortable="true" align="left" width="80" >合同名稱 </th> <th field="WarehouseName" sortable="true" align="left" width="100" >倉庫 </th> <th field="ReceiveDate" sortable="true" align="center" width="70" formatter="com.formatDate" >發料日期 </th> <th field="MaterialTypeName" sortable="true" align="left" width="100" >材料類別 </th> <th field="TotalMoney" sortable="true" align="right" width="50" formatter="com.formatMoney" >金額 </th> <th field="OriginalNum" sortable="true" align="left" width="90" >原始票號 </th> <th field="CreatePerson" sortable="true" align="left" width="50" >編制人 </th> <th field="CreateDate" sortable="true" align="center" width="70" formatter="com.formatDate" >編制日期 </th> <th field="Remark" sortable="true" align="left" width="150" >備注 </th> </tr> </thead> </table>
代碼貼上來有換行了,我本機沒換行,看着整齊些。這些代碼大家應該基本上都明白,所有的data-bind都是knouckoutjs的寫法,class=”easyui-xxxx"及data-options=”{}"的是easyui的寫法。我的data-bind=”datagrid:grid”這個是我用knouckout去初始化grid。上面的三個腳本我要解釋下,第一個是我項目共通的js,第二個就是我的查詢頁面的共通viewModel,第三個是接收本頁面的mvc后台數據Model,傳遞給viewModel,並且綁定到頁面上。這樣view已經寫好了。如果要寫下一個查詢頁面,就是上頁這一段拿來改改查詢條件啊,數據列啊基本就好了,其它的都是共通的。
接下來我們看看這個共通的viewModel,其實也就不到100行代碼
/** * 模塊名:mms viewModel * 程序名: mms.viewModel.search.js * Copyright(c) 2013-2015 liuhuisheng [ liuhuisheng.xm@gmail.com ] **/ var mms = mms || {}; mms.viewModel = mms.viewModel || {}; mms.viewModel.search = function (data) { var self = this; this.idField = data.idField || "BillNo"; this.urls = data.urls; this.resx = data.resx; this.dataSource = data.dataSource; this.form = ko.mapping.fromJS(data.form); delete this.form.__ko_mapping__; this.grid = { size: { w: 4, h: 94 }, url: self.urls.query, queryParams: ko.observable(), pagination: true, customLoad: false }; this.grid.queryParams(data.form); this.searchClick = function () { var param = ko.toJS(this.form); this.grid.queryParams(param); }; this.clearClick = function () { $.each(self.form, function () { this(''); }); this.searchClick(); }; this.refreshClick = function () { window.location.reload(); }; this.addClick = function () { com.ajax({ type: 'GET', url: self.urls.billno, success: function (d) { com.openTab(self.resx.detailTitle, self.urls.edit + d); } }); }; this.deleteClick = function () { var row = self.grid.datagrid('getSelected'); if (!row) return com.message('warning', self.resx.noneSelect); com.message('confirm', self.resx.deleteConfirm, function (b) { if (b) { com.ajax({ type: 'DELETE', url: self.urls.remove + row[self.idField], success: function () { com.message('success', self.resx.deleteSuccess); self.searchClick(); } }); } }); }; this.editClick = function () { var row = self.grid.datagrid('getSelected'); if (!row) return com.message('warning', self.resx.noneSelect); com.openTab(self.resx.detailTitle, self.urls.edit + row[self.idField]); }; this.grid.onDblClickRow = this.editClick; this.auditClick = function () { var row = self.grid.datagrid('getSelected'); if (!row) return com.message('warning', self.resx.noneSelect); com.auditDialog(function (d) { com.ajax({ type: 'POST', url: self.urls.audit + row[self.idField], data: JSON.stringify(d), success: function () { com.message('success', self.resx.auditSuccess); } }); }); }; this.downloadClick = function (vm, event) { com.exportFile(self.grid).download($(event.currentTarget).attr("suffix")); }; };
下面我解釋下這個viewModel中定義的幾個變量
idField: 這是告訴我哪個是字段的主鍵,編輯按鈕中有使用,要取得行數據中的這個主鍵值傳給編輯頁面。
urls: 告訴我每個數據服務api地址:如 urls={query:’api/xxxx/’,audit:’api/xxxx/audit/’,…}
resx: 存儲一些消息標題等中文,這樣比較好國際化,如 resx={noneSelect:’請先選擇一條數據再操作!’,deleteSuccess:’刪除成功!’}
dataSource:這個是用來存儲數據源的對象,如 dataSource = {warehouseItems: [{text:’A倉庫’,value:’A001’},{},…]}
form: 這個是用來存儲條件部值的對象,可以查看條件中的data-bind都是綁定到form.xxxx字段。
這些值我們都可以在后台定義好傳遞到這個viewModel,所以程序就可以做到共通了。那么這個viewModel也就寫好了,如果還有什么問題大家可以給我留言,我再給大家解釋。
接下來我們寫好了這個viewModel,它還需要一些參數,我們要從后台返回給它,我們回過頭來修改一下那個mvc controller
public ActionResult Index() { var model = new { urls = new { query = "/api/mms/receive", remove = "/api/mms/receive/", billno = "/api/mms/receive/getnewbillno", audit = "/api/mms/receive/audit/", edit = "/mms/receive/edit/" }, resx = new { detailTitle = "收料單明細", noneSelect = "請先選擇一條收料單!", deleteConfirm = "確定要刪除選中的收料單嗎?", deleteSuccess = "刪除成功!", auditSuccess = "單據已審核!" }, dataSource = new{ warehouseItems = new mms_warehouseService().GetWarehouseItems(MmsHelper.GetCurrentProject()) }, form = new{ BillNo = "", ProjectName = "", SupplierName = "", WarehouseCode = "", MaterialType = "", ReceiveDate = "" } }; return View(model); }
好了,這就是viewModel需要的全部參數了。返回不同數據給viewModel,它就能創建不同的實例了。
再說一下上面代碼中的url,實際上是指api的地址,
query:數據查詢服務地址
remove:數據刪除服務地址
audit:數據審核的服務地址
edit:這個不是api服務,這個地址是編輯跳轉的頁面地址。
那么接下來我們就要寫這些web api了。那么我們地新建一個api controller,那么這么一來我們就有兩個控制器了,感覺代碼分得太開,復雜了。我就把這兩個類放在一個文件中吧,這樣會更簡潔點,而且業務名字也都一樣,也算合理。
using 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 class ReceiveApiController : ApiController { } }
這里本來有一個問題,mvc cotroller和api controller的類名后綴都得叫Controller,我加了些配置把api控制器的后綴改成了ApiController,具體參照我的上一篇博客。
我們開始在ReceiveApiController類中添加查詢的數據服務
// 查詢:GET api/mms/receive public List<dynamic> Get(RequestWrapper query) { query.LoadSettingXmlString(@" <settings defaultOrderBy='BillNo'> <select> A.*, B.ProjectName, C.MaterialTypeName, D.WarehouseName as WarehouseName, E.MerchantsName AS SupplierName </select> <from> mms_receive A left join mms_project B on B.ProjectCode = A.ProjectCode left join mms_materialType C on C.MaterialType = A.MaterialType left join mms_warehouse D on D.WarehouseCode = A.WarehouseCode left join mms_merchants E on E.MerchantsCode = A.SupplierCode </from> <where defaultForAll='true' defaultCp='equal' defaultIgnoreEmpty='true' > <field name='BillNo' cp='equal' ></field> <field name='ProjectName' cp='like' ></field> <field name='E.MerchantsName' cp='like' variable='SupplierName' ></field> <field name='A.WarehouseCode' cp='equal' ></field> <field name='A.MaterialType' cp='equal' ></field> <field name='ReceiveDate' cp='daterange' ></field> </where> </settings>"); var ReceiveService = new mms_receiveService(); var pQuery = query.ToParamQuery(); var result = ReceiveService.GetDynamicListWithPaging(pQuery); return result; }
上面這段代碼是利用了我的框架寫出來的。我解釋下為什么要這么寫:
首先這個服務要接收條件部的參數form={a:’’,b:’’,…}還要接收grid中的參數排序sort=a order=desc以及分頁請求page=1,rows=20,這些功能我們都要去實現。
還有一個最煩人的東西,我們的查詢條件連接,估計大家都寫過這種代碼:
if (SrcBillType.Length > 0) { sWhere += string.Format(" And T.SrcBillType='{0}'", SrcBillType); }
一兩個還好,那如果我的條件一多,那就一串很惡心的代碼。而且還是拼接sql文,如果不處理還會有sql注入的危險。所以我就一直在想怎么改進這種代碼。
我是這樣想的,我們正常的查詢一般都是前台給我們json數據,不帶任何邏輯的。我們后台應該要有一段邏輯的配置,我的這個配置+前台送過來的數據,應該就可以得到我需要的查詢。
有些人前台傳過來就邏輯都處理好了,是一個自定義的復雜的數據結構,后台再根據這個轉換成我們需要的參數,我個人覺得這樣做的話一來是存在安全性問題,二來前台也會相對復雜,我現在設計前台傳遞過來就是最簡單的json數據結構,如{a:’’,b:’’,c:’’},其它的后台處理。所以我設計了一個類RequestWrapper業實現。
RequestWrapper中包含請求的數據和查詢的邏輯配置,我把這個配置定義成一段xml,上面我數據查詢服務中的那個,包括select、from、where及一些其它節點,然后我們可以在這個里面定義查詢。這個就讓我們想起著名的iBatisNet框架了,它的SQL是存放在xml中的,所以我們可以把這段配置也放在xml文件中,當然也可以寫在代碼里。RequestWrapper如果有接收到分頁或排序的請求時,還會自動處理。
如上Get方法中的配置,不用我解釋大家基本能理解,在where中我稍微說一下,field的name對應的是字段,cp對應的是我在框架中預定義好的查詢邏輯,全部放在Zephyr.Core.Cp類下面,相當於一個lambda表達式wheredata=>return conditionSql這種形式。 如下左圖是我框架中預定義好的。如果我預定義的不夠用,我們每個項目中還可以去拓展這個類。這樣還有一個好處就是我可以很簡單的處理一個復雜的東西,比如我們查詢條件中的 接收日期我用了一個日期區間控件,它得到的值是類似:2013-05-17 到 2013-06-01
而且有可能是from沒有,或者是to沒有,那么我們就需要在DateRange中定義好它的規則可以直接生成一個或兩個條件返回。
說到這里,大家應該對我的這個RequestWrapper有一點點了解了吧。所以我們寫代碼就這樣
public List<dynamic> Get(RequestWrapper query) { query.LoadSettingXmlString(@"xml...."); var ReceiveService = new mms_receiveService(); var pQuery = query.ToParamQuery(); var result = ReceiveService.GetDynamicListWithPaging(pQuery); return result; }
我在Web api的參數綁定時已經把request的值放到requestWrapper中,所以我們只要接收這個參數就已經有請求的數據了。所以我們需要用query.LoadSettingXmlString的方式加載一段xml的邏輯設置。那么這個查詢參數就基本完成了。
我們再new一個mms_receiveService數據服務,這個數據服務其實就是我的Model層的代碼,如下
using System; using System.Collections.Generic; using System.Text; using Zephyr.Core; namespace Zephyr.Models { [Module("Mms")] public class mms_receiveService : ServiceBase<mms_receive> { } public class mms_receive : ModelBase { [PrimaryKey] public string BillNo{ get; set; } public DateTime? BillDate{ get; set; } public string DoPerson{ get; set; } public string ProjectCode{ get; set; } public string SupplierCode{ get; set; } public string ContractCode{ get; set; } public string WarehouseCode{ get; set; } public DateTime? ReceiveDate{ get; set; } public string MaterialType{ get; set; } public string SupplyType{ get; set; } public decimal? TotalMoney{ get; set; } public string PayKind{ get; set; } public string OriginalNum{ get; set; } public string ApproveState{ get; set; } public string ApprovePerson{ get; set; } public DateTime? ApproveDate{ get; set; } public string ApproveRemark{ get; set; } public string CreatePerson{ get; set; } public DateTime? CreateDate{ get; set; } public string UpdatePerson{ get; set; } public DateTime? UpdateDate{ get; set; } public string Remark{ get; set; } } }
Models層中的的代碼是創建項目時我用我生成器批量生成的,我在這里面不用寫任何代碼。
然后我們調用ServiceBase基類中的方法ReceiveService.GetDynamicListWithPaging即可完成分頁查詢。(基類中有很多方法,以后博客中再跟大家說)
所以我們的后台查詢就上面Get中的幾行代碼就搞定了。而且這個方法也可以共通起來,把配置放到xml中去,請求時告訴我你查詢配置是哪個文件,我就自動去加載,實際上一個api就可以搞定所有查詢了。如果要修改查詢,只要修改xml文件就好了。
接下來我們還要在ReceiveApiController類中添加刪除及審核的數據服務:
// 刪除:DELETE api/mms/send public void Delete(string id) { var service = new mms_receiveService(); var result = service.Delete(ParamDelete.Instance().AndWhere("BillNo", id)); MmsHelper.ThrowHttpExceptionWhen(result <= 0,"收料單刪除失敗[BillNo={0}],請重試或聯系管理員!",id); } // 審核:POST api/mms/send/audit [System.Web.Http.HttpPost] public void Audit(string id,JObject data) { var status = data["status"].ToString(); var comment = data["comment"].ToString(); var result = new MmsService().Audit("mms_receive", id, status, comment); MmsHelper.ThrowHttpExceptionWhen(result <= 0,"審核收料單失敗[BillNo={0}],請重試或聯系管理員!",id); }
這樣后台就全部寫完了,是不是很簡潔。至此,我們的查詢功能就全部完成了。
效果展示
收料單號,項目名稱,供應商名稱,都為自動完成控件,跟百度的差不多,會有建議提示下拉。
分頁測試,數據只做了11條,把每頁行數調到10
第一頁,測試ok
第二頁,測試ok
按鈕測試
刷新測試OK,新增、修改會打開新的一個Tab頁 如 /mms/receive/edit/201212260001
導出pdf原來也有做,但是有一個中文亂碼的問題還沒得到解決。
在使用這個viewModel的情況下,我開發一個一般的查詢頁面,基本上可以5-10分鍾完成。
大家是不是也覺得功能還算挺多的頁面,開發也很簡單~^o^~。
這篇利用基礎框架的是我第一篇博客中有介紹:
第一次發博客-說說我的B/S開發框架(asp.net mvc + web api + easyui):http://www.cnblogs.com/xqin/archive/2013/05/29/3105291.html
后述
在這篇中我們主要是說如何前台用一個共通的viewModel,后台利用框架幾句簡單的代碼實現一個復雜的查詢。在這里實際上還涉及到很多其它的問題:
1、如那些控件是怎么做的,那些條件部的控件,除了下拉框easyui中都沒有,其實這些是我自己在easyui的基礎上拓展的,日期期間是國外的一個控件,我改改也放進來了,都統一成easyui的寫法。
2、框架底層實現了哪些東西,基類中有哪些現成的方法。
3、共通的審核、下載是如何做的
4、共通的js中,如何knouckoutjs是怎么和easyui綁定的等等
這些問題我在以后的博客中會一一寫出來,接下來我可能會先寫 如何用一個共通的viewModel搞定所有的編輯頁面,這個比較復雜一點,可能要分幾篇寫,如果大家感興趣,幫我頂下,謝謝大家~~