本人閑來無事就把以前用Asp.net做過的一個醫葯管理信息系統用mvc,ef ,easyui重新做了一下,業務邏輯簡化了許多,旨在加深對mvc,ef(codefirst),easyui,AutoMapper,Ninject等技術的理解和運用,今天拿出來跟大家分享,就是想對這些技術還處在入門階段的朋友做以參考,以及正在用這些技術做項目的朋友做一個交流和探討。
我會在此項目的基礎上去逐一講解這些技術,簡單應用就不講了,去看項目,主要講重點難點以及需要注意的地方,有些地方不明白的可以去下載源代碼,估計一看就能明白,廢話不多說先上一張項目分層圖:
其中Domain為領域模型層;Reposirory為倉儲層,主要負責數據庫操作;Service為服務層,項目的業務邏輯全在此;Infrastructure為基礎結構層,項目通用的類庫在這里;客戶端把View和Controller分開。
一:Entityframework
1:CodeFist
Entityframework中CodeFirst功能主要目的是生成數據庫,而生成數據庫的字段,約束比較簡單,而生成表之間的關系有時比較麻煩,尤其是聯合主鍵有時難搞,先上張項目模型關系圖的一部分:
大家看到這張圖能在CodeFirst的ModelConfiguration中寫出相應代碼生成表之間關系嗎?其中SaleOutDetial(銷售出庫明細)表有3個主鍵SaleOutId,WarePositionId,ProductId,而同時它們又是外鍵,SaleOutId來自於SaleOut(銷售出庫)表,WarePositionId,ProductId來自於PositionStock(貨位庫存)表,而WarePositionId,ProductId是PositionStock表的主鍵也是外鍵,分別來自貨位表和品種表。那如何寫代碼配置SaleOutDetail表於其他表之間的關系呢?

Public class SaleOutDetailConfiguration : EntityTypeConfiguration<SaleOutDetail> { public SaleOutDetailConfiguration() { ToTable("SaleOutDetail"); HasKey(k => new { k.SaleOutId, k.WarePositionId,k.ProductId }); HasRequired(o => o.SaleOut).WithMany(o => o.SaleOutDetails).HasForeignKey(k => k.SaleOutId); HasRequired(o => o.PositionStock).WithMany(o => o.SaleOutDetails).HasForeignKey(k => new { k.WarePositionId, k.ProductId }).WillCascadeOnDelete(false); } }
其中HasKey,HasForeignKey用於配置表主鍵和外鍵, HasRequired,WithMany配置一對多的關系;特別注意一點配置主鍵字段的順序和配置外鍵字段的順序一定要一致,如果上面HasForeignKey(k => new { k.WarePositionId, k.ProductId })寫成HasForeignKey(k => new { k.ProductId , k.WarePositionId})將會出錯。剛開始自己也犯了這個錯誤,字段順序搞錯,結果調試了很久才解決問題。
用HasRequired(o => o.PositionStock).WithMany(o => o.SaleOutDetails)在配置一對多(一個貨位庫存對應多個銷售出庫明細)的關系時,刪除數據時會出現級聯刪除的現象,上面刪除貨位對應的銷售出庫明細記錄也將被刪除,設置WillCascadeOnDelete(false)就不會級聯刪除了。當然也可以用HasOptional來替代HasRequired防止級聯刪除,但這樣表之間的約束已經變了,所以得依據實際情況做選擇。
當你配置表復雜關系都沒問題的話,簡單關系像一對一,多對多就變得簡單了,其他就不講了,可以下載源代碼去看看。如果想對CodeFirst有更多的了解的話可以去看<<Programming Entity Framework_ Code First>>這本書,很薄不到200頁。
2.DbContext
Entityframework查詢數據的三種方式:延遲加載,飢餓加載,顯式加載就不多講了,去看代碼很多地方都已體現。講一下Entityframework刪除和修改數據跟用linq to sql的不同之處吧。EF刪除和修改數據不必像linq to sql 那樣先得查詢出某條記錄,然后再對記錄刪除或修改。
EF刪除數據只需new一實體,實體ID跟要刪除數據的ID相同就是了。比如
Privte void DeleteCustomer(Customer cst) { Using(var context=new JXCContext()) { context.Entry(cst).State =EntityState.Deleted; context.SaveChanges(); } }
EF修改數據只需new一實體,實體ID跟要修改數據的ID相同,把要修改的屬性賦值就行了。比如
Privte void ModifyCustomer(Customer cst) { Using(var context=new JXCContext()) { context.Entry(cst).State =EntityState.Modified; context.SaveChanges(); } }
這種刪除修改數據的方法能夠減少一次數據庫訪問。具體做法以及如何實現實體部分更新,可以去看項目。如果想對EF的數據操作有更多的了解,建議看<<Programming Entity Framework_ DbContext>>這本書,250頁左右。
二:MVC
MVC無非就是View,Controller,模型模版,模型綁定,模型驗證,UnobtrusiveAjax等等,挑幾個講以下.
1:Controller
在Controller類中有OnActionExecuting,OnActionExecuted,OnResultExecuting,OnResultExecuted幾個方法,分別代表控制器方法執行前,執行后,視圖呈現前,呈現后需要調用的方法。通常我們會把一些重復調用的代碼寫在這里,然后放到BaseCtroller類中讓子Controller使用。比如客戶端用Easyui datagrid做一個表單然后調用Controller中的GetPageData方法呈現數據,並且需排序分頁,通常這樣GetPageData方法這樣寫:
xxService.GetPageData('查詢條件1','查詢條件2',‘查詢條件N...’,int.Parse(request["page"]) - 1, int.Parse(request["rows"]),request["sort"],request["order"] == "asc" ? true : false, out recordCount);
像這些從datagrid中傳遞過來的變量我們完全可以封裝起來,放到BaseCtroller中的OnActionExecuting方法中,上代碼:

public class PageDescriptor { public int PageCount { get; set; } public int PageIndex { get; set; } public string Sort { get; set; } public bool Order { get; set; } } public class BaseController:Controller { private PageDescriptor pageDescriptor; public PageDescriptor PageDescriptor { get { return pageDescriptor; } } protected override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.HttpContext.Request; if (request["rows"] != null && request["page"] != null) { pageDescriptor = new PageDescriptor(); pageDescriptor.PageCount = int.Parse(request["rows"]); pageDescriptor.PageIndex = int.Parse(request["page"]) - 1; pageDescriptor.Order = request["order"] == "asc" ? true : false; pageDescriptor.Sort = request["sort"]; } base.OnActionExecuting(filterContext); } }
然后Controller中的GetPageData方法可以這樣寫:xxxService.GetPageData('查詢條件1','查詢條件2',‘查詢條件N...’,PageDescriptor.PageIndex,PageDescriptor.PageCount,PageDescriptor.Sort, PageDescriptor.Order, out recordCount);這樣寫着簡單,省得出錯,可以重復利用。
2:View
視圖就講一個子Action,@Html.Action("actionname")的使用吧。比如有一個品種管理頁面,有查詢,新增,編輯品種等功能,我們可以把新增,編輯功能分別建個view,讓后相對應寫個controller。在品種管理視圖頁面以@Html.Action("AddProduct"),@Html.Action("ModifyProduct")的方式寄宿它們的視圖,這樣省得一個頁面html元素和javascript太多,便於管理。需注意一點的是宿主頁面的javascript變量,html元素在子頁面中可以調用,宿主頁面生成時也包括子頁面的javascript和html,實際上它們都在同一個頁面,寫腳本的時候要防止變量沖突,不了解的可以去該項目"品種管理"模塊去查看。
3:模型驗證
關於模型驗證稍微講多一點。Mvc的模型驗證分為服務器端驗證和客戶端驗證。先講服務器端的驗證。
mvc服務器端的驗證你需要明白模型驗證的基本流程:首先你得知道驗證在什么地方被觸發;驗證觸發后如何收集驗證信息;最后如何把驗證信息反饋到客戶端。驗證觸發的地方有很多:設置Model或Model屬性的ValidationAttribute;或者在控制器中通過ModelState.AddModelError(key,value)這個方法顯式添加驗證;或者讓模型實現IValidatableObject接口,重寫接口的IEnumerable<ValidationResult>Validate(ValidationContext validationContext)方法來添加驗證;驗證觸發后所有的驗證信息被放到一個叫ModelStateDictionary類型的ModelState屬性中,然后被MVC自動獲取,當然你也可以手動去遍歷該詞典獲取驗證信息(后面會講到);最后MVC通過視圖引擎將驗證信息顯示到客戶端,當然你也可以寫點腳本手動去實現(后面會講到)。明白該流程后你才能做到胸有成竹。
Mvc客戶端的驗證其實完全依賴於jquery.validate和jquery.validate.unobtrusive這兩個腳本文件,在Asp.net中你也可以這么做。
這兩個腳本文件中有許多驗證規則,你只需把驗證規則添加到html元素的標簽中就可以進行客戶端的驗證,感覺跟Easyui的驗證很相似,只不過MVC能夠自動去添加驗證規則。比如一個Textbox:
<input class="text-box single-line" data-val="true" data-val-length="只能3到10個字符" data-val-length-max="10" data-val-length-min="3" data-val-required="必填" id="name" name="name" type="text" value="" /> 其中以data-val開頭的標簽說明它要在客戶端驗證,length和required為驗證規則,max ,min為length規則的參數。你可以手動去添加這些驗證規則,也可以讓Mvc自動添加這些規則,如果要mvc自動添加客戶端驗證規則,你得自定義一個ValidationAttribute,並且實現IClientValidatale接口的IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)方法,再把該Atrribute放到model屬性上,這樣mvc在解析model時會通過視圖引擎將驗證規則自動添加到html元素標簽中.實現該接口的目的而且是唯一的目的就在於此。如果你想對自定義客戶端和服務器端驗證有更多的了解,建議看下園子里的這篇文章:http://www.cnblogs.com/artech/archive/2012/05/15/custom-client-validation.html。
不過mvc的驗證也存在一個缺陷,就是你必須單擊頁面的一個submit按鈕提交一個表單后才能觸發客戶端的驗證,客戶端驗證通過后再到服務器端解析模型然后進行服務器端的驗證。(之所以這樣是因為提交form表單時視圖引擎有一個反饋驗證信息的動作)如果你想隨便單擊一個button按鈕以ajax的方式調用控制器,而不是依賴單擊submit按鈕進行客戶端和服務器端驗證該如何實現呢?
其實也挺簡單的,客戶端的驗證你只需用腳本顯式調用('#form').valid()這個方法就可以了,驗證通過返回true,否則false。驗證通過后你再調用controller方法進行服務器端的驗證。在controller中你需收集驗證信息然后以json數據格式返回到客戶端,然后用腳本把驗證信息顯示出來就行了。舉個例子,在項目品種管理頁面添加品種時候,單擊提交按鈕觸發客戶端驗證,通過后繼續服務器端驗證,如圖
先在BaseCtroller添加獲取驗證信息的方法:

protected virtual List<object> GetErrorMessages() { List<object> errors = new List<object>(); foreach (var key in ModelState.Keys) { if (ModelState[key].Errors.Count > 0) { var obj = new { key = key, errorMessage = ModelState[key].Errors.Select(o =>o.ErrorMessage).First() }; errors.Add(obj); } } return errors; }
然后在controller中調用
[HttpPost] public ActionResult AddProduct(ProductDTO product) { if (ModelState.IsValid) { productService.AddProduct(product); return Json(null); } else return Json(GetErrorMessages(), "text/html", JsonRequestBehavior.AllowGet); }
客戶端再寫相關的腳本:

function addProduct() { $('#formAdd').form('submit', { onSubmit: function (param) { if ($('#formAdd').valid()) { param.ProductCategoryId = $('#ProductCategoryId').combotree('getValue'); $('#btnAddProduct').linkbutton('disable'); } }, success: function (data) { $('#btnAddProduct').linkbutton('enable'); if (data.length > 0) ShowValidateMessage($.parseJSON(data)); else $.messager.alert("消息", "操作成功!"); } }) } //顯示驗證信息 function ShowValidateMessage(data) { for (var i = 0; i < data.length; i++) { var $span = $('#tableAdd').find('span[data-valmsg-for=' + data[i].key + ']'); $span.css('color','red').show().text(data[i].errorMessage); } }
這樣就完成了客戶端和服務器端的驗證了。不過用jquery1.6,$('#formAdd').valid()永遠返回true,用jqeury1.5.1才能正確驗證,不知道大家有木有遇到這種情況。
4模型綁定
mvc模型綁定無非就是把數據從客戶端傳遞到服務器端,它默認是通過DefaultModelBinder類來操作的,當然你也也可自定義一個ModelBinder。
當你調用一個action時需要從客戶端傳遞一個name參數,mvc默認按Request.Form["name"],RouteData.Values["name"],Request.QueryString["name"]順序來搜索name的值,當然你也可以通過Json數據格式把name傳遞給action,以ajax方式調用action時經常會這么做。
模型綁定有簡單類型,集合類型,復雜類型的綁定。當你把一個復雜類型嵌套復雜類型的綁定弄明白后,其他的就簡單了。比如項目客戶管理在新增客戶時:
單擊提交按鈕,調用控制器中AddCustomer(Customer customer) 方法時, 該如何編寫一個customer的json數據把頁面數據傳遞給customer呢?
public class Customer { public string ID { get; set; } public string SimpleID { get; set; } public string Name { get; set; } public string Address { get; set; } public string Telephone { get; set; } public string Zip { get; set; } public string CstType { get; set; } public virtual IList<CustomerAddress> CustomerAddresses { get; set; } } } public class CustomerAddress { public System.Guid ID { get; set; } public string Reciever { get; set; } public string Address { get; set; } public string ZipCode { get; set; } public string Telephone { get; set; } public string CustomerId { get; set; } public virtual Customer Customer { get; set; } }
客戶端腳本:

function addCustomer() { var data = $('#addressTable').datagrid('getData').rows; var cst = {}; //關鍵代碼在此 for (var i = 0; i < data.length; i++) { cst['CustomerAddresses[' + i + '].Reciever'] = data[i].Reciever; cst['CustomerAddresses[' + i + '].Address'] = data[i].Address; cst['CustomerAddresses[' + i + '].Telephone'] = data[i].Telephone; cst['CustomerAddresses[' + i + '].ZipCode'] = data[i].ZipCode; } cst.SimpleID = $('#SimpleID').val(); cst.Name = $('#Name').val(); cst.Address = $('#Address').val(); cst.Telephone = $('#Telephone').val(); cst.Zip = $('#Zip').val(); cst.CstType = $('#CstType').val(); $.ajax({ url: '@Url.Action("AddCustomer")', type: 'POST', data: cst, }) }
其實客戶端只需定義一個對象,對象的簡單屬性只要跟Customer的屬性同名,對象中的對象也可以說是數組對應Customer的IList<CustomerAddress>,名稱也必須為CustomerAddresses,對象中的對象的屬性對應CustomerAddress的屬性,這樣一個嵌套復雜類型的復雜類型就成功傳遞到了服務器端。
三:Easyui
Javascript不是本人的強項,Easyui也算不上精通,但熟練使用還是沒問題。只要你把Easyui最麻煩的幾個組件弄明白,我估摸着其它的看下文檔就會了,就講下datagrid和combotree吧。
1:datagrid
比如客戶管理頁面,查詢客戶數據時,如圖:
該dagagrid能排序分頁,多選,單擊行+號能查看客戶收貨地址數據,具體實現如下:

$('#grid').datagrid({ title: '客戶信息', width: 1000, url: '@Url.Action("GetPageData")', collapsible: true, pagination: true, view: detailview, detailFormatter: function (index, row) { return '<div style="padding:2px"><table id="ddv"></table></div>'; }, queryParams: { cstType: $('#ct').combotree('getValue'), cstName: $('#searchName').val() }, columns: [[ { field: 'ck', checkbox: true }, { field: 'ID', title: 'ID', hidden: true }, { field: 'Name', title: '名稱', sortable: true }, { field: 'CstType', title: '客戶類型', sortable: true }, { field: 'Telephone', title: '電話', sortable: true }, { field: 'Zip', title: '郵編', sortable: true }, { field: 'Address', title: '地址', width: 445, sortable: true } ]], toolbar: [ { id: 'btnRemove', text: '刪除客戶', iconCls: 'icon-remove', handler: function () { deleteCustomer(); } }, { id: 'btnEdit', text: '修改客戶', iconCls: 'icon-edit', handler: function () { showEditDialog(); } } ], onExpandRow: function (index, row) { var ddv = $(this).datagrid('getRowDetail', index).find('#ddv'); ddv.datagrid({ url: '@Url.Action("GetCustomerAddress")', queryParams: { cstId: row.ID }, fitColumns: true, singleSelect: true, height:'auto', rownumbers: true, loadMsg: '正在加載客戶地址......', columns: [[ { field: 'Reciever', title: '收貨人', width: 80 }, { field: 'Telephone', title: '電話', width: 100 }, { field: 'ZipCode', title: '郵編', width: 80 }, { field: 'Address', title: '地址', width: 250 } ]], onResize: function () { $('#grid').datagrid('fixDetailRowHeight', index); }, onLoadSuccess: function () { setTimeout(function () { $('#grid').datagrid('fixDetailRowHeight', index); }, 0); } }) $('#grid').datagrid('fixDetailRowHeight', index); } })
注意兩點:1:顯示明細功能需引入datagrid-detailview.js腳本;2:服務器端數據格式得是{total=dataCount,rows=data}這樣的object類型,便於datagrid解析數據。
2:Combotree
比如這樣一個品種分類,如何實現呢?
客戶端腳本就一行代碼$('#tree').combotree({url:'@Url.Action('actionName','controller')'}).關鍵是構建服務器端的數據格式。你得按照[{id:'',name:'',children:[{id:'',name:'',children:[{......}]}]}]去構造數據。很多人喜歡用拼接字符串的方式去構造數據,這樣行,但感覺代碼有點亂,不好控制,尤其是遞歸太多的時候。其實可以用類型對象,把數據賦值給對象屬性,好控制好遞歸。上圖服務器端具體實現如下:

public class TreeDescriptor { public string Id { get; set; } public string Text { get; set; } public string State { get; set; } public List<object> Children { get; set; } } private List<object> AddTopCategory() { var data = productCategoryService.GetProductCategoriesByParentId(null); var nodes = new List<object>(); foreach (var o in data) { TreeDescriptor tree = new TreeDescriptor(); tree.Id = o.ID; tree.Text = o.CategoryName; tree.Children = AddChildrenCategory(o.ID); nodes.Add(new { id = tree.Id, text = tree.Text, children = tree.Children }); } return nodes; } private List<object> AddChildrenCategory(string parentId) { var data = productCategoryService.GetProductCategoriesByParentId(parentId); var nodes = new List<object>(); foreach (var o in data) { TreeDescriptor tree = new TreeDescriptor(); tree.Id = o.ID; tree.Text = o.CategoryName; tree.Children = AddChildrenCategory(o.ID); nodes.Add(new { id = tree.Id, text = tree.Text, children = tree.Children }); } return nodes; } //客戶端調需調用的方法 public JsonResult CreateCategoryTree() { return Json(AddTopCategory()); }
這樣就可以構建能夠遞歸數據的combotree了。
其他的像彈出層,模態窗口,動態構建datagrid在這里就不多講了,有興趣的朋友可以去下載項目看看。
最后,寫這篇文章的目的不在於項目本身,而在跟大家分享技術,由於時間有限,庫存和銷售模塊業務邏輯和客戶端的具體實現沒有去做,有前面的幾個模塊就足夠展示這些技術的運用了,希望大家見諒。源碼:http://files.cnblogs.com/files/chenlinzhi/JXCProject.zip