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

时间: 2024-07-30 16:51:24

MVC的Views中使用递归生成Html【转】的相关文章

asp.net mvc+EF 递归生成树结构返回json

0.数据表结构,主要属性有:Id.parentId(父节Id).Text.Url……等等. 1.新建一个树结构MenuModels 1 public class MenuModels 2 { 3 private int _id; 4 private string _text; 5 private int? _parentid; 6 private string _icon; 7 private string _url; 8 private object _menus; 9 private Dic

C# 中采用treeview递归生成目录树(Winform和Webform两种)

部门表: 课程表: 查询结果结构: 数据结构分析,部门分为部门id和部门名称:课程分为课程id,课程名称,课程路径和课程所属部门. 要求以部门为父节点展示不同部门下的课程. Winform采用treeview递归生成目录树using System;using System.Collections.Generic;using System.ComponentModel;using System.Data;using System.Drawing;using System.Linq;using Sy

ASP.NET MVC开发学习过程中遇到的细节问题以及注意事项

1.datagrid中JS函数传值问题: columns: { field: 'TypeName', title: '分类名称', width: 120, sortable: true, formatter: function (value, row, index) { var contentDetails = "<a href='' style='text-decoration: none;' onclick='showDetailsDialog(" + row.ID + &q

Linq递归生成easyui-tree

我把整个类放这里大加参考: public class ClassOrganization    {        static List<Model.ClassOrganization> org;        public List<Model.ClassOrganization> GetOrgTree(string _pid)        {            List<Model.ClassOrganization> porg = new List<M

在MVC的项目中访问静态页面

MVC在生成项目的时候会生成的WEB-INF底下.这个文件夹下面的文件是受保护的,都会走MVC的流程, 但是我希望在WebContent底下可以使用静态页面, 那么需要进入springmvc-servlet.xml 页面,加上一句话:  <mvc:resources mapping="/**"  location="/**" /> 这样,使用mvc:resources标签,可以将根目录下的文件全部定义为静态html,直接访问即可. 需要放到文件夹中: &

在ASP.NET MVC应用程序中实现Server.Transfer()类似的功能

在ASP.NET MVC应用程序中,如果使用Server.Transfer()方法希望将请求转发到其它路径或者Http处理程序进行处理,都会引发“为xxx执行子请求时出错”的HttpException异常.而在最终实现Server.Transfer()操作的方法内部,我看到这样几行代码. else if (!(handler is Page)) { error = new HttpException(0x194, string.Empty); } 很明显,在方法内部,所有的IHttpHandle

1.仓储模式在MVC应用程序中的使用

目录 1.仓储模式在MVC应用程序中的使用 2.泛型仓储模式在MVC应用程序中的使用 3.MVC Code-First和仓储模式的应用 4.待续.... 这篇文章中,我会解释仓储模式在MVC程序中的使用. 首先,我们需要理解什么是仓储模式[repository Pattern],来看看下面的图片 没有使用仓储模式的MVC应用程序:      使用了仓储模式的MVC应用程序: 仓储模式,是一个抽象层,它将数据库的访问逻辑,映射到实体的访问逻辑. 下面,我们来看做一个应用程序,来体验一下仓储模式吧.

MVC应用程序中管理(更新)上传的文件

实现上传文件功能,有时上传也会操作出错,能让用户有改正有机会,开发上传文件能有更新的功能. 文件上传时,如果是存储于应用程序某一目录的话,在更新时需要了解一些流程,先是删除旧文件,更新数据表相关信息,存储新文件. 本篇让你了解到MVC与jQuery的交互处理. 在数据库中,新建一个更新的存储过程: 找到并打开FileLibraryEntity.cs,添加一个vlid更新方法: 在ExerciseController.cs控制器中,创建一个更新Action: A标记,删除旧文件. B标记,获取新上

在MVC应用程序中动态加载PartialView

有时候,我们不太想把PartialView直接Render在Html上,而是使用jQuery来动态加载,或是某一个事件来加载. 为了演示与做好这个练习,我们先在Views目录下的Home下创建_Partial1.cshtml部分视图,视图内容任你自定义,Insus.NET在本例中只让其显示一些文字与一张图片: 接下来,我们需要建立一个ActionResult()方法,在Controllers目录之下,打开HomeController.cs: 再去Views\Home目录,创建一个DynamicL