MVC的Views中使用递归生成Html【转】
在开发过程中往往会有一个需求,就是将一个树状的数据结构在视图中表示出来。例如最传统的多级分类,系统中有一系列根分类,每个分类中又带有一些子分类,而我们的目标便是在页面上生成一个由ul和li嵌套组成的HTML结构。这个问题看似简单,但是如何让实现变的轻松、易于使用也是一个值得讨论的问题。这次就来谈谈这部分的情况。
实现目标
首先来明确一下实现目标。例如我们有一个Category对象,表示一个类别:
public class Category
{
public string Name { get; set; } public List<Category> Children { get; set; }
}
然后我们准备一个嵌套的数据结构:
public ActionResult Categories()
{
var model = new List<Category>
{
new Category
{
Name = "Category 1",
Children = new List<Category>
{
new Category
{
Name = "Category 1 - 1",
Children = new List<Category>()
},
new Category
{
Name = "Category 1 - 2",
Children = new List<Category>()
},
}
},
new Category
{
Name = "Category 2",
Children = new List<Category>
{
new Category
{
Name = "Category 2 - 1",
Children = new List<Category>()
},
new Category
{
Name = "Category 2 - 2",
Children = new List<Category>()
},
}
},
}; return View(model);
}
自然还会有一个Model类型为List<Category>的视图:
<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<List<Category>>" %> ...
而我们的目标,便是要在视图中显示出这样的HTML:
<ul>
<li>Category 1
<ul>
<li>Category 1 - 1 </li>
<li>Category 1 - 2 </li>
</ul>
</li>
<li>Category 2
<ul>
<li>Category 2 - 1 </li>
<li>Category 2 - 2 </li>
</ul>
</li>
</ul>
那么我们又该怎么做呢?
使用局部视图
如果在平时让我们处理这种数据结构,很明显会使用递归。但是,在视图模板中表示递归是非常困难的,因此我们会借助局部视图。例如:
<%@ Control Language="C#" Inherits="ViewUserControl<List<Category>>" %> <% if (Model.Count > 0) { %>
<ul>
<% foreach (var cat in Model) { %> <li>
<%= Html.Encode(cat.Name) %>
<% Html.RenderPartial("CategoryTree", cat.Children); %>
</li> <% } %>
</ul>
<% } %>
这个局部视图的作用便是生成我们想要的HTML片段。在局部视图内部还会调用自身来生成下一级的HTML。在主视图中生成局部视图也很容易:
<% Html.RenderPartial("CategoryTree", Model); %>
这就实现了递归,也是实现这一功能最易于理解的方式。只可惜这种做法比较麻烦,需要定义额外的局部视图。这种局部视图往往只是为一个主视图服务的,它会和主视图的前后环境相关,分离开去在维护上就会有些不便了。
在页面中定义委托
我们知道,在运行时ASP.NET页面会被编译为一个类,而其中的各种标记,或内嵌的代码都会被作为一个方法里定义或执行的局部变量。如果说我们要在一个方法内“定义”另一个方法,自然只能是使用匿名方法的特性来构造一个委托了。这个委托为了可以“递归”调用,就必须这么写:
<% Action<List<Category>> renderCategories = null; // 先设为null %>
<% renderCategories = (categories) => { // 再定义 %> <% if (categories.Count > 0) { %>
<ul>
<% foreach (var cat in categories) { %> <li>
<%= Html.Encode(cat.Name) %>
<% renderCategories(cat.Children); %>
</li> <% } %>
</ul>
<% } %> <% }; %> <% renderCategories(Model); // 最后再调用,即生成HTML %>
这段代码的确可以生成HTML,但是我不喜欢。我不喜欢的原因倒不是因为这是我眼中的“伪递归”,而是因为这在页面将“定义”与“执行”分开了。事实上,在我们看到HTML标记及逻辑控制的地方并没有在“同时”生成内容,这只是在“定义”。生成内容的时机其实是在最后对renderCategories委托的调用,这容易造成一定误导,因为最后的“生成”可能会遗漏,而定义和生成之间可能会插入一些其他内容。
这种做法的优势,就是在于不用额外分离出一个局部视图,它直接写在主视图中,易于维护,也相对易于理解。
使用Lambda表达式构建递归方法
“定义”与“执行”分离的一个重要原因,还是因为Lambda表达式无法定义递归函数。否则,我们就可以直接定义一个递归执行的委托,并在最后跟上Invoke或直接调用即可。
因此,其实这里就正是使用Lambda表达式编写递归函数的用武之地。例如,我们补充一个类似的Fix方法:
public static class HtmlExtensions
{
public static Action<T> Fix<T>(this HtmlHelper helper, Func<Action<T>, Action<T>> f)
{
return x => f(Fix(helper, f))(x);
}
}
于是在视图中便可以:
<% Html.Fix<List<Category>>(render => categories => { %> <% if (categories.Count > 0) { %>
<ul>
<% foreach (var cat in categories) { %> <li>
<%= Html.Encode(cat.Name) %>
<% render(cat.Children); %>
</li> <% } %>
</ul>
<% } %> <% }).Invoke(Model); %>
不过严格说来,它还是“定义”与“执行”分离的,只是我们现在可以把它们写在一块儿。此外,Fix方法对于模板中的HTML生成实在没有什么意义。
提供一个Render方法辅助递归
Fix方法对页面生成没有什么作用,不过如果有一个可以辅助递归的Render方法便有意义多了:
public static class HtmlExtensions
{
private static Action<T> Fix<T>(Func<Action<T>, Action<T>> f)
{
return x => f(Fix(f))(x);
} public static void Render<T>(this HtmlHelper helper, T model, Func<Action<T>, Action<T>> f)
{
Fix(f)(model);
}
}
于是,我们在页面中就可以这么写:
<% Html.Render(Model, render => categories => { %> <% if (categories.Count > 0) { %>
<ul>
<% foreach (var cat in categories) { %> <li>
<%= Html.Encode(cat.Name) %>
<% render(cat.Children); %>
</li> <% } %>
</ul>
<% } %> <% }); %>
您是否觉得这么做难以理解?我不这么认为,因为从语法上来说,这种HTML生成方式是很简单的:
<% Html.Render(参数, 用于递归的方法 => 当前参数 => { %> ... <% 递归调用 %> ... <% }); %>
至于背后的原理?关心那些做什么。
性能
可惜,根据性能比较,使用Fix构造递归的做法,比使用SelfApplicable的做法要慢上许多。虽然我认为这里不会是性能的关键,但如果您实在觉得无法接受的话,也可以利用SelfApplicable来构造递归的HTML呈现方式。其辅助方法为:
public delegate void SelfApplicable<T>(SelfApplicable<T> self, T arg); public static class HtmlExtensions
{
public static void Render<T>(this HtmlHelper helper, T model, SelfApplicable<T> f)
{
f(f, model);
}
}
于是在视图中:
<% Html.Render(Model, (render, categories) => { %> <% if (categories.Count > 0) { %>
<ul>
<% foreach (var cat in categories) { %> <li>
<%= Html.Encode(cat.Name) %>
<% render(render, cat.Children); %>
</li> <% } %>
</ul>
<% } %> <% }); %>
同样,我们只要记住这么做的“语法”就可以了。
总结
相比之下,我喜欢最后两种做法。因为他们直接构造了“HTML生成”的功能,且“内置”了递归。如果使用一个额外的局部视图,虽然“朴素”但使用较为麻烦。使用“伪递归”的方式,从概念上看这不太像是在生成HTML,程序构造的痕迹(先声明,再定义,最后调用)过于明显了。
源文地址:http://blog.zhaojie.me/2009/09/rendering-tree-like-structure-recursively.html
MVC的Views中使用递归生成Html【转】的更多相关文章
- ASP.NET MVC 中的视图生成
关于 ASP.NET MVC 中的视图生成 在 ASP.NET MVC 中,我们将前端的呈现划分为三个独立的部分来实现,Controller 用来控制用户的操作,View 用来控制呈现的内容,Mode ...
- asp.net mvc+EF 递归生成树结构返回json
0.数据表结构,主要属性有:Id.parentId(父节Id).Text.Url……等等. 1.新建一个树结构MenuModels public class MenuModels { private ...
- MVC中验证码的生成
在项目中验证码的生成通常是需要页面无刷新的,所以验证码图片实际是跟在某个input后面的img,通过控制该img来控制验证码显示的位置,例如: <div> <input id=&qu ...
- MVC 在控制器中获取某个视图动态的HTML代码
ASP.NET MVC 在控制器中获取某个视图动态的HTML代码 如果我们需要动态的用AJAX从服务器端获取HTML代码,拼接字符串是一种不好的方式,所以我们将HTML代码写在cshtml文件中, ...
- ASP.NET MVC开发学习过程中遇到的细节问题以及注意事项
1.datagrid中JS函数传值问题: columns: { field: 'TypeName', title: '分类名称', width: 120, sortable: true, format ...
- [渣翻译] 在ASP.NET MVC WebAPI项目中使用 AngularJS
原文地址http://blog.technovert.com/2013/12/setting-up-angularjs-for-asp-net-mvc-n-webapi-project/ 我们最近发布 ...
- 在ASP.NET MVC应用程序中实现Server.Transfer()类似的功能
在ASP.NET MVC应用程序中,如果使用Server.Transfer()方法希望将请求转发到其它路径或者Http处理程序进行处理,都会引发“为xxx执行子请求时出错”的HttpException ...
- 在MVC应用程序中动态加载PartialView
原文:在MVC应用程序中动态加载PartialView 有时候,我们不太想把PartialView直接Render在Html上,而是使用jQuery来动态加载,或是某一个事件来加载.为了演示与做好这个 ...
- ASP.NET MVC 3: Razor中的@:和语法
原文 ASP.NET MVC 3: Razor中的@:和语法 [原文发表地址] ASP.NET MVC 3: Razor’s @: and <text> syntax[原文发表时间] De ...
随机推荐
- 【Beta】Scrum Meeting 8
前言 Beta阶段第8次会议在5月13日22:00由PM在大运村一公寓三层召开, 时长30min. 任务分配 姓名 今日任务 明日任务 困难 周博闻 修复修改密码问题#54 添加主页公告栏 #57实现 ...
- [Beta]Scrum Meeting#5
github 本次会议项目由PM召开,时间为5月10日晚上10点30分 时长15分钟 任务表格 人员 昨日工作 下一步工作 木鬼 撰写博客整理文档 撰写博客整理文档 swoip 改进界面 改进界面 b ...
- JavaScript初探系列(五)——this指向
一.涵义 this关键字是一个非常重要的语法点.毫不夸张地说,不理解它的含义,大部分开发任务都无法完成.this可以用在构造函数之中,表示实例对象.除此之外,this还可以用在别的场合.但不管是什么场 ...
- stream_context_create解析
(PHP 4 >= 4.3.0, PHP 5, PHP 7) stream_context_create — 创建资源流上下文 说明¶ stream_context_create ([ arra ...
- ffmpeg fails with error "max delay reached. need to consume packet"
rtsp服务默认使用udp协议,容易丢包,报这个错误.改为tcp,则解决. ffmpeg-设置rtsp推流/拉流使用的协议类型(TCP/UDP)(转) 拉流(设置TCP/UDP) //设置参数 AVD ...
- unix udp sendto 最大可发送的数据长度
sendto 的最大可发送数据长度受限于两个值. 第一 [2^16 -1 - 8 -20] 第二 [SO_SNDBUF] 解释受限于[2^16-1-8-20] 数据封装过程 第一步: 用户层 : us ...
- github 体积限制
github 体积限制 仅供了解,切勿滥用.希望自觉维护良好社区. 单文件 单个文件大于 50M 时会收到警告, 100M 时会被拒绝. 该警告将告诉您哪些文件太大: remote: warning: ...
- python初级(302) 6 对象(三)
一.复习 1.什么是魔法方法? 2.什么是类的初始化函数? 二.什么是self 使用一个类可以创建多个对象实例,例如: ball1 = Ball("red", "smal ...
- ABAP基础篇1 内表
内表类型 abap 内表类型有三种: 标准表(一般ABAP程序中用的最多就是这种表) 系统为该表的每一行数据生成一个逻辑索引,自己内部维护着行号(Index)的编码.表的键值不唯一,且没有按照表键自动 ...
- microsoft vs code 绿化
下载地址: https://code.visualstudio.com/#alt-downloads 各个版本比较 User Installer System Installer .zip resou ...