本人闲来无事就把以前用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在这里就不多讲了,有兴趣的朋友可以去下载项目看看。
最后,写这篇文章的目的不在于项目本身,而在跟大家分享技术,由于时间有限,库存和销售模块业务逻辑和客户端的具体实现没有去做,有前面的几个模块就足够展示这些技术的运用了,希望大家见谅。源码:https://files.cnblogs.com/files/chenlinzhi/JXCProject.zip