ASP.NET MVC PartialView实战:四种渲染方式与经典用例
1. PartialView到底是什么——先把它和普通视图分清楚1.1 一句话定义PartialView部分视图在ASP.NET MVC里指的是一个不包含完整页面结构的视图片段。它没有html、head、body这些外壳标签只是一段可以被其他视图反复引用的可复用UI模块本质上是把页面上重复出现的区块拆出来单独管理。打个比方普通View是整栋房子的完整装修方案PartialView就是其中某一间房间的标准样板间。哪个楼层需要这间房直接把样板间复制过去就行不用每个楼层都重新设计一遍。放在项目里一个商品卡片、一个分页器、一组筛选条件、一条新闻列表都可以做成PartialView。1.2 PartialView与普通View的区别很多刚接触MVC的开发者容易混淆这两者其实它们的定位完全不同。普通View是请求的直接响应结果对应一个Action承载的是整个页面的骨架PartialView只是普通View内部的一个零件必须依托于某个View才能被浏览器最终看到。从文件角度说两者都是.cshtml文件在编译和语法上没有任何区别。区别完全在于约定和用法对比项普通ViewPartialView文件位置Views下的对应控制器目录通常放Views/Shared便于全局复用是否独立响应请求是Action直接return View()否必须由其他View或Action调用是否包含完整HTML结构包含不包含文件名约定无强制要求通常以_开头如_ProductList.cshtml数据来源由对应Action传入由调用方以参数形式传入_开头的命名约定不是强制要求但几乎是行业共识。一方面它明确告诉团队成员这个文件是给其他视图引用的不是独立页面另一方面在Visual Studio里搜索文件时也更直观。1.3 什么时候才需要PartialView不是所有重复UI都值得拆成PartialView拆得太碎会让项目变成一堆零散文件的集合反而难维护。基于几年的实战经验真正需要拆的场景大概有这几类。跨页面复用的区块比如整个站点所有页面都有的侧边栏、页脚、推荐位这类组件的渲染逻辑一致只是数据可能不同最值得拆成PartialView放到Shared目录。单个页面内多次出现的同构区块比如一个后台商品管理页面里上架商品、下架商品、待审核商品三个Tab都展示同一套商品卡片结构只是数据源不同。这种情况明显应该把商品卡片拆成PartialView。需要Ajax局部刷新的区块当一个页面里某个区域要单独异步刷新时这个区域天然就应该是一个PartialView。服务器端只返回这段HTML前端拿回来替换指定容器页面其他部分完全不受影响。逻辑独立的复杂区块比如一个包含大量条件判断和循环的视图文件长期在多个页面中出现每次改需求都要同步改多处这种代码异味也是在提示你该提取PartialView了。反过来如果一个区块只有一处用、以后也不太可能被别的页面复用那就没必要拆。过早的拆分会增加文件数量也增加查找成本合理评估复用频率和逻辑复杂度再动手。2. 四种渲染方式Partial、RenderPartial、Action、RenderAction怎么选2.1 四种方式对照ASP.NET MVC提供了四种在视图中渲染PartialView的方式分别是Html.Partial、Html.RenderPartial、Html.Action、Html.RenderAction。前两者是直接渲染当前模型或自定义数据后两者会经由子Action走一遍控制器逻辑再返回结果。!-- 方式一Html.Partial有返回值需要接收 -- Html.Partial(_ProductCard, Model) !-- 方式二Html.RenderPartial直接输出到响应流 -- { Html.RenderPartial(_ProductCard, Model); } !-- 方式三Html.Action经子Action渲染 -- Html.Action(ProductCard, Product) !-- 方式四Html.RenderAction经子Action直接输出 -- { Html.RenderAction(ProductCard, Product); }这四个方式最大的区别在于Partial系是直接把传进来的数据渲染成HTML完全不需要经过任何Action逻辑适合那些给什么数据就展示什么的纯展示型组件。Action系则会重新派发一个子请求到指定的子Action由那个Action去准备数据再返回PartialView适合那些需要自行加载数据的场景。2.2 传参方式Partial和RenderPartial的传参非常直接要么传模型对象要么靠ViewDataDictionary带附加信息。Html.Partial(_ProductCard, Model, new ViewDataDictionary { { ShowPrice, true }, { CssClass, card-lg } })子Action接收端的方式也简单[ChildActionOnly] public ActionResult ProductCard(string categoryId) { var products _service.GetByCategory(categoryId); return PartialView(_ProductCard, products); }注意ChildActionOnly这个特性它保证该Action只能被子请求调用不能用Url直接在浏览器里访问。这是安全性的关键否则用户完全能通过/Product/ProductCard?categoryIdxx绕过页面直接拿数据接口。子Action里可以用ViewBag给PartialView传递辅助数据PartialView内部也能通过Model拿到主数据。这里有个细节很多人踩过坑Html.Action方式下子Action里设置ViewBag.xxx并不会传回主视图页面它只对PartialView内部可见但对Html.Partial来说它会共享当前视图的ViewData和ViewBag内容。2.3 性能对比和选择建议RenderPartial系比Partial系快一点点因为Html.Partial会把生成的HTML字符串先放到内存缓冲区再返回给调用方拼接而RenderPartial直接把内容写入响应流少了一次字符串对象的创建和拼接开销。这点差别在单个页面调用几十个PartialView时会体现出来。Partial系的性能整体优于Action系因为Html.Action额外多了一次子请求派发、路由匹配、控制器实例化和Action执行的过程。那些完全不需要后端逻辑的静态区块用Partial就够完全没必要绕一圈子Action。但有些场景必须用Action系组件自身需要去数据库查询数据、组件需要根据当前路由参数动态生成内容、多个页面共享这个组件但数据来源各自不同。这种情况下子Action把数据准备和视图渲染封装成完整单元调用方完全不用关心数据怎么来的。实际项目里我的使用倾向是纯UI复用用Partial或RenderPartial带数据加载逻辑的复用组件用Action或RenderAction。前者是零件思维后者是微服务思维两个都有应用场景问题只在于选对场景。3. 经典用法一列表页的公共区块拆解3.1 场景描述拿一个典型的电商后台商品管理页举例。页面上方是筛选条件区中间是商品列表底部是分页器。其中商品卡片这一块在全部商品已上架已下架库存预警四个Tab里结构一模一样只是数据来源和数量不同。如果每个Tab各自把卡片HTML复制一份那后续改动卡片样式就要同步改四个位置漏掉一个就是事故。这其实就是典型的PartialView应用场景。3.2 实现步骤第一步先建PartialView文件。在Views/Shared下新建_ProductCard.cshtml内容是单个商品卡片的HTML结构。model WebApplication1.Models.Product div classproduct-card div classproduct-image img srcModel.CoverImage altModel.Name / /div div classproduct-info h3 classproduct-nameModel.Name/h3 p classproduct-priceModel.Price.ToString(N2)/p p classproduct-stock库存Model.Stock/p /div /div第二步在商品列表主视图里用循环调用这个PartialView每件商品传入一份模型数据。model IEnumerableWebApplication1.Models.Product div classproduct-list foreach (var item in Model) { Html.Partial(_ProductCard, item) } /div第三步在于实际页面的数据来源可能不同比如库存预警Tab只需要过滤库存低于10的商品。这类组件不变、数据源变化的需求是PartialView最舒服的配合方式每个Tab的Action各自查询数据视图层面只关心循环渲染。从维护角度来说以后改卡片样式只需要改_ProductCard.cshtml一个文件所有引用位置同步生效。3.3 数据传递_ProductCard的模型类型是Product但有些情况下需要额外传递当前Tab类型或是否可编辑这类标记信息这时候可以用ViewDataDictionary作为第三个参数。Html.Partial(_ProductCard, item, new ViewDataDictionary(ViewData) { { TabType, sale }, { ShowEditButton, true } })PartialView内部这样读取{ var tabType ViewData[TabType] as string; var showEdit (bool)(ViewData[ShowEditButton] ?? false); }这种做法保证了主模型的类型纯粹性又给PartialView增加了额外的上下文信息。要注意ViewDataDictionary(ViewData)这个写法它会把当前视图已有的ViewData复制一份避免丢失主页面已经设置的数据。如果直接new ViewDataDictionary()里面的Model会被置空PartialView的强类型模型就会失效。4. 经典用法二用PartialView做侧边栏和轮播区4.1 侧边栏复用企业官网的侧边栏通常包含最新文章列表、热门推荐、标签云几个部分这些内容在不同页面基本一样。做成PartialView后放到Views/Shared/_Sidebar.cshtml全站共用。这个场景我推荐用Html.Action方式因为侧边栏数据往往需要从数据库拉取而且多个页面都要复用同一份数据准备逻辑。[ChildActionOnly] public ActionResult Sidebar() { var recentArticles _articleService.GetRecent(5); var hotArticles _articleService.GetHot(5); var sidebarVm new SidebarViewModel { RecentArticles recentArticles, HotArticles hotArticles }; return PartialView(_Sidebar, sidebarVm); }主视图里只需要写一行Html.Action(Sidebar, Shared)前端页面调用即可不需要关心Sidebar数据怎么来的。维护时改_Sidebar.cshtml全站侧边栏样式同步更新。这里提醒一点尽量不要用路由Url直接请求子Action来输出PartialView比如在页面里fetch(/Shared/Sidebar)拿到HTML再塞进侧边栏。这样做的坏处是子Action的URL暴露给了前端一旦被用来做恶意高频请求没有ChildActionOnly保护的Action就会变成免费数据接口。而且子Action通常不设防跨站请求防护安全隐患比较大。4.2 静态PartialView还有一种很省事的用法——将完全不涉及数据的静态HTML片段也做成PartialView。比如首页轮播图如果只是几张图片加固定文案不涉及动态数据可以直接把轮播代码放进_BannerCarousel.cshtml然后在需要的页面直接引用。Html.Partial(_BannerCarousel)静态PartialView最大的好处是让主视图文件瘦身。一个首页文件动辄几百行轮播、导航、推荐、关于我们、合作伙伴、页脚等内容全部挤在一起阅读和修改都很痛苦。拆开后主视图变成了一组有序的引用清单每个区块的实现细节都在独立文件中定位问题快得多。这类零参数的PartialView连模型都不需要定义PartialView内部不写model指令即可。不过要注意如果PartialView没写model访问Model属性时拿到的是dynamic类型一旦尝试读取不存在的属性会抛异常调试时有点绕建议还是显式声明模型类型哪怕是定义个空类或者直接声明成dynamic至少意图清晰。5. 经典用法三Ajax局部刷新5.1 处理Ajax请求PartialView最让人舒服的应用场景之一是Ajax局部刷新。比如一个订单列表页用户点击下一页时只刷新列表区域不重新加载整个页面。传统的做法是后端直接返回JSON数据前端遍历拼HTML字符串不仅代码啰嗦拼接HTML还要小心XSS注入。用PartialView就简单了后端直接把PartialView渲染好的HTML字符串传回前端。后端写法public ActionResult OrderList(int page 1) { var orders _orderService.GetPaged(page, 10); var vm new OrderListViewModel { Orders orders, CurrentPage page, TotalPages ... }; // 如果是Ajax请求只返回局部视图否则返回完整页面 if (Request.IsAjaxRequest()) { return PartialView(_OrderList, vm); } return View(vm); }Request.IsAjaxRequest()方法是ASP.NET MVC内置的扩展方法用来判断请求头里是否带X-Requested-With: XMLHttpRequest标记。很多前端框架如jQuery的$.ajax默认会带这个头所以这个判断方式在传统MVC项目里非常可靠。5.2 前端代码前端用jQuery就可以完成局部刷新我把常用的写法直接贴出来。function loadOrders(page) { $.ajax({ url: /Order/OrderList, type: GET, data: { page: page }, success: function(html) { $(#order-list-container).html(html); }, error: function() { toastr.error(加载失败请重试); } }); }关键点是主视图里的#order-list-container容器要和_OrderList返回的最外层节点对应。例如_OrderList.cshtml里最外层是div idorder-list-body !-- 循环渲染订单行 -- /div那么前端替换的容器应该选择这个#order-list-body而不是包裹它的外层div否则会把分页器等不该替换的结构也一并替换掉。在分页场景中常见的做法是下一页的URL直接指向触发了Ajax的Action而不是完整的页面Url。模板里可以这样写a hrefjavascript:void(0); onclickloadOrders((Model.CurrentPage 1))下一页/a这里要注意如果当前页已经是最后一页要禁止继续点击否则后端会返回空列表界面没有提示也不好看。可以在PartialView里加判断最后一页时隐藏下一页按钮。6. 常见坑和排查技巧实录6.1 PartialView文件找不到最常见的问题是这个报错The partial view xxx was not found or no view engine supports the searched locations.排查思路很固定。先确认文件确实在Views/Shared目录或者对应控制器的Views/ControllerName目录下然后确认文件名拼写完全一致。MVC视图引擎默认搜索顺序是Views/当前控制器目录、Views/Shared如果这两个位置都没有就会报错。遇到过一种隐蔽情况文件路径是对的、名字也对但还是找不到。后来发现是.cshtml文件没有包含在项目文件里或者文件是后来手动拷进目录但项目没刷新。用Visual Studio打开解决方案资源管理器右键点击Views文件夹选择刷新基本就能解决。6.2 强类型PartialView的Model为nullHtml.Partial(_Card, item)写法正确但在PartialView里检查Model是null。最常见的原因是调用方传递的模型本身是null。比如foreach循环空了或者集合为null循环体不执行当然没问题但如果你在PartialView里直接Model.Id就会炸。更隐蔽的原因是前面提到过的ViewDataDictionary覆盖问题。当你写了new ViewDataDictionary()并传了额外的键值但没有把原ViewData的Model带过去PartialView里的Model就是null。正确做法是new ViewDataDictionary(ViewData) { { ExtraKey, value } }6.3 ViewDataViewBag共享导致的污染问题Html.Partial和Html.RenderPartial默认共享主视图的ViewData和ViewBag这既是便利也是隐患。假设主视图设置了一个ViewBag.TitlePartialView内部如果也修改ViewBag.Title修改会传回主视图吗答案是——会。因为ViewBag本质上是对ViewData的动态包装Partial系不隔离数据上下文子视图对ViewBag的修改会影响调用方。这在某些场景会导致页面标题、页面状态等数据被意外篡改。如果希望PartialView内部有独立的数据上下文可以用Html.Action方式子Action里设置的ViewBag不会回传主视图天然隔离。6.4 缓存策略问题PartialView在页面里渲染的内容是否能缓存、缓存多久很多人没有规划过。对于纯静态内容或低频变化的内容直接对PartialView做OutputCache没什么问题但要注意Html.Action方式下子Action的缓存行为和主页面缓存会有交互。实际项目中遇到过一次诡异现象子Action设置了缓存主页面没设置缓存结果无论主页面请求多少次子Action多次返回的都是同一批数据。原因是OutputCache对整个Action的输出做了缓存后续所有请求直接命中缓存。如果这正好符合需求那没问题但如果这个侧边栏数据其实是登录用户个性化内容这就严重错了。记住一个原则内容千人千面的PartialView永远不要做输出缓存全站一致的公共内容缓存收益才最大化。6.5 局部刷新时JavaScript失效Ajax加载PartialView后PartialView内部包含的事件绑定函数通常不会被正常执行因为新插入的DOM节点是动态生成的页面初始化时绑定的事件钩子绑不到它们身上。解决办法有两个。一是在Ajax成功回调里手动重新绑定事件把事件绑定逻辑抽成一个可复用的函数每次加载完后调用一次二是事件绑定用事件委托把事件处理绑定到容器或document上用事件冒泡机制捕获。事件委托更推荐因为不管内容怎么刷新绑定在容器上的监听器始终保留也就不用重复绑定了。6.6 一个容易忽略的性能问题如果页面里同一个PartialView被循环调用了上百次而且这个PartialView内部又使用了Html.Action去加载数据悲剧就发生了每循环一次就发一次子Action请求每次子Action都查一次数据库。做优化前先看看数据是否能一次查询到位。遇到这种场景强烈建议把列表循环改为整列表渲染。也就是说与其每条数据都调用PartialView去各自渲染不如在子Action或主逻辑里把全部数据一次性准备成ListProduct然后传入PartialView内部循环输出。这既保留了PartialView的复用性又把数据库压力从N次查询降到1次。最后说一点实际心得。PartialView这个功能看起来简单真正用好的关键在于边界划分和命名规范。每拆一个PartialView前先问一句这段UI是否真的会在多个地方复用它和主视图交互数据的方式是否清晰合理。带着这两个问题去做设计和重构你拆出来的每一个PartialView都会经得起时间和需求的考验。我在多个项目里用它做过商品卡片复用、公共侧边栏、区域刷新的分页组件每次都能把维护成本明显降下来。希望这篇内容能让你在实际项目里少走弯路。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →