在视图中使用递归生成树状结构


在开发过程中往往会有一个需求,就是将一个树状的数据结构在视图中表示出来。例如最传统的多级分类,系统中有一系列根分类,每个分类中又带有一些子分类,而我们的目标便是在页面上生成一个由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,程序构造的痕迹(先声明,再定义,最后调用)过于明显了。

  您喜欢哪种做法呢?如果您遇到了我这样的需求,您会怎么做呢?

  最后我想进行一个小调查:您满意WebForm的页面作为视图模板引擎吗?您平时最喜欢使用什么视图模板引擎,为什么呢?


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3