资讯动态

C# MVC控制器前后端传值:六条通道与模型绑定实战指南

发布时间:2026/10/5 4:16:40 来源:尧图企业网站定制
简介针对C# MVCModel-View-Controller框架中控制器与视图、模型之间数据交互的系统学习资料适合正在入门ASP.NET MVC或希望梳理前后端传值方式的开发者。内容从MVC基础概念切入重点讲解控制器如何借助ViewModel强类型视图模型、ViewBag/ViewData动态对象、TempData跨请求数据以及模型绑定与Ajax异步通信完成与视图的双向传值并给出避免在视图中直接访问数据库、使用AntiForgeryToken防CSRF等最佳实践。压缩包共112个文件大小1.31MB包含17个C#源码文件、8个cshtml视图文件、13个JavaScript脚本、7个CSS样式文件以及配置文件、JSON数据和必要依赖库目录清晰适合作为练手工程或教学配套。已有755人学习下载。通过源码可直接实践表单提交、重定向保留消息、动态绑定等常见场景帮助开发者快速掌握安全高效的数据传递方案。1. C# MVC 控制器前后端传值先把六条通道理清楚再写代码如果你写 C# MVC 有一阵子了大概率经历过这种场景控制器里算好了数据视图里却不知怎么拿或者表单提交过来模型绑定器告诉你“不能转换”你盯着控制器方法签名半天看不出问题。C# MVC 控制器前后端传值说到底就是回答一个问题一次 HTTP 请求从浏览器到控制器、再从控制器回到浏览器的路上数据存在哪儿、以什么格式走。MVC 框架给了你 ViewBag、ViewData、TempData、强类型模型、路由/表单参数、Ajax/JSON 六条通道每条通道的生存周期、类型约束和适用场景都不一样。这篇笔记适合刚接触 MVC 传值的新手也适合被模型绑定和 JSON 序列化坑过的熟手——我按“控制器往视图送数据、视图往控制器送数据、异步传值、踩坑排查”这条线把所有姿势过一遍。2. 控制器到视图的三条老路ViewBag、ViewData、TempData 各自能干到哪一步2.1 ViewBag 与 ViewData动态类型和数据字典的取舍控制器往视图传值最直接的就是 ViewBag 和 ViewData。这两个东西底层共享同一个 ViewDataDictionary区别只在写法上ViewBag 用 dynamic 动态语法ViewData 用字符串键值对。我一般建议能用 ViewBag 就别用 ViewData因为 ViewBag 写起来少一层中括号视图里读的时候也更顺。public ActionResult Index() { // ViewBag 写法动态属性 ViewBag.PageTitle 设备列表; ViewBag.TotalCount 128; // ViewData 写法字典键值 ViewData[UserName] admin; ViewData[RoleId] 3; return View(); }这段代码里 ViewBag 和 ViewData 写的是同一个字典键名“PageTitle”和“TotalCount”会自动转成 ViewBag 的动态属性名。视图里读取时ViewBag.PageTitle 和 ViewData[PageTitle] 拿到的值是一样的。需要注意ViewBag 是 dynamic编译期不做类型检查你把 TotalCount 赋成字符串视图里拿到的就是字符串赋值时写错属性名也不会报编译错误只有运行时才暴露。这是很多人觉得 MVC 传值“玄学”的第一个来源——ViewBag 里的数据永远不会在编译期给你后悔药。从数据流上看ViewBag/ViewData 的生命周期只覆盖“当前这一次请求的视图渲染”。控制器赋值视图读取请求结束就销毁不能跨请求使用。它们适合传递页面标题、提示消息、下拉框选项这类轻量数据不适合传递核心业务模型。原因有二一是 dynamic 在视图里写起来没有智能提示二是强类型视图模型才是 MVC 推荐的做法后续会详细说。2.2 TempData跨一次跳转的临时存储何时非它不可TempData 的生存周期比 ViewBag 长一点它存在 Session 里默认在读取一次之后标记删除跨一次重定向仍然有效。典型场景是 Post-Redirect-GetPRG模式表单提交后服务器处理然后 RedirectToAction 跳转到另一个 Action那个 Action 里要显示“保存成功”这类一次性消息。[HttpPost] public ActionResult Save(DeviceModel model) { // 保存业务数据到数据库 _service.Save(model); // 用 TempData 传一次性提示消息 TempData[SuccessMsg] 设备信息保存成功; // 重定向到列表页 return RedirectToAction(Index); } public ActionResult Index() { // 这里读取 TempData读取后该键会被标记删除 var msg TempData[SuccessMsg] as string; if (!string.IsNullOrEmpty(msg)) { ViewBag.Tip msg; // 转存到 ViewBag避免重复读取 } return View(); }这个例子里最关键的是“读取一次”的约定。TempData 的默认行为是第一次读取后该项就会被标记为删除当前请求结束时真正移除。如果你在 Index Action 里读了 TempData[SuccessMsg]但没有把它转存到 ViewBag 或 ViewData那么视图里再读 TempData 就是 null。所以我的习惯是在 Action 里把 TempData 读出来后立刻转存绝不在视图里直接碰 TempData。另一个坑如果你在一次请求里连续读两次 TempData[SuccessMsg]第一次返回正确值第二次可能返回 null。因为第一次读取就标记删除了。这就是为什么很多人说 TempData“丢数据”——不是真丢了而是读取次数超出了它的寿命。要改变这个行为可以用 TempData.Keep(SuccessMsg) 或 TempData.Peek(SuccessMsg)前者主动保留后者只查看不标记删除。2.3 从控制器把模型整包丢给视图强类型视图的标准姿势业务数据的传递最正确的通道是强类型视图模型。控制器里 return View(model)视图第一行用 model 指令声明类型这样视图里就有完整的智能提示和编译期类型检查。public ActionResult Detail(int id) { var device _service.GetById(id); if (device null) { return HttpNotFound(); } return View(device); }对应的视图文件 Detail.cshtml 第一行需要声明模型类型model DeviceManagement.Models.DeviceModel h2Model.DeviceName/h2 p设备编号Model.DeviceCode/p p状态Model.Status/p这里要注意一个命名约定视图第一行的 model 指令类型必须和控制器 return View() 传入的对象类型一致或者至少能兼容基类/接口关系。不一致时MVC 不会在编译期报错运行时页面直接抛异常提示“未将对象引用设置到对象的实例”。这个报错很多新手误以为是数据库返回了 null其实多半是模型类型没配好。强类型传值的额外好处是支持视图里的表单自动绑定。视图里用 Html.BeginForm 和 Html.TextBoxFor(m m.DeviceName) 生成表单控件控件 name 属性会自动带上模型属性路径提交回来时模型绑定器能自动组装出完整的 DeviceModel 对象。这是后面第 3 章“视图到控制器”的基础。3. 视图到控制器的反向通路表单、路由参数与模型绑定3.1 表单 POST 与强类型参数接收属性名匹配是第一原则浏览器把表单数据以 application/x-www-form-urlencoded 格式 POST 给服务器时表单里每个控件的 name 属性就是键输入值就是值。控制器 Action 接收这些键值对时模型绑定器做的事情很简单把请求里的键值对按“属性名匹配”原则映射到 Action 方法参数对象的属性上。public class DeviceInputModel { public string DeviceName { get; set; } public string DeviceCode { get; set; } public int Status { get; set; } } [HttpPost] public ActionResult Create(DeviceInputModel model) { if (!ModelState.IsValid) { // 校验失败时把 model 原样返回视图用户已填的数据不会丢 return View(model); } _service.Create(model); TempData[SuccessMsg] 新增设备成功; return RedirectToAction(Index); }视图里对应的表单控件 name 属性必须写成 model.DeviceName 这种全路径form methodpost action/Device/Create input typetext nameDeviceName / input typetext nameDeviceCode / input typetext nameStatus / button typesubmit提交/button /form模型绑定器的默认匹配规则不区分大小写所以 namedevicename 也能绑到 DeviceName。但它要求键名和属性名完全对应不存在模糊匹配。如果表单里 nameName 而模型属性叫 DeviceName绑定器不会做“去掉前缀再匹配”这种聪明事——它会认为没有对应值属性保持默认值。这正是很多人翻车的地方写了 Html.TextBoxFor(m m.DeviceName)生成出来的 name 是 DeviceName自己手写 HTML 时却写成了别的名字。模型绑定器也支持复杂类型嵌套。比如模型里有属性 Owner 是 UserModel 类型表单控件 name 写成 Owner.UserName绑定器会依据前缀拆解并组装嵌套对象。集合类型则用索引器语法 nameItems[0].Name这个在动态添加表格行的场景里非常有用。3.2 路由参数、QueryString 与可选参数三个来源的绑定顺序除了表单 POST控制器接收前端数据还有两个常见来源路由参数和 QueryString。MVC 的模型绑定器会按固定顺序搜索值来源表单字段 → 路由值 → QueryString。先命中先生效后面的不再尝试。public ActionResult Detail(int id, string keyword) { // id 可能来自路由 /Device/Detail/5 // keyword 可能来自 QueryString ?keywordabc var device _service.GetById(id); return View(device); }路由配置里默认有一条 {controller}/{action}/{id} 的模板所以 /Device/Detail/5 里的 5 会自动映射到 id 参数。keyword 没有路由占位符就会从 QueryString 里找。如果路由里也有 keyword 占位符路由值优先于 QueryString。这里有几个参数类型转换的边界要注意id 声明为 int但 URL 里传了 /Device/Detail/abc模型绑定器会转换失败给 ModelState 添加一条错误参数值为默认值 0但不会抛异常。如果你在 Action 里直接用这个 id 查数据库可能查到 id0 的记录返回 404 或空页面而这个错误被静默吞掉了。所以我一般在 Action 开头检查 ModelState 是否有效或者给 id 加一个可空类型 int? 先判断再取。3.3 模型绑定器的字段匹配规则与调用链绑定失败到底该看哪儿模型绑定失败时80% 的情况可以从 ModelState 里看到具体错误。ModelState 是控制器和视图之间传递校验信息的黑匣子里面装着每个属性绑定时的原始值和错误信息。[HttpPost] public ActionResult Create(DeviceInputModel model) { if (!ModelState.IsValid) { // 绑定错误例如 Status 字段传入了 abc foreach (var key in ModelState.Keys) { var errors ModelState[key].Errors; if (errors.Any()) { Console.WriteLine($字段 {key} 绑定失败{errors[0].ErrorMessage}); } } return View(model); } // 业务处理 return RedirectToAction(Index); }常见的绑定失败原因就三类类型不匹配字符串传给 int、目标属性只读或不存在、日期格式不符合当前区域性。日期格式是重灾区如果服务器区域性是 zh-CN浏览器提交 2024/13/01 这种非法日期会失败提交 2024-01-01 通常没问题但提交 01/13/2024 这种美式格式在某些区域设置下也可能解析失败。工业场景里C# 上位机通过 HTTP POST 给 MVC 控制器传数据时经常带上 time 字段格式五花八门建议在 Action 里接收字符串再手动 DateTime.TryParseExact 解析而不是让模型绑定器自动转。4. Ajax 与 JSONC# MVC 里前后端异步传值的完整配置4.1 用 fetch 把 JSON 数据 POST 给控制器Content-Type 必须对齐前后端分离的页面里控制器经常要接收 JSON 格式的请求体而不是传统表单。这时 Action 参数需要加 [FromBody] 特性让模型绑定器从请求体里读取 JSON而不是从表单字段里找。public class QueryRequest { public string DeviceCode { get; set; } public int PageIndex { get; set; } public int PageSize { get; set; } public string SortField { get; set; } } [HttpPost] public ActionResult Search([FromBody] QueryRequest request) { var list _service.Search(request.DeviceCode, request.PageIndex, request.PageSize); return Json(new { code 0, data list, total list.TotalCount }, JsonRequestBehavior.AllowGet); }前端用 fetch 发送时的关键点是 Content-Type 和序列化fetch(/Device/Search, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ deviceCode: DVC-001, pageIndex: 1, pageSize: 20, sortField: CreateTime }) }) .then(res res.json()) .then(data { console.log(data.code, data.total); });C# 侧模型绑定器对 JSON 的解析默认使用 Newtonsoft.Json.NET Framework 的 MVC 5 默认就是它ASP.NET Core MVC 里默认是 System.Text.Json但你也可以手动换回 Newtonsoft。JSON 反序列化时属性名匹配默认不区分大小写所以前端传 deviceCode 可以匹配到 DeviceCode。注意键名是 camelCase 还是 PascalCase 都能映射但嵌套深度必须和模型一致。前端传 { device: { code: DVC-001 } } 给 QueryRequest 是绑不上的因为 QueryRequest 没有 device 属性。4.2 序列化循环引用与时间格式两个最容易让 JsonResult 翻车的问题控制器返回 JsonResult 时最经典的翻车现场是导航属性循环引用。EF 里的实体类通常有关联属性DeviceModel 有个 Category 属性指向分类CategoryModel 又有 Devices 集合指向设备。序列化 DeviceModel 时JSON 序列化器顺着 Category 找到 CategoryModel再顺着 Devices 找到 DeviceModel循环往复直到抛出“检测到循环引用”的异常。// Entity Framework 实体类 public class DeviceModel { public int Id { get; set; } public string DeviceName { get; set; } public CategoryModel Category { get; set; } // 导航属性 } public class CategoryModel { public int Id { get; set; } public string CategoryName { get; set; } public virtual ICollectionDeviceModel Devices { get; set; } }解决循环引用的常见做法有三条一是序列化配置里设置 ReferenceLoopHandling.Ignore让序列化器碰到循环时跳过已引用过的对象二是使用 DTO/ViewModel把实体类映射成只包含基础字段的传输对象这个做法最干净也符合分层思想三是给导航属性标 [JsonIgnore]直接不序列化该属性。项目里如果是老代码一劳永逸的办法是全局配置别在每个 Action 里单独指定// Global.asax 或 Startup 里 GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling Newtonsoft.Json.ReferenceLoopHandling.Ignore;时间格式是另一个坑。Newtonsoft.Json 默认把 DateTime 序列化成 ISO 8601 格式但 ASP.NET MVC 里老的 JsonResult 用的 JavaScriptSerializer 可能序列化成 /Date(1699833600000)/ 这种带斜杠的格式前端 new Date() 解析时成功率依赖浏览器实现。我的习惯是在控制器里先把数据投影成匿名对象把所有 DateTime 转成字符串var list _service.GetList().Select(d new { d.Id, d.DeviceName, CreateTime d.CreateTime.ToString(yyyy-MM-dd HH:mm:ss) }).ToList(); return Json(new { code 0, data list }, JsonRequestBehavior.AllowGet);4.3 控制器返回 JSON 与视图局部更新的配合用一个固定的响应结构省掉大量分支后端返回 JSON 给前端渲染时最忌讳每次返回的结构都不统一。有的 Action 返回 { success: true }有的返回 { code: 0, msg: ok }前端每个方法都要写一次状态判断和错误提示。我在项目里会把控制器返回统一包装成一个 ApiResult 结构前端拿到后先检查 code 再决定渲染还是弹错误。public class ApiResult { public int Code { get; set; } public string Msg { get; set; } public object Data { get; set; } public static ApiResult Ok(object data null, string msg success) { return new ApiResult { Code 0, Msg msg, Data data }; } public static ApiResult Error(string msg, int code 1) { return new ApiResult { Code code, Msg msg }; } }控制器里所有 AJAX Action 都返回这个结构前端统一走一套处理逻辑这是第 6 章要展开说的内容这里先留个引子。5. 传值避坑模型绑定翻车、编码混乱和异步误判的排查记录5.1 现象整数属性绑定失败数据变成 0表单里 Status 字段用户输入了“在线”两个字或者 C# 上位机 POST 过来 statusnull绑定器把 null 赋给 int 类型的 Status 属性不会抛异常属性值为 0ModelState 里有一条错误记录。如果你没用 ModelState.IsValid 做入口检查0 就会被当正常值写进数据库事后查数据发现状态全变成 0很难定位。原因就是模型绑定器对转换失败采取“静默降级”不给默认值以外的提示。解决方式给 Status 属性改成 int?然后参数为空时显式校验或者进入 Action 立刻判断 ModelState.IsValid不通过就返回错误信息。用可空类型配合 ?? 运算符是最实用的兜底var status model.Status ?? 0; // 显式处理空值 if (status 0 || status 2) { return Json(ApiResult.Error(状态值不合法)); }5.2 现象DateTime 绑定一提交就失败换个浏览器又好了我遇到过 C# 上位机通过 HTTP POST 给 MVC 传采集时间格式是 yyyyMMddHHmmss比如 20241215153000。模型绑定器按 DateTime.TryParse 解析这个格式在 zh-CN 区域下也能解析成功但在某些服务器区域设置下会把 15 辨认为月份导致失败。更隐蔽的是前端 HTML 表单用浏览器输出的值是 yyyy-MM-dd这个格式一般没问题但如果前端用 JavaScript 自己拼字符串拼出 2024-12-15 3:30 PM绑定器解析结果就看区域脸色了。解决方式传入字符串再用 DateTime.TryParseExact 手动指定格式这是做 C# 上位机对接时最可靠的做法。public ActionResult Save(string collectTime, DeviceInputModel model) { DateTime parsedTime; var formats new[] { yyyyMMddHHmmss, yyyy-MM-dd HH:mm:ss, yyyy/MM/dd HH:mm:ss }; if (!DateTime.TryParseExact(collectTime, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTime)) { return Json(ApiResult.Error($时间格式无法解析{collectTime})); } model.CollectTime parsedTime; // 继续业务处理 }5.3 现象AJAX 请求被当成普通页面请求返回 HTML控制器里用 Request.IsAjaxRequest() 判断是否为异步请求决定返回 JSON 还是 View。问题是这个扩展方法检查的是 HTTP 头 X-Requested-With: XMLHttpRequest而用 fetch 发送请求时默认不带这个头IsAjaxRequest() 返回 false于是走了返回 View 的分支前端拿到一堆 HTML 字符串JSON.parse 直接报错。如果你用的前端库是 axios默认会带这个头原生 fetch 不带。解决方式别再依赖这个玄学判断改成检查 Content-Type 或干脆让 URL 区分。我的习惯是异步接口路径统一以 /api/ 开头或者 Action 上加自定义特性标记路由层面就把 AJAX 和页面请求分开了。5.4 现象TempData 里的数据莫名其妙的没了一个页面流程是A 页面表单 → POST 到 Save → 重定向到 Index → Index 里读 TempData 显示提示。看起来没问题但如果中间有一次 Response.Redirect 用了 endResponse: true或者浏览器在提交时被 302 反复跳了一次TempData 就可能提前失效。另外如果你在 Index 里读了 TempData[SuccessMsg]又把它放进了 ViewBag然后在布局页Layout里也读了一次 TempData[SuccessMsg]因为布局页渲染发生在视图之前或之后读取顺序会导致数据被提前标记删除。排查方法把 TempData 的读取点钉死在 Action 里读一次就转存到 ViewBag视图和布局页绝不直接碰 TempData。这个方法用了三年没再丢过数据。6. 一个能端到端复用的传值技巧用统一 ApiResult 包装控制器返回最后落到一个具体的、能立刻搬进项目的技巧把控制器的所有异步返回统一包装成 ApiResult 结构前端用一个统一方法接收。这个做法不需要引入新框架在现有 C# MVC 项目里加一个类就能用长期收益是前后端联调时不用每个接口对一遍返回格式。先定义包装类上一章已经给了基础版本这里加上泛型版本方便带数据public class ApiResult { public int Code { get; set; } public string Msg { get; set; } public object Data { get; set; } } public static class ApiResultHelper { public static ApiResult Ok(object data null, string msg ok) { return new ApiResult { Code 0, Msg msg, Data data }; } public static ApiResult Error(string msg, int code 1) { return new ApiResult { Code code, Msg msg }; } }控制器里所有返回 JSON 的 Action 统一这样写[HttpPost] public ActionResult Delete(int id) { var result _service.Delete(id); if (!result.Success) { return Json(ApiResultHelper.Error(result.ErrorMsg)); } return Json(ApiResultHelper.Ok(null, 删除成功)); }前端统一处理async function postJson(url, payload) { const response await fetch(url, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify(payload) }); const result await response.json(); if (result.code ! 0) { alert(result.msg); throw new Error(result.msg); } return result.data; } // 调用示例 postJson(/Device/Delete, { id: 5 }).then(() { location.reload(); }).catch(err console.error(err));这个技巧手把手用了几个项目后我最大的体会不是减少代码量而是排查问题的速度明显加快。以前 AJAX 返回了 HTML 报错页面前端控制台一片红无从下手现在统一结构后后端异常可以通过 filter 捕获并填充到 ApiResult.Msg前端直接弹出具体错误文案省去抓包分析的时间。最后说一个我踩过的坑ApiResult 里的 Data 如果传的是 EF 实体序列化时循环引用还是会炸所以包装之前务必先投影成 DTO 或匿名对象。你是直接包装实体类还是先做投影建议先投影这一步就是“传值不出错”和“传值能踩坑”的分界线。希望这篇笔记能帮你把 C# MVC 的传值通道理清楚少走我走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑