简介这是一套基于C#与EasyUI框架开发的Web后台管理功能完整示例面向.NET初学者及Web前端交互实践者解决MVC架构下常见CRUD操作与文件交互上传/导出的集成难题。项目采用VS2013开发环境后端基于SQL Server 2014兼容2005/2008含分页适配说明涵盖用户信息表的新增、修改、删除、分页展示、Excel批量导出及图片上传等核心功能代码结构清晰便于理解前后端数据交互逻辑与EasyUI组件调用方式。压缩包为RAR格式共包含多个.cs业务逻辑文件、.aspx视图页、JavaScript脚本及数据库脚本等整体大小28.54MB适合作为教学参考或快速二次开发基础模板。已有219人学习下载读者可直接运行调试掌握EasyUI布局与增删改查全流程、SQL Server分页实现差异处理、NPOI导出Excel及FileUpload控件上传机制等实用技能。1. 一个被低估的“老派”前端组合C# EasyUI 实现的 CRUDExcel上传全链路至今在政企内网、制造业MES、教育教务系统里跑得比某些新框架还稳你可能觉得 EasyUI 过时了但某高校教务处的排课系统、某制造企业车间报工终端、某区级档案管理系统至今还在用它——不是因为技术选型多先进而是因为「改不动」「够用」「没人敢动」。这个 C#EasyUI 的完整 CRUD 示例不是玩具 Demo它把新增、修改、删除、Excel 导出、文件上传这五项高频后台操作全部串成一条可落地、可调试、可二次部署的完整链路。它不依赖 Vue/React 构建流程不搞 Webpack 配置玄学IIS 一放就能跑后端是标准 WebForms 或 ASP.NET MVC本项目为 WebForms数据库直连 SQL Server所有交互通过 JSON 接口完成。如果你正接手一个维护中的老系统或者需要快速交付一个给非技术人员用的内部管理页又或者想带新人理解「前后端分离雏形」怎么从零搭起——这个包就是你的后悔药。它没用 SignalR 做实时没上 Redis 缓存但每个按钮点击后你能清晰看到请求发了什么、后端怎么解析、JSON 怎么序列化、Excel 文件怎么生成并流式返回——没有黑匣子全是可打断点、可加日志、可改字段的实打实代码。2. 项目结构与技术栈解剖WebForms 为壳EasyUI 为皮ADO.NET 为骨Excel 操作靠 NPOI 扛大旗这个项目不是 SPA也不是前后端完全分离架构而是一个典型的「服务端渲染 前端组件增强」模式。它用 WebForms 的 .aspx 页面承载 UI用 EasyUI 的 JS/CSS 提供表格、表单、弹窗等交互能力后端逻辑写在 .aspx.cs 中数据访问层直接调用 ADO.NETExcel 导出和文件上传则分别由 NPOI 和标准HttpPostedFile处理。整个结构轻量、无构建步骤、无 node_modules 膨胀适合部署在老旧 Windows Server IIS 环境中。2.1 目录结构与核心文件职责说明项目解压后共 12 个关键文件不含 bin 和 obj这些需编译生成/Default.aspx ← 主入口页含 EasyUI DataGrid Toolbar /Default.aspx.cs ← 后端事件处理绑定表格、响应增删改按钮 /EditForm.aspx ← 弹窗编辑页新增/修改共用含 EasyUI Form 表单 /EditForm.aspx.cs ← 表单提交逻辑参数校验、SQL 插入/更新、返回 JSON /ExportHandler.ashx ← 通用 Excel 导出处理器非 .aspx性能更可控 /UploadHandler.ashx ← 文件上传处理器支持单文件、限制类型/大小 /Scripts/easyui/ ← EasyUI v1.5.3 官方 JS/CSS已精简去除了不需要的插件 /Content/themes/ ← EasyUI 自定义主题蓝色系适配内网风格 /Models/Student.cs ← 简单实体类示例为 Student字段含 ID/Name/Class/Score /Utils/ExcelHelper.cs ← NPOI 封装类CreateExcelFromDataTable、WriteToResponse /Utils/DbHelper.cs ← ADO.NET 封装ExecuteScalar、ExecuteNonQuery、FillDataTable /Web.config ← 关键配置httpRuntime maxRequestLength10240 /10MB 上传上限提示所有 .aspx 页面均未启用 ViewStateEnableViewStatefalse避免大数据量表格回传时页面臃肿EasyUI 版本锁定为 1.5.3兼容 IE10且与 WebForms postback 兼容性最佳不建议升级到 1.6否则datagrid的onSelect回调会与__EVENTTARGET冲突。2.2 技术选型背后的务实逻辑为什么用 WebForms 而非 MVCWebForms 的Page_LoadButton_Click模型对刚从 WinForm 转来的开发者极友好% %表达式可直接嵌入 JS 变量如url: % ResolveUrl(~/EditForm.aspx) %省去配置路由或全局变量注入ViewState 关闭后性能损耗几乎可忽略。为什么 EasyUI 不换 Element UI 或 LayUIEasyUI 的datagrid支持remoteSort: trueremoteFilter: true配合 WebForms 的Request[sort]和Request[order]可直接做服务端排序无需重写分页逻辑其form.load()方法能自动将 JSON 数据映射到表单字段省去手写$(#name).val(data.name)更重要的是——它有官方中文文档且错误提示明确比如parser error一定是因为 JSON 格式错而不是 Promise reject 黑盒。NPOI 为何比 EPPlus 更适合此场景EPPlus 依赖 Office Open XML SDK在 Server 2012 R2 上需额外安装运行时且对.xlsx格式强绑定NPOI 纯托管实现HSSFWorkbookxls和XSSFWorkbookxlsx双模支持导出时可动态判断数据量 65536 行用 xls兼容老 Excel否则用 xlsx。本项目ExcelHelper.cs中已封装该判断逻辑。2.3 后端数据流全景图从按钮点击到数据库写入的七步链路以「新增学生」为例完整链路如下每步均可打断点验证用户点击 toolbar 上的「新增」按钮 → 触发 EasyUI$(#dg).datagrid(addRow, {...})JS 调用$(#w).window(open)打开EditForm.aspx弹窗弹窗加载时JS 发起 GET 请求/EditForm.aspx?modeadd后端Page_Load中根据Request[mode]渲染空表单用户填写后点击「保存」→ JS 序列化表单为 JSONPOST 到/EditForm.aspx注意不是 AJAX是传统 form submitEditForm.aspx.cs中protected void Page_Load(object sender, EventArgs e)捕获Request.HttpMethod POST调用Request.Form[name]等取值校验通过后拼接 SQL INSERT 语句交由DbHelper.ExecuteNonQuery(sql, params)执行成功后Response.Write({\success\:true,\msg\:\添加成功\}); Response.End();前端 JS 解析 JSON 并关闭窗口注意所有后端接口均未使用[WebMethod]或ScriptManager避免 WebForms 的__VIEWSTATE干扰 JSON 返回Response.Write后必须跟Response.End()否则 IIS 可能追加 HTML 尾部导致 JSON 解析失败。3. 核心模块逐行拆解DataGrid 初始化、弹窗表单、Excel 导出、文件上传四块硬骨头3.1 DataGrid 初始化与 CRUD 交互如何让 EasyUI 表格真正“活”起来Default.aspx中的 DataGrid 初始化代码表面看只是配置项堆砌实则每项都对应一个易翻车点table iddg classeasyui-datagrid >function formatOperate(value, row, index) { return a hrefjavascript:void(0) onclickeditRow( row.id ) classeasyui-linkbutton>function editRow(id) { $(#w).window(open); // 打开编辑窗口 $(#fm).form(load, /EditForm.aspx?modeeditid id); // 加载数据EasyUI 自动发起 GET }3.2 弹窗表单EditForm.aspx复用同一页面实现新增/修改的底层机制EditForm.aspx是本项目最精巧的设计点——它用同一个 .aspx 页面通过 URL 参数modeadd或modeeditid123切换行为后端Page_Load中据此分支处理protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { string mode Request.QueryString[mode]; if (mode edit) { int id Convert.ToInt32(Request.QueryString[id]); var student DbHelper.FillDataTable(SELECT * FROM Students WHERE IDid, new SqlParameter(id, id)).Rows[0]; // 将数据转为 JSON注入到页面 JS 变量中 ClientScript.RegisterStartupScript(this.GetType(), initData, $var initData {JsonConvert.SerializeObject(student)};, true); } else if (mode add) { ClientScript.RegisterStartupScript(this.GetType(), initData, var initData {};, true); } } }前端 JS 在$(document).ready()中读取initData并调用$(#fm).form(load, initData)即可自动填充表单。这种「服务端预加载 前端自动绑定」模式比纯 AJAX 加载更稳定避免跨域、CORS、IE 兼容问题。表单提交逻辑在EditForm.aspx.cs的Page_Load中处理if (Request.HttpMethod POST) { string mode Request.Form[mode]; // 注意这里取 Form 而非 QueryString string name Request.Form[name]; string classVal Request.Form[class]; string scoreStr Request.Form[score]; // 校验实际项目应抽离为 Validator 类 if (string.IsNullOrWhiteSpace(name)) { WriteJsonError(姓名不能为空); return; } decimal score; if (!decimal.TryParse(scoreStr, out score) || score 0 || score 100) { WriteJsonError(成绩必须是 0~100 的数字); return; } if (mode add) { int newId DbHelper.ExecuteNonQuery( INSERT INTO Students (Name, Class, Score) VALUES (name, class, score); SELECT SCOPE_IDENTITY();, new SqlParameter(name, name), new SqlParameter(class, classVal), new SqlParameter(score, score)); WriteJsonSuccess($添加成功ID{newId}); } else // edit { int id Convert.ToInt32(Request.Form[id]); DbHelper.ExecuteNonQuery( UPDATE Students SET Namename, Classclass, Scorescore WHERE IDid, new SqlParameter(name, name), new SqlParameter(class, classVal), new SqlParameter(score, score), new SqlParameter(id, id)); WriteJsonSuccess(修改成功); } }WriteJsonSuccess是封装方法private void WriteJsonSuccess(string msg) { Response.Clear(); Response.ContentType application/json; Response.Write(${{\success\:true,\msg\:\{msg}\}}); Response.End(); }提示Response.Clear()必须在Write前调用否则可能混入 HTML 头部Response.End()阻止后续生命周期执行避免Page_Render输出多余内容。3.3 Excel 导出ExportHandler.ashxNPOI 流式写入内存占用低于 5MB 即可导出 10 万行ExportHandler.ashx是一个通用 HTTP Handler不走 Page 生命周期性能更高。它接收GET请求参数如?tableStudentswhereClass高三1班动态生成 Excelpublic void ProcessRequest(HttpContext context) { string table context.Request.QueryString[table]; string where context.Request.QueryString[where]; string sql $SELECT * FROM {table}; if (!string.IsNullOrEmpty(where)) sql $ WHERE {where}; DataTable dt DbHelper.FillDataTable(sql); // 根据行数选择工作簿类型 IWorkbook workbook dt.Rows.Count 65536 ? new HSSFWorkbook() // xls兼容老版本 Excel : new XSSFWorkbook(); // xlsx ISheet sheet workbook.CreateSheet(数据导出); // 写入表头 IRow headerRow sheet.CreateRow(0); for (int i 0; i dt.Columns.Count; i) { ICell cell headerRow.CreateCell(i); cell.SetCellValue(dt.Columns[i].ColumnName); } // 写入数据行关键避免创建过多 Cell 对象 for (int i 0; i dt.Rows.Count; i) { IRow row sheet.CreateRow(i 1); for (int j 0; j dt.Columns.Count; j) { ICell cell row.CreateCell(j); object value dt.Rows[i][j]; if (value DBNull.Value) continue; // 自动识别类型写入避免全部当字符串 if (value is DateTime) cell.SetCellValue((DateTime)value); else if (value is double || value is float || value is decimal) cell.SetCellValue(Convert.ToDouble(value)); else cell.SetCellValue(value.ToString()); } } // 设置响应头触发浏览器下载 context.Response.Clear(); context.Response.ContentType application/vnd.ms-excel; string fileName ${table}_{DateTime.Now:yyyyMMddHHmmss}.xls; if (workbook is XSSFWorkbook) fileName fileName.Replace(.xls, .xlsx); context.Response.AddHeader(Content-Disposition, $attachment;filename{fileName}); // 流式写入不生成临时文件 using (MemoryStream ms new MemoryStream()) { workbook.Write(ms); ms.WriteTo(context.Response.OutputStream); } }注意workbook.Write(ms)是关键它把整个 Excel 结构写入内存流再直接输出到Response.OutputStream全程无磁盘 IO若数据量极大50 万行建议改用SXSSFWorkbookNPOI 的流式写入版但本项目未引入因 90% 场景下 10 万行以内足够。3.4 文件上传UploadHandler.ashx支持单文件、类型白名单、大小限制、安全重命名UploadHandler.ashx处理 EasyUI 的filebox组件上传请求public void ProcessRequest(HttpContext context) { HttpPostedFile file context.Request.Files[filebox]; // EasyUI 默认字段名 if (file null || file.ContentLength 0) { WriteJsonError(context, 未选择文件); return; } // 白名单校验防止 .asp/.php 上传 string[] allowedExtensions { .jpg, .jpeg, .png, .pdf, .doc, .docx, .xls, .xlsx }; string ext Path.GetExtension(file.FileName).ToLowerInvariant(); if (!allowedExtensions.Contains(ext)) { WriteJsonError(context, $不支持的文件类型{ext}仅允许 {string.Join(,, allowedExtensions)}); return; } // 大小限制5MB if (file.ContentLength 5 * 1024 * 1024) { WriteJsonError(context, 文件大小不能超过 5MB); return; } // 安全重命名GUID 原扩展名防止中文/特殊字符 string newFileName Guid.NewGuid().ToString(N) ext; string uploadPath context.Server.MapPath(~/Uploads/); // 创建目录如果不存在 if (!Directory.Exists(uploadPath)) Directory.CreateDirectory(uploadPath); string fullPath Path.Combine(uploadPath, newFileName); file.SaveAs(fullPath); // 返回 JSON包含访问 URL供前端展示或存库 string fileUrl $~/Uploads/{newFileName}; WriteJsonSuccess(context, new { url fileUrl, fileName file.FileName, size file.ContentLength }); }EasyUI 前端调用方式$(#filebox).filebox({ buttonText: 选择文件, buttonIcon: icon-upload, prompt: 请选择要上传的文件..., onChange: function (newValue, oldValue) { // 文件选择后自动上传 $(this).filebox(upload); } });提示filebox的upload方法默认 POST 到当前页面因此UploadHandler.ashx的 URL 需在 JS 中显式指定$(#filebox).filebox({ uploader: % ResolveUrl(~/UploadHandler.ashx) % });4. 避坑指南五个血泪经验总结专治部署即崩、上传失败、导出乱码、表格不刷新4.1 现象点击「导出 Excel」后浏览器卡死F12 查看 Network 显示 pending最终超时原因ExportHandler.ashx中未设置Response.BufferOutput false导致大文件写入时 IIS 缓冲区满而阻塞或workbook.Write(ms)后未调用ms.Position 0导致ms.WriteTo(...)从末尾开始写空数据。解决在ExportHandler.ashx开头添加context.Response.BufferOutput false;MemoryStream写入前加ms.Position 0;。4.2 现象上传文件后UploadHandler.ashx报错Object reference not set to an instance of an object定位到file.ContentLength原因Web.config 中httpRuntime maxRequestLength10240 /单位是 KB但executionTimeout默认 110 秒大文件上传超时后Request.Files为空或前端filebox未设置namefilebox导致后端取不到。解决在web.config的system.web下增加httpRuntime maxRequestLength10240 executionTimeout300 /并在system.webServer下补充security requestFiltering requestLimits maxAllowedContentLength10485760 / /requestFiltering /security注意maxAllowedContentLength单位是字节10MB 104857604.3 现象DataGrid 分页后点击「编辑」弹窗表单数据为空原因EditForm.aspx中$(#fm).form(load, ...)加载的是绝对 URL如/EditForm.aspx?modeeditid123但 EasyUI 的form.load()默认用 GET 请求而 WebForms 的Page_Load中IsPostBack为 false 时才执行查询但form.load()的 GET 请求不会触发Page_Load的IsPostBackfalse分支因为它是独立请求。解决EditForm.aspx中移除if (!IsPostBack)包裹改为始终执行数据加载逻辑或改用$.getJSON手动取数据再form.load()。4.4 现象导出的 Excel 中中文显示为方框或乱码原因NPOI 创建单元格时未设置字体Excel 默认用 Arial不支持中文或Response.ContentType错误设为application/octet-stream。解决在ExportHandler.ashx中添加字体设置IFont font workbook.CreateFont(); font.FontName 微软雅黑; font.FontHeightInPoints 10; ICellStyle style workbook.CreateCellStyle(); style.SetFont(font); // 应用到所有单元格 for (int i 0; i sheet.LastRowNum; i) { IRow row sheet.GetRow(i); if (row ! null) for (int j 0; j row.LastCellNum; j) { ICell cell row.GetCell(j); if (cell ! null) cell.CellStyle style; } }4.5 现象IIS 部署后所有 EasyUI 样式丢失表格变成原始 HTML 表格原因EasyUI 的 CSS/JS 文件路径错误link hrefeasyui/themes/default/easyui.css中的easyui/是相对路径但 IIS 应用程序池的物理路径与虚拟目录不一致或Web.config中 MIME 类型未注册.js和.css。解决统一用% ResolveUrl(~/Scripts/easyui/themes/default/easyui.css) %检查Web.config的system.webServerstaticContent是否包含mimeMap fileExtension.js mimeTypeapplication/javascript / mimeMap fileExtension.css mimeTypetext/css /5. 二次开发实战三步改造让这个“老古董”支撑真实业务需求5.1 改造一为上传功能增加「多文件 进度条」支持兼容 IE10原filebox仅支持单文件且无进度反馈。要升级为多文件需替换为uploadify或原生input[typefile]FormData。我选择后者因无需额外 JS 库前端替换filebox为原生 input在Default.aspx中input typefile idmultiFile multiple accept.jpg,.png,.pdf styledisplay:none; / a hrefjavascript:void(0) classeasyui-linkbutton>$(#multiFile).change(function () { var files this.files; if (files.length 0) return; var formData new FormData(); for (var i 0; i files.length; i) { formData.append(files, files[i]); // 注意 key 是 files非 filebox } $.ajax({ url: % ResolveUrl(~/UploadHandler.ashx) %, type: POST, data: formData, processData: false, contentType: false, xhr: function () { var xhr new window.XMLHttpRequest(); xhr.upload.addEventListener(progress, function (evt) { if (evt.lengthComputable) { var percentComplete evt.loaded / evt.total * 100; $(#progress).progressbar(setValue, parseInt(percentComplete)); } }, false); return xhr; }, success: function (data) { $.messager.alert(提示, 上传成功 data.files.length 个文件); } }); });后端UploadHandler.ashx支持多文件修改ProcessRequest// 原来取单文件 // HttpPostedFile file context.Request.Files[filebox]; // 改为取文件集合 HttpFileCollection files context.Request.Files; Liststring uploadedUrls new Liststring(); for (int i 0; i files.Count; i) { HttpPostedFile file files[i]; // ...原有校验逻辑... string newFileName Guid.NewGuid().ToString(N) ext; string fullPath Path.Combine(uploadPath, newFileName); file.SaveAs(fullPath); uploadedUrls.Add($~/Uploads/{newFileName}); } // 返回 JSON 数组 WriteJsonSuccess(context, new { urls uploadedUrls });注意IE10 支持XMLHttpRequest.upload但 IE9 及以下不支持若需兼容应回退到 iframe 方案。从那以后我每次接到上传需求都强制走一遍xhr.upload兼容性测试表IE10/11、Chrome 60、Firefox 50再决定是否引入 polyfill。5.2 改造二为 DataGrid 增加「服务端搜索」避免前端 filter 性能崩盘原项目搜索靠 EasyUI 的filter属性数据全量加载后前端过滤1 万行就卡顿。改为服务端搜索只需两处改动前端在 toolbar 添加搜索框绑定onSearch事件div idtoolbar a hrefjavascript:void(0) classeasyui-linkbutton>function doSearch() { var keyword $(#searchBox).textbox(getValue); $(#dg).datagrid(load, { keyword: keyword, page: 1 // 重置到第一页 }); }后端修改Default.aspx.cs的GetStudents方法假设是 WebMethod或Default.aspx的Page_Load若用 QueryString// 若用 QueryString 方式推荐免去 ScriptManager 配置 string keyword Request.QueryString[keyword]; string sql SELECT * FROM Students; ListSqlParameter parameters new ListSqlParameter(); if (!string.IsNullOrEmpty(keyword)) { sql WHERE Name LIKE keyword OR Class LIKE keyword; parameters.Add(new SqlParameter(keyword, % keyword %)); } DataTable dt DbHelper.FillDataTable(sql, parameters.ToArray()); // ...序列化返回 JSON...5.3 改造三导出 Excel 时自动合并相同班级的单元格提升报表可读性业务方常提“导出的 Excel同班级学生希望合并‘班级’列”。NPOI 支持区域合并只需在ExportHandler.ashx的数据写入循环后添加// 合并班级列假设班级在第2列索引为1 int classColIndex 1; int startRow 1; // 第一行是表头 string currentClass ; for (int i 1; i sheet.LastRowNum; i) { IRow row sheet.GetRow(i); if (row null) continue; ICell cell row.GetCell(classColIndex); string classValue cell?.ToString() ?? ; if (classValue ! currentClass) { // 结束上一个合并区域 if (i startRow 1) // 至少两行才合并 { sheet.AddMergedRegion(new CellRangeAddress(startRow, i - 1, classColIndex, classColIndex)); } currentClass classValue; startRow i; } } // 处理最后一组 if (sheet.LastRowNum startRow currentClass ! ) { sheet.AddMergedRegion(new CellRangeAddress(startRow, sheet.LastRowNum, classColIndex, classColIndex)); }提示AddMergedRegion会覆盖被合并区域内的单元格内容因此必须在所有数据写入完成后调用合并后若需居中需单独设置CellStyle的Alignment属性。从那以后我每次做导出增强都先确认 NPOI 版本是否支持CellRangeAddress1.2.3 均支持再动手写合并逻辑。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?