尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C# MVC控制器前后端传值全解析:模型绑定到JSON交互的实战指南

C# MVC控制器前后端传值全解析:模型绑定到JSON交互的实战指南 简介控制器前后端传值是C# MVC开发中的核心环节这份资源整理了一套可运行的示例工程与配套笔记面向ASP.NET MVC初学者和需要系统梳理数据传递方式的开发者。压缩包内共112个文件以C#源文件.cs承载控制器与模型逻辑Razor视图.cshtml负责页面展示JavaScript和CSS实现前端交互与样式另有JSON、Config等配置文件及项目工程文件整体仅1.31MB可快速下载并用Visual Studio直接打开调试。资源已有755人学习下载。内容依据MVC分层思想逐一演示了ViewModel强类型传值、ViewBag/ViewData动态传值、TempData跨请求数据共享、模型绑定自动映射表单参数以及Ajax异步交互等五种前后端通信方式每个示例都配有精简代码和实现说明还总结了保持控制器简洁、使用AntiForgeryToken防止跨站请求伪造、按数据生命周期选择传递方式等最佳实践。对照练习后可以建立起从控制器到视图再到前端的完整数据链路认知提升实际项目开发中的传值效率与代码质量。1. 控制器传值所有 MVC 项目的第一个分水岭“C#MVC控制器前后端传值”本质上解决的是一个非常具体的工程问题浏览器里的表单、AJAX 请求、路由里的参数怎么安全、完整地落到控制器方法里再以 HTML 或 JSON 的形式回到页面。很多项目刚开始不重视这一层等页面超过二十个、参数超过十几个改一个字段名就得全链路排查后端拿到的数据要么是 null要么类型对不上调试像开黑匣子。这篇文章把控制器接收参数、返回数据的完整链路拆开讲从模型绑定到强类型视图再到 JSON 交互和 PRG 模式适合写过几个 MVC 页面但没系统理过传值体系的初级开发者也适合被 ViewBag 和 TempData 坑过的中级开发者做一次排查。2. 传值链路的第一站路由与模型绑定以及六种传值方式的选型2.1 从 URL 到方法参数控制器拿值的完整链路浏览器发出一个 GET 或 POST 请求ASP.NET MVC 先做路由匹配把 URL 段映射到某个控制器的某个 Action 方法再把 HTTP 请求里携带的数据经模型绑定器Model Binder按名字填进方法的参数。这个环节是前后端传值的地基路由匹配错了或者参数名对不上后面什么都白搭。先看 ASP.NET Core MVC 的默认路由注册这段代码在 Program.cs 里// ASP.NET Core MVC.NET 6/7/8的默认路由注册 app.MapControllerRoute( name: default, pattern: {controllerHome}/{actionIndex}/{id?});pattern 里的三个占位符自解释controller 对应控制器名action 对应方法名id 是可选参数。请求/Order/Detail/1001会被映射到OrderController.Detail方法1001自动填进id。注意查询字符串不在 pattern 里它由模型绑定器另一路处理。再看一个 Action 签名展示同一个方法从不同来源取值的自然写法public class OrderController : Controller { // GET /Order/Detail/1001?sourceportal public IActionResult Detail(int id, string source) { // id 来自路由片段 1001source 来自查询串 portal // 绑定器自动完成了从字符串到 int 的转换 return Content($order{id}, source{source}); } }这段代码的逻辑说明模型绑定器维护一个值集合里面同时包含表单字段、路由片段、查询字符串绑定器按参数名去这些源里找匹配项。id从路由片段里取到source从查询串里取到开发者不需要自己解析Request.QueryString。转换失败时绑定器不会抛异常而是把错误记进ModelState所以 Action 里可以用ModelState.IsValid统一判断。在 ASP.NET Core MVC 里这套机制换了一层 endpoint 路由的外壳但控制器接收参数的逻辑和 .NET Framework 时代的 MVC5 几乎一脉相承。搞过 Spring MVC 的老同学可以把控制器理解成 handler method把模型绑定理解成参数解析器上手会很快。有一点值得注意ASP.NET MVC 里传值手段多但新项目建议直接按 ASP.NET Core MVC 的方式写代码避免把老 MVC5 的某些过时习惯带进来。提示路由的id?表示可选如果方法签名里有非可空int而路由没提供值绑定器会尝试从查询串或表单里找找不到就给默认值 0不会直接 404。2.2 六种传值方式的选型表传值方式太多很多人是“哪个顺手用哪个”结果项目后期到处是黑匣子。先把 MVC 框架里常见的方式放进一张表再讲我的取舍逻辑传值方式生命周期怎么读典型场景ViewData当前请求内ViewData[Key]需强转给视图塞辅助数据ViewBag当前请求内ViewBag.Key写起来快临时用TempData跨一次请求TempData[Key]读一次会用掉PRG 后传提示语Model 强类型当前请求内model指令主数据渲染最推荐Session跨请求持久HttpContext.Session登录态等少量数据JsonResult AJAX单次异步交互前端response.json()局部刷新/前后端配合我的取舍建议是主线数据用强类型辅助数据用 ViewBag跨请求用 TempDataSession 只给那种每个页面都要读的场景比如登录用户名。最忌讳在控制器里定义 static 变量传值——IIS 应用池回收、多用户并发都会让你怀疑人生这种方案看起来省事实际是最贵的。ViewBag 和 ViewData 是同一份字典数据ViewBag 只是 ViewData 的 dynamic 包装两者可以混读但项目里最好只选一个用。主数据不要走 ViewBag因为它是黑匣子编译器无法检查动态类型的属性是否存在存进去的是不是正确类型要等运行期才知道。TempData 适合传“一次性提示语”但它背后有会话序列化开销别拿它传大数据更别传实体对象列表。3. 把表单数据送进控制器GET、POST 与复杂对象绑定的可复现代码3.1 一个表单从页面到控制器的完整例子这是最简单的场景也是每个 MVC 项目都会遇到的用户填表单点提交控制器接收后处理。下面的视图代码用 Razor 写放在Views/Order/Create.cshtmlmodel OrderViewModel form asp-actionCreate asp-controllerOrder methodpost label订单号/label input typetext nameOrderNo valueModel?.OrderNo / label金额/label input typenumber nameTotal valueModel?.Total / button typesubmit提交/button /form对应的控制器方法[HttpPost] public IActionResult Create(OrderViewModel order) { if (!ModelState.IsValid) { return View(order); // 校验失败把已填的值送回表单 } return RedirectToAction(nameof(Index)); }参数说明name属性是模型绑定器的查找键绑定器拿到表单字段后按名字OrderNo、Total去匹配OrderViewModel的同名属性自动创建对象并赋值。asp-action和asp-controller是 Razor 辅助标签运行时生成正确的action和method属性比手写字符串更稳。ModelState.IsValid对应 ViewModel 上的数据注解比如[Required]、[Range]校验失败时把对象原样送回视图用户输入不会丢。这里有个新手常踩的点表单的 method 必须是post否则浏览器用查询字符串提交敏感数据会出现在 URL 里。POST 请求体默认是application/x-www-form-urlencoded编码键值对形式和查询字符串很像但位置不同。如果用 fetch 手动发请求Content-Type 也要保持一致。3.2 嵌套对象和集合怎么命名订单场景里ViewModel 经常包含客户信息和多条明细。C# 侧定义如下public class OrderViewModel { public int CustomerId { get; set; } public Customer Customer { get; set; } public ListOrderItem Items { get; set; } } public class Customer { public string Name { get; set; } } public class OrderItem { public string Sku { get; set; } public int Qty { get; set; } }视图里的 input 名称要写成点号和索引器形式绑定器才能识别出嵌套关系input nameCustomer.Name value(Model?.Customer?.Name) / input nameItems[0].Sku value(Model?.Items?[0]?.Sku) / input nameItems[0].Qty value(Model?.Items?[0]?.Qty) /这里点号表示嵌套属性方括号表示集合下标。绑定器在Customer为 null 时会先 new 一个出来再把Name填进去对Items也是同理按索引逐个生成对象。注意一个前提集合属性必须有可写的 setter或者在声明时初始化比如public ListOrderItem Items { get; set; } new();。很多项目里绑定后Items为 null就是属性只有 getter 或者根本没初始化绑定器无法往里填充这在调试时非常迷惑。集合绑定还有一个边界坑下标必须连续。如果页面只有Items[2].Sku没有Items[0]和Items[1]绑定器会放弃解析整个集合。要么下标从 0 连续排列要么用隐藏字段给缺失下标补空值否则就等着参数集合为 null 吧。3.3 文件上传的接收写法文件上传是传值体系里比较特殊的一类数据不是键值对而是 multipart 格式的二进制流。视图端先要保证 form 有enctypeform enctypemultipart/form-data asp-actionUpload methodpost input typefile namefile / button typesubmit上传/button /form控制器端用IFormFile接收[HttpPost] public async TaskIActionResult Upload(IFormFile file) { if (file null || file.Length 0) return BadRequest(没有拿到文件); var saveDir Path.Combine(Directory.GetCurrentDirectory(), uploads); Directory.CreateDirectory(saveDir); var savePath Path.Combine(saveDir, Path.GetFileName(file.FileName)); await using var stream System.IO.File.Create(savePath); await file.CopyToAsync(stream); return Json(new { fileName file.FileName, size file.Length }); }IFormFile是 ASP.NET Core 对上传文件的流式抽象不要为了拿字节数先把整个文件读进内存。大文件场景下全量读入内存会拖垮进程CopyToAsync边读边写才是正道。生产环境还要补两道防线扩展名白名单防脚本文件和大小限制MaxRequestBodySize否则一个超大文件就能让你的站点陷入假死。有人把文件 base64 编码后塞进 JSON 提交那是另一套方案适合小文件或接口对接场景但不适合常规表单。浏览器上传原生就是 multipart没必要绕一圈。4. 把数据送回前端强类型视图与 JSON 交互的标准输出姿势4.1 用 model 传主数据而不是 ViewBag控制器把数据送回视图最简单的方式是return View(vm)让 Razor 视图通过model指令拿到强类型对象。看一个编辑页的完整写法public IActionResult Edit(int id) { var order _repo.Find(id); if (order null) return NotFound(); var vm new OrderViewModel { Id order.Id, OrderNo order.OrderNo, Total order.Total }; return View(vm); }视图顶部声明模型类型页面里就能直接引用model OrderViewModel pModel.OrderNo/p input asp-forOrderNo /这里我一般不用 ViewBag 传主数据原因很实际ViewBag 是 dynamic编译器不检查类型属性名写错要等运行期才报错查起来效率极低。强类型视图在 Razor 编译期就能发现属性不存在的问题改字段名时全项目编译一遍哪里断了立刻知道。asp-forOrderNo这个辅助标签会自动生成name和value比手写nameOrderNo更不容易出错。补充一个细节ViewData 和 ViewBag 是同一份字典用哪个都行但一个页面里别混着用。混用的后果是代码可读性差后来的人不知道数据到底存在哪个容器里。4.2 控制器返回 JSONContent-Type 与 JSON 匹配配置AJAX 场景下控制器返回 JSON 是常态。前端用 fetch 发送数据控制器用[FromBody]接收[HttpPost] public IActionResult Save([FromBody] OrderViewModel order) { if (order null || order.OrderNo null) return BadRequest(new { error 字段没绑上 }); return Json(new { ok true, orderId order.Id }); }前端对应的请求写法fetch(/Order/Save, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ orderNo: A001, total: 299.9 }) })[FromBody]的作用是告诉绑定器不要走表单解析直接从请求体读 JSON。前端Content-Type必须设为application/json否则请求体是纯文本后端解析不出对象。这里最值得花十分钟统一的就是 JSON 匹配配置。前端 JavaScript 习惯用 camelCase 的 keyorderNoC# 属性是 PascalCaseOrderNo如果不做配置最容易出现“对象不为 null 但字段全是默认值”的现象。在 Program.cs 里加一段配置builder.Services.AddControllers() .AddJsonOptions(options { options.JsonSerializerOptions.PropertyNamingPolicy JsonNamingPolicy.CamelCase; options.JsonSerializerOptions.PropertyNameCaseInsensitive true; options.JsonSerializerOptions.Encoder System.Text.Encodings.Web.JavaScriptEncoder.Default; });PropertyNamingPolicy CamelCase让序列化时把OrderNo输出成orderNo两端命名习惯就对上了PropertyNameCaseInsensitive是双保险前端万一传了orderno也能匹配。做完这步前后端 JSON 传值的大部分字段名问题都能消掉调试时不再需要逐个字段对大小写。4.3 POST-Redirect-GET怎么避免刷新重复提交表单提交后直接return View()有一个隐患用户按 F5 刷新浏览器会重新提交上一次的 POST 请求订单被创建两次。业界标准做法是 PRG 模式控制器代码长这样[HttpPost] public IActionResult Create(OrderViewModel order) { if (!ModelState.IsValid) return View(order); TempData[Notice] 创建成功; return RedirectToAction(nameof(Index)); } public IActionResult Index() { ViewBag.Notice TempData[Notice]; return View(); }处理完业务先RedirectToAction浏览器收到 302 后主动 GET 一次Index地址栏变成/Order/Index。这时候按 F5 刷新只是重复 GET不会重新提交表单。TempData在这里承担“一次性提示”的职责它只存活到下一次请求读取正好匹配“跳转后显示一条成功信息刷新后消失”的交互需求。需要提醒的是TempData默认读一次就会标记删除如果跳转后的页面还要在别处再读一次第二次就拿到 null。保留数据要用TempData.Peek(Key)或TempData.Keep(Key)后者适合在同一个请求里后续还要用的场景。5. 前后端传值避坑5 条让调试翻车的排查清单5.1 参数全是 null先看请求体再对签名现象POST 提交后成功进入 Action但参数对象非 null内部字段全是 null 或 0。原因前端 input 没有写name属性或者name与后端 ViewModel 属性名不一致。另一个常见原因是表单里用了disabled控件——禁用状态的字段根本不参与提交值到不了后端。解决打开浏览器 F12 的 Network 面板找到提交请求看 Form Data 或 Payload 里实际有哪些字段名再与 Action 参数逐个核对。光靠肉眼对源码效率太低请求体是真相。public IActionResult Create(OrderViewModel order) { ... } // 前端必须是 input nameOrderNo /OrderNo 才能绑上5.2 JSON 提交后端对象全是默认值现象fetch 提交 JSON后端[FromBody]参数对象不是 null但所有属性都是默认值——字符串 null数字 0。原因两种可能。一是Content-Type没设成application/json浏览器默认发text/plain后端解析不到请求体二是 JSON key 用了 camelCaseC# 属性是 PascalCase且项目没有配置命名映射策略。解决先确认请求头再用 curl 模拟一次curl -i -X POST http://localhost:5000/Order/Save \ -H Content-Type: application/json \ -d {orderNo:A001}如果 curl 能通而页面不行问题在前端如果 curl 也不行检查后端AddJsonOptions的 JSON 匹配配置。字段名大小写这种问题配置了PropertyNameCaseInsensitive true后基本能根治。5.3 ViewBag 传 List视图 foreach 时炸掉现象控制器里ViewBag.List items;视图里foreach (var item in ViewBag.List)第二行抛NullReferenceException或类型转换异常。原因ViewBag 是 dynamic编译器不检查类型。控制器实际塞进去的是 null或者塞的是别的类型视图运行时才暴露。这个黑匣子行为在传值里最容易让人迷惑。解决主数据一律改用强类型视图。控制器里改成return View(items);视图声明model IReadOnlyListOrderItem。如果确实要用 ViewBag 传辅助数据塞之前先判空视图里再用as转换并判 null。5.4 TempData 只读了一次就没了现象PRG 跳转后第一次显示“创建成功”用户刷新页面提示消失或者跳到第二个页面时拿不到。原因TempData默认读一次即标记删除请求结束时就清理了。第二次读取自然为空这不是偶发问题是它的设计行为。解决需要保留时改用Peek读取或在读取前调用Keep。var notice TempData.Peek(Notice) as string; // 读但不删除 TempData.Keep(Notice); // 保留到下一次请求5.5 文件上传始终拿到 null现象选了文件点提交IFormFile file参数是 null。原因form 标签没写enctypemultipart/form-data。浏览器默认用application/x-www-form-urlencoded编码文件内容不会按 multipart 格式发送后端自然拿不到。另一个常见原因是 input 的name与参数名不一致。解决form 上明确写enctypemultipart/form-datainput 的namefile和参数IFormFile file对齐。控制器开头加快速失败if (file null || file.Length 0) return BadRequest(没有拿到文件);尽早暴露问题而不是带着 null 往下走日志排查会省很多时间。6. 把传值收口成一套约定提交单元、命名与验收习惯6.1 给自己定三条约定传值代码写多了会发现大部分返工都来自没有约定。我现在的做法是给自己定三条规则一个 Action 只接收一个 ViewModel 参数把它看作一个完整的“提交单元”散装参数最多留给id这种查询场景前端name或 JSON key 与 C# 属性名保持一致相差的部分由配置统一负责转换而不是靠人肉记住哪个字段改过名返回视图时统一强类型 ViewModel返回 JSON 时用 DTO 或匿名对象别把 EF 实体直接序列化——导航属性循环引用会让你序列化直接炸掉。6.2 验收习惯三个 grep提交代码前我会做三个快速检查页面里出现的每个name字段都应该能在 ViewModel 里 grep 到对应属性控制器return View(模型)的类型必须与视图顶行model声明一致每次调接口发现传值问题先看 Network 里的 Payload 再下结论不要第一时间怀疑后端。注意传值问题的根源往往在“两端各改了一边”前端代码和后端代码不是同一个人维护时这个检查尤其重要。我最早做 MVC 项目时习惯用 ViewBag 到处塞数据项目到中期一个字段重构直接劝退改一个属性名全站静态查找。后来把传值收口成强类型 ViewModel字段名错了编译期就报出来才算把前后端传值这件事真正握在手里。如果你已经在传值上花过不少冤枉时间从下一个功能开始把提交单元、命名策略和强类型视图这三件事定下来后面会顺畅很多。希望帮到你。本文还有配套的精品资源点击获取
返回列表