FEATURED · 精选文章

Asp.Net MVC+Layui企业级系统增删改查实战:从数据库到弹窗全解析

发布时间 / 2026/9/9 21:30:28
来源 / 创域科博编辑部
栏目 / 资讯中心
Asp.Net MVC+Layui企业级系统增删改查实战:从数据库到弹窗全解析 简介一份将ASP.NET MVC与Layui结合起来的增删改查CRUD入门示例主要面向刚接触Web开发的初学者也适合需要系统回顾MVC分层与前端交互的开发者。资源包共687个文件压缩后大小约65.44MB文件类型以dll程序集、cshtml视图页面、cs控制器与模型源码、js与css前端资源为主同时包含config配置、sln解决方案、sqlite数据库等项目依赖文件结构完整便于直接运行和对照学习。目前已有493人学习下载。示例在基础的用户增删改查之外加入了分页、排序、搜索、AJAX异步数据交互、表单校验、异常处理等常见功能并通过Entity Framework简化数据库操作配合Layui的表格、弹出层组件实现界面展示可帮助学习者理清从数据模型、控制器动作到视图渲染的完整开发流程同时关注输入验证与SQL注入等数据安全问题是入门Web全栈开发很实用的参考案例。 做企业级管理系统这些年我发现在国内中小型项目中Asp.Net MVC配合Layui的组合依然非常能打。很多人一开始纠结要不要上前后端分离、要不要用Element Plus结果项目排期一压发现Layui这种“服务端渲染前端组件库”的模式反而是最快出活儿的方案。这篇文章我就拿一个资产管理系统的核心功能——增删改查来做完整拆解从数据库设计、后端Action编写到Layui表格渲染、弹窗表单提交全程带你走一遍顺便把我踩过的坑也一并交代清楚。不管你是有一定基础的.NET开发还是刚接触MVC的新手这篇都适合你照着敲。我尽量少讲虚的全是能直接落地的代码和配置。1. 项目整体设计与选型分析这个项目从需求层面看非常简单维护资产信息包含新增、编辑、删除、查询列表。但越是简单的东西越能看出选型和架构上的门道。1.1 为什么选Asp.Net MVC而不是前后端分离现在的技术圈风向确实偏向Vue/React WebApi但落到实际业务场景如果你的团队本来就没有专门的前端工程师或者项目只需要在内部局域网运行那么MVC这种服务端渲染模式反而更合适。理由有三点开发效率高一个页面从后端到前端全在一个项目里不用维护两套代码仓库不需要处理跨域和Token认证逻辑。学习曲线低新来的同事只要会C#基础拖过WebForm基本一天就能上手MVC。部署成本低发布后直接挂IIS不用装Node环境不用配置Nginx反向代理。Layui的价值在于它不像Bootstrap那样只提供CSS样式而是把表格、分页、弹窗、日期选择这些高频组件全部封装好了直接调用table.render()、layer.open()就能出一个漂亮的后台界面省去大量手写DOM事件的功夫。而且Layui的文档是中文的遇到问题查起来非常快这在国内开发环境里是实打实的优势。1.2 项目分层与职责边界虽然是一个简单的增删改查我还是建议按MVC的分层思想把代码理顺层级职责本项目对应的内容Model层数据结构定义Asset实体类、数据库上下文View层页面展示与用户交互Razor视图 Layui组件Controller层请求路由与业务调度AssetController中的Action方法Service层可选业务逻辑封装资产增删改查的方法封装我见过太多人把所有代码全塞进Controller一个Action写两百行增删改查看着能用但后续加个字段都提心吊胆。这个项目虽然小我还是单独拆了一个AssetService来放数据操作逻辑Controller只负责接收参数和返回JsonResult这样后期扩展统计报表之类的接口时服务层可以直接复用不用动页面代码。2. 环境搭建与数据库准备既然标题写的是Asp.Net MVC那默认你已经装了Visual Studio。我这里用的是VS2019 .NET Framework 4.7.2这套组合在老旧系统和新的WinServer上都能稳定运行。2.1 创建项目并引入Layui打开VS新建项目选择“ASP.NET Web应用程序(.NET Framework)”模板选“MVC”身份验证这里可以直接选“不进行身份验证”因为我们后面通过Controller控制访问权限。然后去Layui官网下载最新版的压缩包解压后把layui文件夹整个拷到项目的Scripts目录下。我建议用本地文件别用CDN。原因很现实内网系统经常没有外网权限你线上CDN加载速度可能不错但客户的内网环境基本访问不了外网。引用方式在布局页_Layout.cshtml里加上link href~/Scripts/layui/css/layui.css relstylesheet / script src~/Scripts/layui/layui.js/script注意Layui的模块化加载机制页面里通过layui.use([table, layer, form], function () { ... })来加载需要的模块。不是一次性把整个库暴露到全局这也让它在老浏览器上不会爆出一堆兼容性错误。2.2 创建资产表与实体模型数据库我直接用SQL Server表结构按最常规的方式设计CREATE TABLE [dbo].[Asset] ( [Id] INT IDENTITY (1, 1) NOT NULL, [Name] NVARCHAR (50) NOT NULL, [Category] NVARCHAR (20) NULL, [Status] INT DEFAULT ((0)) NOT NULL, [PurchaseDate] DATETIME NULL, [Remark] NVARCHAR (200) NULL, PRIMARY KEY CLUSTERED ([Id] ASC) );字段含义很直白Name资产名称Category分类办公设备、IT耗材、家具等Status状态0表示在库、1表示已领用、2表示维修中PurchaseDate购入日期Remark备注。对应地在项目里建一个实体类然后我用Entity Framework的Database First模式直接通过VS的“添加→ADO.NET实体数据模型”从数据库反向生成DbContext。如果你不熟悉EF直接用SqlConnection写SQL也行但EF的语法简洁且自带参数化查询能有效防止注入我还是推荐用它。3. 查询功能Layui Table与后端接口对接查询是列表页的命脉也是Layui表格组件最核心的使用场景。页面加载时向服务器发请求拉取数据前端做分页展示。3.1 后端分页接口Layui的table组件在请求时会自动带上两个关键参数page当前页码limit每页条数。所以后端接收参数很固定public ActionResult Page(string keyword, int page 1, int limit 10) { var service new AssetService(); var list service.GetPageList(keyword, page, limit); var count service.GetCount(keyword); return Json(new { code 0, msg , count, data list }, JsonRequestBehavior.AllowGet); }这里有个新人最容易踩的坑MVC的Json()方法默认不允许GET请求返回JSON必须显式加上JsonRequestBehavior.AllowGet否则浏览器控制台会报“此请求已被阻止”。Layui Table默认就是用AJAX的GET方式拉数据的所以这个参数不能省。服务层里的分页查询也很常规注意EF的IQueryable延迟执行的特性public ListAsset GetPageList(string keyword, int page, int limit) { using (var db new AssetDbContext()) { var query db.Assets.AsQueryable(); if (!string.IsNullOrEmpty(keyword)) query query.Where(a a.Name.Contains(keyword)); return query.OrderByDescending(a a.Id) .Skip((page - 1) * limit) .Take(limit) .ToList(); } }AsQueryable()是懒加载查询的关键它允许我们先拼接Where条件再在真正ToList()时才生成SQL去数据库执行。3.2 前端表格渲染前端页面里我建一个Asset/Index.cshtml视图里面只放一个table idassetTable/table然后在script里做渲染layui.use([table, layer, form], function () { var table layui.table; table.render({ elem: #assetTable, url: /Asset/Page, page: true, cols: [[ { field: Id, title: 编号, width: 80, sort: true }, { field: Name, title: 资产名称, minWidth: 120 }, { field: Category, title: 分类, width: 100 }, { field: Status, title: 状态, width: 100, templet: #statusTpl }, { field: PurchaseDate, title: 购入日期, width: 120 }, { fixed: right, title: 操作, toolbar: #rowBar, width: 180 } ]], parseData: function (res) { return { code: res.code, msg: res.msg, count: res.count, data: res.data }; } }); });注意两点一是Layui对接口返回格式有约定必须是{code: 0, msg: , count: 100, data: []}这种结构缺一个字段表格都渲染不出来。如果你的返回格式对不上可以在parseData里做转换兜底。二是状态字段的显示。数据库里存的是0和1页面上直接显示数字非常难看。我用了templet模板在上面的html里定义script typetext/html idstatusTpl {{# if (d.Status 0) { }} span stylecolor:green;在库/span {{# } else if (d.Status 1) { }} span stylecolor:orange;已领用/span {{# } else { }} span stylecolor:red;维修中/span {{# } }} /scriptLayui的模板语法是{{# ... }}里面跑JavaScript用起来非常灵活比在Controller里拼字符串再返回优雅得多。4. 新增与编辑功能的完整实现增和改在本质上是一件事向服务器提交一组字段唯一区别是主键Id是否存在。所以我习惯把这两个操作合并成一个Save接口来实现前端弹窗共用同一个表单。4.1 弹窗如何承载表单Layui的layer.open弹窗支持两种常见方式一种是用type: 2加载一个iframe子页面另一种是type: 1直接把页面上的DOM作为弹窗内容。我推荐后者因为不用多写一个完整视图传值和回填都简单。在Index.cshtml里用script标签包住表单模板设置display:none防止页面加载时就显示出来script typetext/html ideditFormWrap form classlayui-form lay-filterassetForm stylepadding: 20px; input typehidden nameId ideditId / div classlayui-form-item label classlayui-form-label资产名称/label div classlayui-input-block input typetext nameName required lay-verifyrequired placeholder请输入名称 classlayui-input / /div /div div classlayui-form-item label classlayui-form-label分类/label div classlayui-input-block input typetext nameCategory classlayui-input / /div /div div classlayui-form-item label classlayui-form-label状态/label div classlayui-input-block select nameStatus option value0在库/option option value1已领用/option option value2维修中/option /select /div /div div classlayui-form-item label classlayui-form-label购入日期/label div classlayui-input-block input typetext namePurchaseDate idpurchaseDate classlayui-input / /div /div div classlayui-form-item label classlayui-form-label备注/label div classlayui-input-block textarea nameRemark classlayui-textarea/textarea /div /div div classlayui-form-item styletext-align:center; button classlayui-btn lay-submit lay-filterassetSubmit保存/button /div /form /script操作列上加两个按钮通过layui-event绑定点击script typetext/html idrowBar a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdel删除/a /script表格的table.on(tool(assetTable))事件里根据obj.event来判断是编辑还是删除。4.2 新增和编辑的前端处理逻辑打开弹窗的逻辑比较简单新增时直接弹窗编辑时先用ajax查详情回填到表单里。关键代码如下var active { add: function () { $(#editId).val(0); $(#editFormWrap form)[0].reset(); // 清空表单 layer.open({ type: 1, title: 新增资产, area: [680px, 520px], content: $(#editFormWrap).html(), success: function (layero) { layui.form.render(); // 重新渲染下拉框等 layui.laydate.render({ elem: layero.find(#purchaseDate)[0] }); } }); }, edit: function (obj) { layer.open({ type: 1, title: 编辑资产, area: [680px, 520px], content: $(#editFormWrap).html(), success: function (layero) { var form layui.form; $.get(/Asset/Detail, { id: obj.data.Id }, function (res) { form.val(assetForm, res.data); // 批量赋值 layui.laydate.render({ elem: layero.find(#purchaseDate)[0] }); }); } }); } };这里有个非常重要的点$(#editFormWrap).html()拿到的是静态HTML字符串里面的select和日期控件不会自动变成Layui样式必须在layer.open的success回调里用form.render()和laydate.render()重新初始化。我一开始没加这步结果下拉框显示为原生样式且无法联动查了半天。这就是Layui开发的一个核心思维任何动态插入到页面的DOM只要涉及表单控件都得重新render否则样式和事件都不生效。4.3 后端保存接口在前端表单提交事件里拦截并转为AJAX请求form.on(submit(assetSubmit), function (data) { var field data.field; field.PurchaseDate field.PurchaseDate ? new Date(field.PurchaseDate) : null; $.ajax({ url: /Asset/Save, type: POST, data: JSON.stringify(field), contentType: application/json, dataType: json, success: function (res) { if (res.code 0) { layer.closeAll(page); table.reload(assetTable); layer.msg(保存成功); } else { layer.msg(res.msg); } } }); return false; // 阻止表单默认提交 });后端的Save方法判断Id是否为0决定新增还是修改[HttpPost] public ActionResult Save(Asset model) { if (ModelState.IsValid false) return Json(new { code 1, msg 数据校验不通过 }); var service new AssetService(); if (model.Id 0) service.Insert(model); else service.Update(model); return Json(new { code 0, msg 保存成功 }); }这里解释一下为什么推荐用JSON.stringify提交而不是传统的$.post。因为MVC的模型绑定器对JSON反序列化比较智能遇到DateTime、子对象这类复杂类型时JSON方式不易出错。如果你的项目对命名风格有要求比如字段是驼峰命名MVC端配置一下JSON.Net的序列化设置就能无缝绑定。4.4 编辑时的时间回填问题编辑回填的时候很多人会栽在日期格式上。EF把数据库的datetime读出来是DateTime类型直接赋给input显示是2024-01-15T10:30:00这样的ISO格式Layui的日期控件根本认不出它。我推荐在后端返回时就把日期格式化为字符串public ActionResult Detail(int id) { var model assetService.GetById(id); var data new { Id model.Id, Name model.Name, Category model.Category, Status model.Status, PurchaseDate model.PurchaseDate?.ToString(yyyy-MM-dd), Remark model.Remark }; return Json(new { code 0, data }, JsonRequestBehavior.AllowGet); }这样前端form.val()赋值的时候日期框直接显示2024-01-15不会出现格式错乱。5. 删除功能与状态管理的细节删除操作很简单但我也要详细说说因为它牵涉到“物理删除还是逻辑删除”的取舍。5.1 后端删除接口后端提供两个方案[HttpPost] public ActionResult Delete(int id) { assetService.Delete(id); // 物理删除 return Json(new { code 0 }); } [HttpPost] public ActionResult SoftDelete(int id) { assetService.SoftDelete(id); // 逻辑删除把Status改成某个特殊值 return Json(new { code 0 }); }实际开发中我强烈建议使用逻辑删除。资产管理这类系统审计追踪很重要你不希望谁手抖删了一条资产记录就彻底找不回来了。逻辑删除的实现就是在表里加一个IsDeleted标记查询时统一过滤掉。前端删除的交互用layer.confirm做一个二次确认table.on(tool(assetTable), function (obj) { if (obj.event del) { layer.confirm(确定删除该资产吗, { icon: 3, title: 提示 }, function (index) { $.post(/Asset/Delete, { id: obj.data.Id }, function (res) { if (res.code 0) { layer.close(index); table.reload(assetTable); } }); }); } });5.2 批量删除的实现思路另一个高频需求是批量删除。实现思路是给表格加一列checkbox然后获取选中行的数据// 列配置里加一行 { type: checkbox, width: 50 } // 批量删除按钮 $(#btnBatchDelete).click(function () { var checkStatus table.checkStatus(assetTable); var ids checkStatus.data.map(function (item) { return item.Id; }); if (ids.length 0) { layer.msg(请先勾选要删除的记录); return; } $.ajax({ url: /Asset/BatchDelete, type: POST, data: { ids: ids }, traditional: true, success: function (res) { table.reload(assetTable); } }); });很多人的误区是以为table.checkStatus返回的是当前页选中的数据。实际上Layui会记住跨页勾选的状态前提是你在table.render时开启了page分页并且每一行的data里包含唯一主键Id它才能正确追踪。这个机制很好用。后端接收[HttpPost] public ActionResult BatchDelete(Listint ids) { assetService.BatchDelete(ids); return Json(new { code 0 }); }MVC模型绑定器会自动把ids[]或逗号拼接的字符串映射到Listint参数上不用自己Split。6. 常见问题与排查技巧实录做这类MVCLayui增删改查项目我总结了一些高频故障和相应的处理办法列成一份速查表希望对你有帮助。6.1 表格数据加载不出来最常见的三个原因接口没有返回Layui约定的格式比如缺少count字段表格就显示不出分页。打开浏览器F12直接看响应体一眼就能定位。Controller返回的Json被全局过滤器拦截了。如果项目里配置了登录认证未通过认证时请求会被重定向到登录页此时接口返回的实际是HTML表格自然渲染失败。这种情况要检查Global.asax里的过滤器或者改在Web.config里配置匿名访问。接口报500错误。接口代码里写了db.Dispose()之类或者用了using之后又访问了实体对象都会报错。建议在Global.asax里注册一个Application_Error事件把异常写到日志排查效率高很多。6.2 日期控件日历面板显示位置偏移Layui的日期选择器默认渲染在触发元素的父级position: relative容器里如果你的弹窗容器设置了overflow: hidden日历面板就会被裁掉或者位置错乱。解决办法有三给弹窗内容容器加上styleoverflow: visible;给laydate的position参数设成fixed把弹窗的area宽度调大给日历面板留出空间我试下来最省事的是设position: fixed它让日历面板脱离弹窗的层叠上下文基本不会再被裁切。6.3 时间类型序列化问题默认的JavaScriptSerializer会把DateTime序列化成/Date(1700000000000)/这样一段东西前端解析很麻烦。推荐的方案是将Json序列化器改成Newtonsoft.Json在Global.asax里配置GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings new JsonSerializerSettings { DateFormatString yyyy-MM-dd HH:mm:ss };或者在实体对应属性上标注[JsonConverter(typeof(DateFormatConverter))]哪种都行。我习惯用第一种全局生效省得每个属性都加特性。6.4 表单提交时提示“防伪令牌”错误MVC默认在表单里生成AntiforgeryToken如果我们用AJAX提交这个Token不会自动带上于是服务端校验直接失败。解决方法就是在页面加载时把Token放到请求头$.ajaxSetup({ headers: { RequestVerificationToken: $(input[name__RequestVerificationToken]).val() } });另一个思路是直接在Save相关的Action上标注[ValidateAntiForgeryToken]如果不放Token就返回403这样其实更安全但排查问题时更容易让新人懵。我建议在项目初期先不加这个特性的验证等所有增删改查跑通了再统一加上免得调试时多一层干扰。6.5 刷新表格后停留在第一页table.reload(assetTable)默认回到第一页这往往不是用户想要的。比如我在第5页编辑了一条记录保存后应该刷新并还留在第5页。要记得读取当前页码再传回去var page table.reload(assetTable, { page: { curr: table.cache.assetTable.page } });table.cache里保存着当前表格的页码、排序信息用这种方式刷新体验好很多。6.6 工具栏按钮事件不触发遇到table.on(tool(assetTable))不执行的情况先确认tool事件绑定的表格ID是否和table.render里的elem一致。另外如果操作列是通过toolbar指定外部模板ID比如toolbar: #rowBar模板里的lay-event属性必须写对。有一次我把lay-eventedit写成了lay-eventEdit大小写不一致事件死活用不了。还有一个小众注意点Layui官方在2.8版本之后部分模块的引入方式有变化老版本的项目直接升级后可能初始化报错。如果项目已经稳定不建议频繁升级Layui版本踩过的坑没必要再踩一遍。7. 关于这个项目我最后想分享的一点经验整个资产管理系统的增删改查逻辑上并不复杂核心代码加起来几百行。但如果你真的从头到尾把这块写完跑通你会理解MVC的请求管道是怎么运作的Layui的组件是如何通过JSON和服务器通信的以及一个合格的后台管理页面应该注意哪些交互细节。我实际开发中最大的体会是别小看这种看似“简单”的CRUD功能很多系统上线后出问题恰恰是列表加载慢、批量操作丢失、日期格式错乱这类细节没有处理好。把增删改查这块地基打扎实后续你在上面加报表、加导入导出、加权限控制都会顺手很多。最后再送你一个小技巧开发过程中打开浏览器的F12Network面板里多看看请求的Payload和Response特别是前后端字段名不一致的问题比对一下Payload里的JSON和Controller接收的模型参数往往一眼就能找到原因。这个习惯比任何调试工具都管用也是排查前后端联调问题最高效的路径。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻