news 2026/9/9 21:30:24

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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" rel="stylesheet" /> <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 List<Asset> 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 id="assetTable"></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 type="text/html" id="statusTpl"> {{# if (d.Status === 0) { }} <span style="color:green;">在库</span> {{# } else if (d.Status === 1) { }} <span style="color:orange;">已领用</span> {{# } else { }} <span style="color:red;">维修中</span> {{# } }} </script>

Layui的模板语法是{{# ... }}里面跑JavaScript,用起来非常灵活,比在Controller里拼字符串再返回优雅得多。

4. 新增与编辑功能的完整实现

增和改在本质上是一件事:向服务器提交一组字段,唯一区别是主键Id是否存在。所以我习惯把这两个操作合并成一个Save接口来实现,前端弹窗共用同一个表单。

4.1 弹窗如何承载表单

Layui的layer.open弹窗支持两种常见方式:一种是用type: 2加载一个iframe子页面,另一种是type: 1直接把页面上的DOM作为弹窗内容。我推荐后者,因为不用多写一个完整视图,传值和回填都简单。

Index.cshtml里用script标签包住表单模板,设置display:none防止页面加载时就显示出来:

<script type="text/html" id="editFormWrap"> <form class="layui-form" lay-filter="assetForm" style="padding: 20px;"> <input type="hidden" name="Id" id="editId" /> <div class="layui-form-item"> <label class="layui-form-label">资产名称</label> <div class="layui-input-block"> <input type="text" name="Name" required lay-verify="required" placeholder="请输入名称" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">分类</label> <div class="layui-input-block"> <input type="text" name="Category" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">状态</label> <div class="layui-input-block"> <select name="Status"> <option value="0">在库</option> <option value="1">已领用</option> <option value="2">维修中</option> </select> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">购入日期</label> <div class="layui-input-block"> <input type="text" name="PurchaseDate" id="purchaseDate" class="layui-input" /> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">备注</label> <div class="layui-input-block"> <textarea name="Remark" class="layui-textarea"></textarea> </div> </div> <div class="layui-form-item" style="text-align:center;"> <button class="layui-btn" lay-submit lay-filter="assetSubmit">保存</button> </div> </form> </script>

操作列上加两个按钮,通过layui-event绑定点击:

<script type="text/html" id="rowBar"> <a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a> <a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</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.opensuccess回调里用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(List<int> ids) { assetService.BatchDelete(ids); return Json(new { code = 0 }); }

MVC模型绑定器会自动把ids[]或逗号拼接的字符串映射到List<int>参数上,不用自己Split。

6. 常见问题与排查技巧实录

做这类MVC+Layui增删改查项目,我总结了一些高频故障和相应的处理办法,列成一份速查表,希望对你有帮助。

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,日历面板就会被裁掉或者位置错乱。解决办法有三:

  • 给弹窗内容容器加上style="overflow: 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-event="edit"写成了lay-event="Edit",大小写不一致,事件死活用不了。

还有一个小众注意点:Layui官方在2.8版本之后,部分模块的引入方式有变化,老版本的项目直接升级后可能初始化报错。如果项目已经稳定,不建议频繁升级Layui版本,踩过的坑没必要再踩一遍。

7. 关于这个项目,我最后想分享的一点经验

整个资产管理系统的增删改查,逻辑上并不复杂,核心代码加起来几百行。但如果你真的从头到尾把这块写完跑通,你会理解MVC的请求管道是怎么运作的,Layui的组件是如何通过JSON和服务器通信的,以及一个合格的后台管理页面应该注意哪些交互细节。

我实际开发中最大的体会是:别小看这种看似“简单”的CRUD功能,很多系统上线后出问题,恰恰是列表加载慢、批量操作丢失、日期格式错乱这类细节没有处理好。把增删改查这块地基打扎实,后续你在上面加报表、加导入导出、加权限控制,都会顺手很多。

最后再送你一个小技巧:开发过程中打开浏览器的F12,Network面板里多看看请求的Payload和Response,特别是前后端字段名不一致的问题,比对一下Payload里的JSON和Controller接收的模型参数,往往一眼就能找到原因。这个习惯比任何调试工具都管用,也是排查前后端联调问题最高效的路径。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 21:29:19

2026年IDE智能体革命:从辅助编程到代理编程的实战指南

这两年&#xff0c;我明显感觉到IDE战场上的枪口方向变了。2024年大家还在比谁的补全更顺滑&#xff0c;2025年比的是谁能帮你把多文件改动一口气做完&#xff0c;而到了2026年&#xff0c;核心竞争点已经变成“智能体编程”——也就是让IDE里的Agent从一个只会“接话”的助手&…

作者头像 李华
网站建设 2026/9/9 21:27:09

Telegraf:用一个TOML配置快速跑通指标采集全链路

Telegraf&#xff1a;用一个TOML配置快速跑通指标采集全链路 【免费下载链接】telegraf Agent for collecting, processing, aggregating, and writing metrics, logs, and other arbitrary data. 项目地址: https://gitcode.com/GitHub_Trending/te/telegraf 十几台服务…

作者头像 李华
网站建设 2026/9/9 21:25:58

ibaAnalyzer V6.3.1工业数据分析实操指南:从波形分析到故障诊断

简介&#xff1a;这是一款面向工业数据分析和IT专业人员的专业工具&#xff0c;专门用于解析和可视化ibapda生成的dat文件&#xff0c;解决海量原始数据难以理解和处理的问题。压缩包共25个文件&#xff0c;约40MB&#xff0c;核心包括Windows安装程序&#xff08;.exe&#xf…

作者头像 李华
网站建设 2026/9/9 21:23:37

Pytest二次开发核心指南:从Hook机制到框架封装

做测试的人&#xff0c;几乎都绕不开 Pytest。但“怎么才能算对 Pytest 做二次开发”这个问题&#xff0c;我在社区里、面试中、团队内部都见过太多误解了。有人写了几个 fixture 封装一下接口请求&#xff0c;就说自己做了二次开发&#xff1b;有人把项目的公共方法整理到一个…

作者头像 李华
网站建设 2026/9/9 21:23:17

STM32 USART3 DMA收发详解:通道配置、发送判断与不定长接收

简介&#xff1a;面向嵌入式开发者的STM32F103标准库工程&#xff0c;专注于利用DMA方式实现USART3的接收与发送&#xff0c;解决传统串口中断频繁占用CPU的问题&#xff0c;适用于工业通信、数据采集等实时性较高的场景。压缩包共171个文件、约3.41MB&#xff0c;包含C源码、H…

作者头像 李华