我们在项目开发中,做得最多的可能就是CRUD,那么我们如何在ASP.NET MVC中来做CRUD呢?如果说只是单纯实现功能,那自然是再简单不过了,可是我们要考虑如何来做得比较好维护比较好扩展,如何做得比较漂亮。做开发要有工匠精神,不要只求完成开发任务,那样的话,永远停留在只是简单的写业务逻辑代码水平,我们要做有追求的程序员。本来这么简单的东西,我真是懒得写,但是看到即便是一些工作了好些年的人,做东西也是只管实现功能,啥都不管,还有些界面css样式要么就硬编要么就毫无规则的在页面中进行穿插,遇到要设置间距甚至直接写多个 ,我觉得还是要写出来给那些人看下。硬编的前提是只有你这一个界面使用。
我们先来看下我们要实现的效果,功能需要:新增、修改、删除、查询、分页、排序、导出excel、打印、上传图片、支持表单验证,优雅的实现有多简单?非常简单。
需要用到哪些UI组件?我都是基于bootstrap这种扁平化响应式风格的,jquery.dataTables.js、toastr.js、bootstrapValidator.js、bootstrap-confirmation.js、printThis.js
由于是演示用,所以控制器中直接调用了EF上下文操作,重点是看前端部分的渲染和交互,不要重复你的代码!不要重复你的代码!不要重复你的代码!重要的事情说三遍!为什么没有用mvc自带的模型验证?答:太傻逼!1、样式改起来操蛋;2、每次要点击提交表单才促发验证。
这里表数据量少,是直接一次性加载,然后内存中分页的,如果表数据量多,那么就要用服务器分页,同样很简单。修改下配置项,如下所示:然后控制器中对应的方法稍微修改下即可。
options.bServerSide = true;,
options.fnServerParams = function (aoData) { //查询条件 aoData.push( { "name": "LogName", "value": $("#LogName").val() } ); };
BaseController控制器基类
[PublicAuthorize] public class BaseController : Controller { HomeService _HomeService = new HomeService(); #region 字段 ////// 新增 /// protected string CText = "新增"; /// /// 读取 /// protected string RText = "读取"; /// /// 更新 /// protected string UText = "更新"; /// /// 删除 /// protected string DText = "删除"; /// /// 数据有误! /// protected string VoidText = "数据有误!"; #endregion #region 属性 /// /// 获取点击的菜单ID /// public string MenuId { get { return Request.QueryString["MenuId"]; } } /// /// 自动构建页面标题导航 /// public MvcHtmlString HeadString { get { return new MvcHtmlString(_HomeService.GetHead(int.Parse(MenuId))); } } /// /// 构造非菜单页的界面导航标题 /// /// 页面标题 public void CreateSubPageHead(string title) { ViewBag.HeadString = HeadString; ViewBag.MenuId = MenuId; ViewBag.SubHeadString = title; } #endregion [HttpGet] public virtual ActionResult Index() { if (!string.IsNullOrEmpty(MenuId)) { ViewBag.MenuId = MenuId; ViewBag.HeadString = HeadString; } return View(); } /// /// 操作成功 /// /// 提示文本 /// protected virtual AjaxResult SuccessTip(string message) { return new AjaxResult { state = ResultType.success.ToString(), message = message }; } /// /// 操作失败 /// /// 提示文本 /// protected virtual AjaxResult ErrorTip(string message) { return new AjaxResult { state = ResultType.error.ToString(), message = message }; } }
控制器DefaultController
public class DefaultController : BaseController { private MyContext db = new MyContext(); ////// 客户列表 /// /// /// [HttpPost] public JsonResult List(Customer filter) { //filter.PageSize = int.MaxValue; IQueryable dataSource = db.Customers; if (!string.IsNullOrEmpty(filter.Name)) { dataSource = dataSource.Where(x => x.Name == filter.Name).OrderBy(x => x.CreateTime); } List queryData = dataSource.ToList(); var data = queryData.Select(u => new { ID = u.Id, Name = u.Name, CreateTime = u.CreateTime.ToDateStr(), Address = u.Address }); //构造成Json的格式传递 var result = new { iTotalRecords = queryData.Count, iTotalDisplayRecords = 10, data = data }; return Json(result, JsonRequestBehavior.AllowGet); } #region CRUD [HttpGet] public ActionResult Create() { return View(); } [HttpPost] public JsonResult Create([Bind(Include = "Name,Address,CreateTime,Msg,HeadsUrl")] Customer _Customer) { AjaxResult _AjaxResult = null; if (ModelState.IsValid) { db.Customers.Add(_Customer); _AjaxResult = db.SaveChanges() > 0 ? SuccessTip(string.Format("{0}成功!", CText)) : ErrorTip(string.Format("{0}失败!", CText)); ; } else { _AjaxResult = ErrorTip(VoidText); } return Json(_AjaxResult, JsonRequestBehavior.AllowGet); } [HttpGet] public ActionResult Update(int Id) { var model = db.Customers.Where(x => x.Id == Id).FirstOrDefault(); return View(model); } [HttpPost] public JsonResult Update([Bind(Include = "Id,Name,Address,CreateTime,Msg,HeadsUrl")] Customer _Customer) { AjaxResult _AjaxResult = null; if (ModelState.IsValid) { db.Entry(_Customer).State = EntityState.Modified; _AjaxResult = db.SaveChanges() > 0 ? SuccessTip(string.Format("{0}成功!", UText)) : ErrorTip(string.Format("{0}失败!", UText)); } else { _AjaxResult = ErrorTip(VoidText); } return Json(_AjaxResult, JsonRequestBehavior.AllowGet); } [HttpPost] public JsonResult Delete(int Id) { var model = db.Customers.Where(x => x.Id == Id).FirstOrDefault(); db.Customers.Remove(model); AjaxResult _AjaxResult = db.SaveChanges() > 0 ? SuccessTip(string.Format("{0}成功!", DText)) : ErrorTip(string.Format("{0}失败!", DText)); return Json(_AjaxResult, JsonRequestBehavior.AllowGet); } [HttpPost] public JsonResult DeleteList(List<int> ids) { var list = db.Customers.Where(x => ids.Contains(x.Id)).ToList(); db.Customers.RemoveRange(list); AjaxResult _AjaxResult = db.SaveChanges() > 0 ? SuccessTip(string.Format("{0}成功!", DText)) : ErrorTip(string.Format("{0}失败!", DText)); return Json(_AjaxResult, JsonRequestBehavior.AllowGet); } #endregion #region File handle /// /// 导出Excel /// /// public FileResult ExportExcel() { string excelPath = Server.MapPath("~/Excel/用户列表.xls"); GenerateExcel genExcel = new GenerateExcel(); genExcel.SheetList.Add(new UserListSheet(db.Customers.ToList(), "用户列表")); genExcel.ExportExcel(excelPath); return File(excelPath, "application/ms-excel", "用户列表.xls"); } /// /// 上传文件 /// /// public JsonResult ExportFile() { HttpPostedFileBase file = Request.Files["txt_file"]; uploadFile _uploadFile = new uploadFile(); if (file != null) { string str = DateTime.Now.ToString("yyyyMMddhhMMss"); var fileFullName =string.Format("{0}{1}_{2}",Request.MapPath("~/Upload/"),str ,file.FileName); try { file.SaveAs(fileFullName); _uploadFile.state = 1; } catch { _uploadFile.state = 0; } finally { _uploadFile.name = str+"_"+file.FileName; _uploadFile.fullName = fileFullName; } } else { _uploadFile.state = 0; } return Json(_uploadFile, JsonRequestBehavior.AllowGet); } /// /// 删除文件 /// /// [HttpPost] public JsonResult DeleteFile(string key) { var fileFullName = Path.Combine(Request.MapPath("~/Upload"), key); int state = 0; try { state = FileHelper.DeleteFile(fileFullName) ? 1 : 0; //var model = db.Customers.Where(x => x.HeadsUrl == key).FirstOrDefault(); //if(model!=null) //{ // db.Customers.Remove(model); //} } catch { state = 0; } return Json(state, JsonRequestBehavior.AllowGet); } #endregion }
视图页面,razor、css、js都分离,不要放到一个文件中。
Index视图:
@model List<Secom.Smp.Data.Models.Customer> @{ ViewBag.Title = "用户列表"; ViewBag.ParentTitle = "系统管理"; Layout = "~/Views/Shared/_Page.cshtml"; } <style type="text/css"> .divModal { width: 700px; } style> <div class="page-content-body"> <div class="row"> <div class="col-md-12"> <div class="portlet-title"> <div class="caption font-dark"> <i class="icon-settings font-dark">i> <span class="caption-subject bold uppercase">用户列表span> div> <div class="actions"> div> div> <div class="portlet-body"> <div class="table-toolbar"> <div class="row"> <div class="col-md-6"> <div class="btn-group"> <button id="btnAdd" class="btn sbold green" onclick="DataTablesObj.doCreateModal('/Admin/Default/Create')" data-toggle="modal">添加用户<i class="fa fa-plus">i>button> <button id="btnDeleteList" title="确定要删除吗?" class="btn sbold btn-danger deleteBtn" data-toggle="confirmation" data-placement="right" data-btn-ok-label="继续" data-btn-ok-icon="icon-like" data-btn-ok-class="btn-success" data-btn-cancel-label="取消" data-btn-cancel-icon="icon-close" data-btn-cancel-class="btn-danger"> 批量删除 <i class="fa fa-minus">i> button> div> div> <div class="col-md-6"> <div class="btn-group pull-right"> <button class="btn green btn-outline dropdown-toggle" data-toggle="dropdown"> 操作 <i class="fa fa-angle-down">i> button> <ul class="dropdown-menu pull-right"> <li> <a href="/Admin/Default/ExportExcel" target='_blank' class="fa fa-file-excel-o"><span style="margin-left:10px;"> 导出Excel span>a> li> <li> <a href="#" class="fa fa-file-excel-o" id="printView"><span style="margin-left:10px;"> 打印预览span>a> li> ul> div> div> div> div> <table class="table table-striped table-bordered table-hover table-checkable order-column" id="table_local">table> div> <div class="modal fade" id="defaultModal" tabindex="-1" role="dialog" aria-labelledby="defaultModalLabel" aria-hidden="true"> <div class="modal-dialog divModal" role="document"> <div class="modal-content"> div> div> div> div> div> div> @section scripts { <script src="@Html.ScriptsPath("lib/printThis.js")">script> }
Create视图:
@model Secom.Smp.Data.Models.Customer @{ ViewBag.Title = "Create"; Layout = "~/Views/Shared/_Form.cshtml"; } <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×span>button> <h4 class="modal-title" id="defaultModalLabel">添加用户h4> div> @using (Html.BeginForm("Create", "Default", new { area = "Admin" }, FormMethod.Post, new { @id = "defaultForm" })) { <div class="modal-body"> <div class="row"> <div class="col-md-5"> <div class="form-group"> @Html.LabelFor(x => x.HeadsUrl,new { @class = "control-label" }): <input type="file" id="txt_file" name="txt_file" class="file-loading" accept="image/*" /> @Html.HiddenFor(x=>x.HeadsUrl,new { @id = "hidFileUrl" }) div> div> <div class="col-md-7"> <div class="form-group"> @Html.LabelFor(x => x.Name, new { @class = "control-label" }): @Html.TextBoxFor(x => x.Name, new { @id = "Name", @placeholder = "请输入用户名", @class = "form-control" }) div> <div class="form-group"> @Html.LabelFor(x => x.Address, new { @class = "control-label" }): @Html.TextBoxFor(x => x.Address, new { @id = "Address", @placeholder = "请输入地址", @class = "form-control" }) div> <div class="form-group"> @Html.LabelFor(x => x.CreateTime): <div class="input-group input-medium date date-picker"> @Html.TextBoxFor(x => x.CreateTime, new { @class = "form-control", @readonly = true }) <span class="input-group-btn"> <button class="btn default" type="button"> <i class="fa fa-calendar">i> button> span> div> div> div> div> div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">关闭button> <button type="submit" class="btn sbold green">提交button> div> } @section scripts { }
Update视图:
@model Secom.Smp.Data.Models.Customer @{ ViewBag.Title = "Update"; Layout = "~/Views/Shared/_Form.cshtml"; } <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×span>button> <h4 class="modal-title" id="defaultModalLabel">修改用户h4> div> @using (Html.BeginForm("Update", "Default", new { area = "Admin" }, FormMethod.Post, new { @id = "updateForm" })) { @Html.HiddenFor(x=>x.Id) <div class="modal-body"> <div class="row"> <div class="col-md-5"> <div class="form-group"> @Html.LabelFor(x => x.HeadsUrl, new { @class = "control-label" }): <input type="file" id="txt_file" name="txt_file" class="file-loading" accept="image/*" /> @Html.HiddenFor(x => x.HeadsUrl, new { @id = "hidFileUrl" }) div> div> <div class="col-md-7"> <div class="form-group"> @Html.LabelFor(x => x.Name, new { @class = "control-label" }): @Html.TextBoxFor(x => x.Name, new { @id = "Name", @placeholder = "请输入用户名", @class = "form-control" }) div> <div class="form-group"> @Html.LabelFor(x => x.Address, new { @class = "control-label" }): @Html.TextBoxFor(x => x.Address, new { @id = "Address", @placeholder = "请输入地址", @class = "form-control" }) div> <div class="form-group"> @Html.LabelFor(x => x.CreateTime): <div class="input-group input-medium date date-picker"> @Html.TextBoxFor(model => model.CreateTime, new { @type = "date", @class = "form-control", @readonly = true, @Value = Model.CreateTime.ToDateStr(),@id= "CreateTime" }) <span class="input-group-btn"> <button class="btn default" type="button"> <i class="fa fa-calendar">i> button> span> div> div> div> div> div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">关闭button> <button type="submit" class="btn sbold green">提交button> div> } @section scripts { }
_Form模板页视图
DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>@ViewBag.Titletitle>
head>
<body>
@RenderBody()
@RenderSection("scripts", required: false)
@Html.AutoLoadPageJs()
body>
html>
看下这个自定义的扩展方法AutoLoadPageJs方法,其实就是模拟mvc的路由寻址方式去找指定目录下面的js
////// 根据mvc路由自动加载js文件(如果不存在则不加载) /// /// /// public static MvcHtmlString AutoLoadPageJs(this HtmlHelper helper) { var areas = helper.ViewContext.RouteData.DataTokens["area"]; var action = helper.ViewContext.RouteData.Values["action"]; var controller = helper.ViewContext.RouteData.Values["controller"]; string url = areas == null ? string.Format("views/{0}/{1}", controller, action) : string.Format("views/areas/{2}/{0}/{1}", controller, action, areas); return LoadJsString(helper,url); } /// /// 构造js加载的html字符串 /// /// /// js文件路径 /// public static MvcHtmlString LoadJsString(HtmlHelper helper, string url) { var jsBuilder = new StringBuilder(); string jsLocation = "/content/release-js/"; #if DEBUG jsLocation = "/content/js/"; #endif string jsFullUrl = Path.Combine(jsLocation, url + ".js"); if (File.Exists(helper.ViewContext.HttpContext.Server.MapPath(jsFullUrl))) { jsBuilder.AppendFormat("", jsFullUrl); } return new MvcHtmlString(jsBuilder.ToString()); }
我们看下View对应的js文件
index.js
$(function () { //-------------初始化datatable var obj = DataTablesObj; obj.showReadBtn = false;//显示详情按钮 obj.showDeleteBtn = true;//显示删除按钮 obj.showUpdateBtn = true;//显示更新按钮 obj.updateUrl = "/Admin/Default/Update"; obj.deleteUrl = "/Admin/Default/Delete"; obj.batchDeleteUrl = "/Admin/Default/DeleteList";//批量删除路径 obj.options.columns = [{ title: "", "visible": false, "data": "ID" }, obj.checboxFied, { "data": "Name", title: "用户名称" }, { "data": "Address", title: "用户地址" }, { "data": "CreateTime", title: "创建时间" }, obj.opratorFiled ]; obj.options.searching = true; obj.options.sAjaxSource = "/Admin/Default/List"; //数据源地址 obj.init(obj.options); //表单验证配置项 FormValidatorObj.options.fields = { Name: { message: '用户名验证失败', validators: { notEmpty: { message: '用户名不能为空' } } }, Address: { validators: { notEmpty: { message: '地址不能为空' } } } }; //打印预览 $("#printView").on("click", function () { $("#table_local").printThis({ debug: false,// 调试模式下打印文本的渲染状态 importCSS: true, importStyle: true, printContainer: true, //loadCSS: "/Content/bootstrap.css", pageTitle: "用户列表", removeInline: false, printDelay: 333, header: null, formValues: true, header: "用户列表
", footer: null }); }) })
create.js
$(function () { DatetimepickerObj.init('CreateTime');//(控件ID) //--------------表单验证 FormValidatorObj.init("defaultForm","defaultModal","table_local"); //(表单id,[modal容器Id],[datable容器ID]) //初始化编辑界面的数据 //--------------添加界面中的上传控件 FileInputObj.init(undefined,"txt_file", "hidFileUrl", "/Admin/Default/ExportFile",true); //配置项,控件ID,存储文件路径的控件ID,上传路径,是否新增页面 });
update.js
$(function () { DatetimepickerObj.init('CreateTime');//(控件ID) //--------------表单验证 FormValidatorObj.init("updateForm","defaultModal","table_local"); //(表单id,[modal容器Id],[datable容器ID]) //初始化编辑界面的数据 //([配置项],控件ID,存储文件路径的控件ID,图片路径,上传路径,删除路径) FileInputObj.initUpdateImg(undefined,"txt_file", "hidFileUrl", "/Admin/Default/ExportFile", "/Admin/Default/DeleteFile"); })
看上去比较多的界面功能,你看下,代码就这么点,而且各自职责很清晰。对datatables.js组件进行二次封装,base-Datatable.js代码如下:
// ajax加载调试用 //# sourceURL=base-Datatable.js //DataTables表格组件扩展对象--created by zouqj 2017-7-03 var DataTablesObj = (function () { //------------------------------静态全局属性------------------------------------- var infoStr = "总共 (_PAGES_) 页,显示 _START_ -- _END_ ,共 (_TOTAL_) 条"; var lengthMenuStr = '每页显示:
我们进行二次封装的UI组件对象,他们的options属性,是复杂属性,也就是类似与引用类型,要使用深拷贝然后再去修改,否则修改的是引用而不是副本。我这里没有去使用clone,而是直接在options对象上进行赋值了,那是因为我赋值的那些属性每个引用页面都会去再赋值一遍,而js是运行在客户端的,也就是说每个客户的电脑上面都会有一份完整的js文件副本,这和运行在服务器端的C#语言是不一样的。其实使用 var options = clone(DataTablesObj.options);再去给options赋值是标准做法,但是会失去一部分性能。我的js水平太菜,所以封装得不是特别好,但是至少页面干净、方便维护。
js方法上面的注释,我也是按照自定义的风格进行注释,参数用()括起来,可选参数就用[],这些东西都可以当成约定或者规范,目的就是为了让所有的开发人员写的代码像一个人写的。本来我想把方法的参数都封装成一个对象的,只是之前觉得参数个数少,就没有那样去封装了,参数用对象的好处就是可以无序,而且更易扩展,所有这些进行二次封装的UI组件,文件命名我都加了前缀base-,其实里面的对象命名我是加的后缀Obj,你也可以根据自己的爱好设定,比如说公司名称简写作为前缀,但是一定确定了,就要团队成员遵守约定。
日期组件同样进行封装base-Datetimepicker.js,这里的打印我暂时就没有去进行封装了。在js中通常使用原型链的方式来实现继承,我这里没有使用,原因2点,一是自身对这块掌握得不好,二是公司都是后端开发人员,就按照这种最简单的方式进行封装。
//ajax加载调试用 //# sourceURL=base-Datetimepicker.js //Datetimepicker日期组件扩展对象--created by zouqj 2017-7-13 var DatetimepickerObj = (function () { this.options = { language: 'zh-CN',//显示中文 format: 'yyyy-mm-dd',//显示格式 minView: "month",//设置只显示到月份 initialDate: new Date(),//初始化当前日期 autoclose: true,//选中自动关闭 todayBtn: true//显示今日按钮 }; this.init= function (ctrlId) { $("#" + ctrlId).datetimepicker(DatetimepickerObj.options); } //(开始日期控件,结束日期控件) this.initStartEnd = function (startCtrl, endCtrl) { var startCtrl = $("#" + startCtrl); var endCtrl = $("#" + endCtrl); startCtrl.datetimepicker({ format: 'yyyy-mm-dd', minView: 'month', language: 'zh-CN', autoclose: true, startDate: new Date() }).on("click", function () { startCtrl.datetimepicker("setEndDate", endCtrl.val()) }); endCtrl.datetimepicker({ format: 'yyyy-mm-dd', minView: 'month', language: 'zh-CN', autoclose: true, startDate: new Date() }).on("click", function () { endCtrl.datetimepicker("setStartDate", startCtrl.val()) }); } return this; }).call({});
那么其实我们界面上真正自己要写的就3个View,3个js,一个控制器提供对应的方法调用,其它的不管是C#还是js都去进行封装,css样式通通按照规范写成皮肤文件。项目中可以有一个皮肤css,一个布局css,一个自定义css。
所以说.net做东西就是这么简单,看似复杂的功能界面,一下子就搞定了,只要把东西都封装好了,做一个这样的功能最多大半天就搞定,仔细看下js总共就写几十行左右,还算上了复制粘贴修改下的,哪里需要去经常加班?加班应该是php程序员和java程序员的专利,做不加班的.net程序员~