资讯详情

资讯详情

C# + EasyUI 后台管理:增删改查模板与避坑指南

简介面向 C# 与前端开发者的 EasyUI 后台管理示例工程基于 VS2013、MVC 与 SQL Server 2014演示单表数据管理完整流程。功能覆盖新增、修改、删除、分页、导出 Excel 与图片上传核心是 UserInfoDAL.cs 中的分页实现默认使用 SQL Server 2012 关键字分页若数据库为 2005/2008 可切换 getPage2005 方法便于读者对照不同数据库版本的分页差异。资源为 RAR 压缩包包体约 28.54MB页面标注文件总数为 0文件类型明细暂未公开从功能描述看内部应包含可编译的 MVC 项目源码、数据库脚本及示例页面。目前已有 218 人学习下载适合正在学习 C# EasyUI 整合开发、需要参考增删改查与导出上传实践的初中级开发者按示例稍作改造即可接入自身项目。1. C# EasyUI 后台管理一个能直接抄的增删改查模板很多人一听 EasyUI 觉得过时了但在企业内部管理系统这个场景里它是少有的“不需要前端工程化、打开即用”的方案。最近我拆了一个用 C# EasyUI 写的后台管理项目麻雀虽小但新增、修改、删除、导出 Excel、文件上传这些高频操作全都齐了而且没有用任何重量级框架后端就是 ASP.NET 的一般处理程序前端就是 EasyUI 的 datagrid 和 dialog。如果你的场景正好是快速交付的内部系统——进销存、订单登记、工单管理——这套写法比折腾 Vue Element Admin 要快得多后面改起来也直观。这篇文章我会从工程结构拆到核心代码再讲几个网上经常语焉不详、但实际必踩的坑。2. 架构与布局EasyUI 前端与 C# 后端的工程骨架先说结论EasyUI 项目最容易翻车的点不是功能写不出来而是页面的布局、弹窗和表格之间的关系没理清。我拆的这个项目是一个典型的订单管理后台浏览器打开后是一个左侧菜单、顶部标题、中间 TAB 页的标准布局所有业务页面都通过 TAB 加载进 iframe。后端接口全部走 .ashx 一般处理程序返回 JSON 字符串前端 datagrid 拿到后自动渲染表格。整体没有用 MVC也没有前后端分离放在今天看很“复古”但胜在部署简单——一个网站目录丢到 IIS 上就能跑。2.1 技术选型为什么 EasyUI 还在被大量内部系统使用这个项目的技术栈非常老派ASP.NET WebForm 时代的一般处理程序ashx、ADO.NET 手写数据访问、前端 jQuery EasyUI。我最开始觉得这是历史包袱但看完代码后意识到对于内部管理系统来说EasyUI 的“所见即所得”确实有不可替代的优势datagrid 自带分页、排序、行选择dialog 自带拖拽和遮罩tree 自带级联展开这些在 Element UI 里要写一堆配置的东西EasyUI 一个 class 属性就搞定而且不需要 node_modules。常见做法是后端保持“返回 JSON 就好”的形态前端用 EasyUI 的组件去消费。这个项目里所有列表页的返回格式都遵循同一个约定{ total: 120, rows: [ { OrderID: 1, OrderNo: SO2024001 } ] }只有遵守这个结构datagrid 才能直接绑定数据。其实 EasyUI 不关心后端是 ashx、WebService 还是 WebAPI它只认这个 JSON 形状。我一般会把这段逻辑抽成一个公共方法所有列表接口共用避免每个页面写一遍分页代码。后端返回的时候还要注意字段名大小写JavaScript 对大小写敏感OrderID和orderId是两回事。2.2 目录结构与页面骨架一个最小后台长什么样项目的目录结构不复杂但分层很清楚业务代码集中在三个地方/OrderManager /ashx /OrderHandler.ashx // 订单模块统一入口 /views /order.aspx // 订单列表页面 /orderEdit.aspx // 订单新增/编辑弹窗页面或纯 div /js /order.js // 页面交互逻辑 /css /Scripts /jquery.min.js /jquery.easyui.min.js /App_Code /DbHelper.cs // 数据库连接辅助类 /OrderDal.cs // 订单数据访问层 /Upload // 上传文件保存目录 /index.html // 后台主框架主框架用的是 EasyUI 的 layout 布局代码很短但它是整个后台的地基body classeasyui-layout div>$(#menuTree).tree({ onClick: function (node) { var url node.attributes ? node.attributes.url : ; if (!url) return; $(#mainTabs).tabs(add, { title: node.text, href: url, closable: true }); } });这里有个参数问题要注意tabs(add)的href是把远程页面直接渲染进当前 DOMiframe 方式则会让子页面里的easyui-datagrid宽度计算异常。实际项目中我更喜欢在 TAB 里加一个 iframe让每个页面保持独立的脚本上下文刷新、关闭、重新打开都不会互相污染变量。这个选择直接影响后面所有页面的写法建议先想清楚再动手。3. 核心模块拆解一个订单从新增到导出的完整链路这个项目的核心价值就在这一章增删改查、导出 Excel、上传文件这五个功能几乎可以原样搬到任何一张业务表上。我按一条订单数据从录入到导出的完整路径来讲每个环节都给出关键代码。3.1 后端统一入口OrderHandler 怎么分发 action一般处理程序是 C# 后端最轻的入口所有操作通过action参数区分public class OrderHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType application/json; context.Response.Charset utf-8; string action context.Request[action]; string result ; switch (action) { case list: result GetList(context); break; case add: result Save(context, isUpdate: false); break; case update: result Save(context, isUpdate: true); break; case delete: result Delete(context); break; case export: ExportExcel(context); return; // 导出返回的是文件流不走 JSON case upload: result Upload(context); break; default: result {\success\:false,\msg\:\未知操作\}; break; } context.Response.Write(result); } public bool IsReusable { get { return false; } } }逻辑说明这里的ProcessRequest是所有请求的网关。export单独处理因为它要Response.End()输出文件流如果走统一的 JSON 返回会破坏文件内容。upload虽然也返回 JSON但本质上是在处理 multipart/form-data所以它接收的不是普通表单字段而是文件流。参数说明action是每次请求必须带的参数add和update共用Save方法靠isUpdate决定是执行 INSERT 还是 UPDATE这样可以保证新增和编辑对字段的处理逻辑完全一致。很多新手在新增和编辑里各写一套赋值代码后面加了字段要改两处非常容易漏。3.2 新增与修改dialog 表单回填与保存列表页的工具栏放了“新增”“编辑”“删除”“导出”“上传附件”五个按钮。新增和编辑共用同一个 dialog区别只在打开时是否回填数据function openEdit(title, row) { $(#dlg).dialog(open).dialog(setTitle, title); if (row) { // row 来自 datagrid 当前选中行 $(#fm).form(load, row); } else { $(#fm).form(clear); } } function saveOrder() { $(#fm).form(submit, { url: ashx/OrderHandler.ashx?action ($(#OrderID).val() ? update : add), onSubmit: function () { return $(this).form(validate); }, success: function (data) { var r JSON.parse(data); if (r.success) { $(#dlg).dialog(close); $(#dg).datagrid(reload); } else { alert(r.msg); } } }); }逻辑说明$(#fm).form(load, row)是 EasyUI 的关键方法它会自动把 row 对象里同名字段填进表单所以后端返回的字段名必须和表单控件的 name 完全一致。保存时通过OrderID是否有值来判断是新增还是修改避免两个按钮两套逻辑。form(validate)会触发表单里所有 easyui-textbox 的 required 校验不通过就不会提交。后端Save方法对应的核心逻辑是参数接收和实体赋值private string Save(HttpContext context, bool isUpdate) { string orderNo context.Request[OrderNo]; string customer context.Request[Customer]; decimal amount Convert.ToDecimal(context.Request[Amount]); int result isUpdate ? OrderDal.Update(orderNo, customer, amount) : OrderDal.Insert(orderNo, customer, amount); return {\success\: (result 0) ,\msg\:\保存成功\}; }这里有个容易被忽略的点context.Request[OrderNo]拿到的值如果没填会是空字符串不会为 null。所以在后端校验必填项时要判断string.IsNullOrEmpty而不是 null否则空表单提交时校验形同虚设。3.3 删除单条与批量删除的请求拼接删除操作看似简单但批量删除的传参方式最容易翻车。前端选中多行后把主键拼成逗号分隔的字符串传给后端function deleteOrders() { var rows $(#dg).datagrid(getSelections); if (rows.length 0) { alert(请先选择要删除的记录); return; } var ids rows.map(function (r) { return r.OrderID; }).join(,); $.post(ashx/OrderHandler.ashx, { action: delete, ids: ids }, function (data) { var r JSON.parse(data); if (r.success) { $(#dg).datagrid(reload); } else { alert(r.msg); } }); }后端接收时不能直接把 ids 拼进 SQL这是血泪经验。我见过太多项目因为这里偷懒导致删一条数据把整张表清空。常见的稳健做法是先用正则卡死输入范围private string Delete(HttpContext context) { string ids context.Request[ids]; if (string.IsNullOrEmpty(ids) || !Regex.IsMatch(ids, ^[0-9,]$)) { return {\success\:false,\msg\:\参数不合法\}; } int result OrderDal.Delete(ids); return {\success\: (result 0) ,\msg\:\删除成功\}; }参数说明正则^[0-9,]$限定了 ids 只能是数字和逗号这意味着即使有人把异常参数传进来也不会进入 SQL 拼接。在 DAL 层删除时仍然建议用参数化写法把 ids 作为整体传进去配合数据库层面的外键约束双保险。如果业务要求只能删一条前端就不应该把getSelections的代码写进去直接取第一行即可。3.4 导出 ExcelNPOI 的引入与导出流程导出是这个项目里最实在的功能用的常见方案是 NPOI因为它不需要服务器装 Office也不依赖 COM 组件。核心流程分三步查数据、写工作簿、输出响应流。private void ExportExcel(HttpContext context) { var list OrderDal.GetAllForExport(); // 取出全量或筛选后的数据 HSSFWorkbook workbook new HSSFWorkbook(); ISheet sheet workbook.CreateSheet(订单列表); string[] headers { 订单号, 客户名称, 金额, 创建时间 }; IRow headerRow sheet.CreateRow(0); for (int i 0; i headers.Length; i) { headerRow.CreateCell(i).SetCellValue(headers[i]); } int rowIndex 1; foreach (var item in list) { IRow row sheet.CreateRow(rowIndex); row.CreateCell(0).SetCellValue(item.OrderNo); row.CreateCell(1).SetCellValue(item.Customer); row.CreateCell(2).SetCellValue(Convert.ToDouble(item.Amount)); row.CreateCell(3).SetCellValue(item.CreateTime.ToString(yyyy-MM-dd HH:mm:ss)); } context.Response.Clear(); context.Response.ContentType application/vnd.ms-excel; context.Response.AddHeader(Content-Disposition, attachment;filename HttpUtility.UrlEncode(订单导出.xls)); workbook.Write(context.Response.OutputStream); context.Response.End(); }逻辑说明导出列表的列顺序最好和页面上 datagrid 的列保持一致这样用户对导出的文件结构有预期不会拿到手还要猜哪列是什么。Content-Disposition里的文件名必须用UrlEncode包一层否则中文文件名在部分浏览器里会变成乱码或者干脆下载失败这一点在后面的避坑章节会详细说。金额用Convert.ToDouble而不是直接存字符串是因为 Excel 里数字列如果以字符串写入后续求和、排序都会遇到问题。这个导出方法还承担了一个隐藏职责它是整个项目的数据质量检测器。如果一个表格能正常显示但导出报错多半是字段类型在绑定和写入之间发生了不一致这时候排查顺序建议先看实体类的属性类型再看 SQL 查询里的 CAST 和 CONVERT。3.5 上传文件异步上传到服务器并回填路径上传功能在这个项目里是给订单挂附件用的比如合同扫描件、对账单。前端用的是 EasyUI 的 filebox但实际提交不是走 EasyUI 自带的 upload 控件而是直接通过隐藏表单提交到 ashx。我的做法是用一个普通 form 包裹 filebox触发onChange后立即提交form iduploadForm enctypemultipart/form-data methodpost input idfileBox namefile classeasyui-filebox >private string Upload(HttpContext context) { HttpFileCollection files context.Request.Files; if (files.Count 0) { return {\success\:false,\msg\:\没有接收到文件\}; } HttpPostedFile file files[0]; string ext Path.GetExtension(file.FileName).ToLower(); // 只允许白名单内的扩展名防止上传恶意文件 string[] allowed { .jpg, .png, .pdf, .xls, .xlsx, .doc, .docx }; if (!allowed.Contains(ext)) { return {\success\:false,\msg\:\不允许的文件类型\}; } string dir /Upload/ DateTime.Now.ToString(yyyyMM); string physicalPath context.Server.MapPath(dir); if (!Directory.Exists(physicalPath)) Directory.CreateDirectory(physicalPath); string newName DateTime.Now.ToString(yyyyMMddHHmmss) _ Guid.NewGuid().ToString(N).Substring(0, 8) ext; file.SaveAs(Path.Combine(physicalPath, newName)); string url dir / newName; return {\success\:true,\url\:\ url \}; }逻辑说明文件名的重命名很关键不能直接用原始文件名保存否则同名的文件会把之前的覆盖掉。我用时间戳加随机串保证唯一性同时保留了扩展名。按月份建目录是为了避免单个文件夹文件过多。参数说明file.SaveAs的参数必须是服务器物理路径所以要先MapPath把虚拟路径转成物理路径。前端拿到返回的url后一般会把它写进一个隐藏域等保存订单时一起提交到后端。还有一种做法是上传后直接把附件信息写到附件表订单主表只存关联 ID这个项目用的是前者——URL 存在订单表的AttachmentUrl字段里简单直接。4. 避坑与排查C# EasyUI 项目里的 4 个高频问题这个部分的内容来自我实际跑这个项目时踩过的坑有些是 EasyUI 的“黑匣子”行为有些是 C# 后端和浏览器配合的经典问题。按“现象 → 原因 → 解决”梳理出来给看到此文的人一个后悔药。4.1 datagrid 时间字段显示成 /Date(1717...)/ 格式现象列表页能正常加载但凡是日期字段都显示成类似/Date(1717353600000)/的字符串用户看着完全不知道是什么时间。原因后端用 JavaScriptSerializer 或部分 JSON 序列化组件对 DateTime 序列化时会产生这种微软特有的日期格式。EasyUI 的 datagrid 不会自动识别它直接当普通文本渲染。解决有两种常用办法。第一种是在后端序列化前把所有 DateTime 字段统一转成字符串比如item.CreateTime.ToString(yyyy-MM-dd HH:mm:ss)这也是这个项目采用的方式。第二种是在前端列配置里写 formatter 处理{ field: CreateTime, title: 创建时间, formatter: function (value) { if (!value) return ; return value.replace(/\/Date\((\d)\)\//, function (_, t) { return new Date(parseInt(t)).toLocaleString(); }); } }我的建议是优先在后端解决。前端 formatter 虽然能解决展示但导出、编辑回填、条件查询时还要再处理一遍不如源头就给出干净的字符串。4.2 导出 Excel 文件名变成下载下载乱码现象点击导出后浏览器能下载文件但文件名是一串百分号编码或者直接变成____.xls用户根本不知道文件内容是什么。原因Content-Disposition头里的filename如果直接写中文HTTP 头不支持非 ASCII 字符浏览器没法正确解码。不同浏览器对未编码中文的兼容性不一致Chrome 可能正常IE 和部分国产浏览器就乱码。解决在设置响应头时做一次 URL 编码也就是前文代码里写的HttpUtility.UrlEncode(订单导出.xls)。这个写法在 IIS 部署的项目里是标准的。同时要注意UrlEncode默认编码是 UTF-8如果页面请求本身是 GB2312老项目常见需要指定编码context.Response.AddHeader(Content-Disposition, attachment;filename HttpUtility.UrlEncode(订单导出.xls, System.Text.Encoding.UTF8));4.3 批量删除时后端拿不到 ids现象点击批量删除后弹窗提示“参数不合法”或者干脆报 400调试发现后端context.Request[ids]为 null。原因前端代码里用的是$(#dg).datagrid(getSelections)但 datagrid 如果没配置idFieldgetSelections返回的行对象里的主键字段名是随机的或者为空。还有一个常见原因是用 GET 请求传了一串很长的逗号分隔 ID超过 URL 长度限制被服务器直接拒绝。解决前端必须先确认 datagrid 配置了idField: OrderID这样getSelections才能正确返回选中的行。传参方式统一用 POST把 ids 放在请求体里而不是 URL 上$.post(ashx/OrderHandler.ashx, { action: delete, ids: ids }, function (data) { ... });后端拿到 ids 后先做正则校验确认是纯数字加逗号再进 DAL这样即使传参格式有问题也不会是 SQL 层的错误。4.4 上传文件提交后直接 404 或者没有响应现象上传小文件正常但文件超过几 MB 后提交请求直接失败服务器没有返回任何 JSON。有时候错误页显示 404有时候显示 500。原因这是 IIS 默认的上传大小限制在拦截不是代码的问题。IIS 7 以上有两个地方限制请求体大小ASP.NET 层面的httpRuntime和 IIS 层面的requestFiltering只改一处经常会发现不生效。解决在web.config里同时配置两层system.web httpRuntime maxRequestLength51200 executionTimeout120 / /system.web system.webServer security requestFiltering requestLimits maxAllowedContentLength52428800 / /requestFiltering /security /system.webServer参数说明maxRequestLength单位是 KB51200 即 50MBmaxAllowedContentLength单位是字节52428800 也是 50MB。两个值建议保持一致。改完配置要重启 IIS 应用程序池才生效这也是很多人改了没用就放弃排查的原因玄学背后其实是没回收工作进程。5. 二次开发把订单模板改造成通用 CRUD 生成器的思路如果你拿到这个项目只是为了照抄一个订单页面那前面的内容已经够了。但如果你想把这套东西复用到 N 张表上我强烈建议走完下面的改造这也是我第二次接手类似项目时的教训不要每张表都复制一遍OrderHandler.ashx和order.js否则改一个公共逻辑要动几十个文件。5.1 后端用泛型把增删改查收敛到基类改造思路是把OrderHandler里的公共逻辑上浮到一个BaseHandlerT泛型 T 代表业务实体实体类上用特性标注表名和主键public class BaseHandlerT : IHttpHandler where T : class, new() { protected virtual string TableName { get { return typeof(T).Name; } } protected virtual string PrimaryKey { get { return ID; } } public void ProcessRequest(HttpContext context) { string action context.Request[action]; switch (action) { case list: // 从 Request 读分页参数 page, rows // 用反射读取 T 的属性拼接 WHERE 条件 break; case add: // 从 Request.Form 反射给实体赋值 // INSERT INTO TableName(...) VALUES(...) break; case update: // 同上按主键 UPDATE break; case delete: // 正则校验 ids 后 DELETE FROM TableName WHERE PrimaryKey IN (ids) break; } } }反射赋值的核心代码大概是这样foreach (var prop in typeof(T).GetProperties()) { string value context.Request[prop.Name]; if (value null) continue; var converted Convert.ChangeType(value, prop.PropertyType); prop.SetValue(entity, converted); }参数说明Convert.ChangeType对普通类型够用但遇到 DateTime、decimal 时要注意当前线程的区域性设置建议统一用CultureInfo.InvariantCulture。有了这个基类新加一个业务模块只需要三样东西一个实体类、一个继承BaseHandlerT的空壳 ashx、一个前端列配置 JSON。之前五小时的工作量能压缩到一小时。5.2 前端把 datagrid 列配置抽成 JSON 驱动表单前端同样要做一层收敛。核心是定义一张“表结构配置”让 datagrid 的列、dialog 里的表单项都从这份配置生成var orderCols [ { name: OrderID, label: 订单号, hidden: true }, { name: OrderNo, label: 订单号, required: true, width: 120 }, { name: Customer, label: 客户名称, required: true, width: 100 }, { name: Amount, label: 金额, required: true, editor: numberbox }, { name: CreateTime, label: 创建时间, required: false, width: 150 } ]; function buildDatagridCols(cols) { return cols.filter(function (c) { return !c.hidden; }) .map(function (c) { return { field: c.name, title: c.label, width: c.width }; }); } function buildFormHtml(cols) { var html ; cols.forEach(function (c) { if (c.hidden) { html input typehidden name c.name id c.name /; } else { var required c.required ? required:true : required:false; html input name c.name classeasyui-textbox data-optionslabel:\ c.label \, required /; } }); return html; }逻辑说明buildFormHtml生成的表单还需要在 dialog 打开时用上一章说到的form(load, row)回填隐藏域会自动接收主键值。这套改造做完后新增一个模块的路径就变成了建表 → 建实体类 → 写一份列配置 JSON → 页面里调用buildDatagridCols和buildFormHtml完成渲染CRUD 逻辑全部走基类。列配置 JSON 成了系统里最核心的资产业务调整字段时只改这个文件前后端同步变化。最后再说一个验证习惯我每接一张新表都会强制走一遍这个流程——建表、写实体、配列 JSON、生成页面、导出一次 Excel。导出是最吃字段类型的一步能导出成功基本说明整条链路的映射是对的这一步跑通剩下的就是业务字段层面的微调了。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →