稍微有一定复杂性的系统,多级菜单都是一个必备组件。

本篇专题讲述如何生成动态多级菜单的通用做法。

我们不用任何第三方的组件,完全自己构建灵活通用的多级菜单。

需要达成的效果:容易复用,可以根据model动态产生。

文章提纲

  • 概述要点 && 理论基础
  • 详细步骤

    一、分析多级目录的html结构

    二、根据html结构构建data model

三、根据data model动态生成树形结构

四、解析树形结构成html

  • 总结

概述要点 && 理论基础

要实现动态菜单,只要解决两个问题:

1. 前端调用

2. 后端可根据model生成菜单

前端调用我们通过自定义html helper的方法;

后端生成菜单我们通过Mvc的TagBuilder来实现。

一、如何自定义html helper?

前面系列文章我们专门介绍过html helpers,例如:
@Html.ActionLink("linkText","someaction","somecontroller",new { id = "123" },null)
生成结果:

<a href="/somecontroller/someaction/123">linkText</a>

本次专题我们需要自定义一个html helper用来生成菜单, 命名为GetMenuHtml

View中可以通过 @Html.GetMenuHtml() 实现输出菜单的html

先简单介绍下如何实现自定义的helper, 具体过程在详细步骤中再说。

一、定义一个public static的类,在此类中再添加一个public static的方法, 首个参数为 this HtmlHelper helper,该方法就可以像普通的html helper来使用。

二、前端引入类的命名空间:

@using XEngine.Web.Utility.MenuHelper

三、在要使用的地方添加:

@Html.SayHi()

二、MVC生成html标签

我们使用TagBuilder

System.Web.Mvc命名空间下TagBuilder的使用详细介绍:

https://msdn.microsoft.com/en-us/library/system.web.mvc.tagbuilder(v=vs.111).aspx

大家重点关注下方框部分,详细步骤中可以看到如何使用。

详细步骤

分成四大步骤

一、分析多级目录的html结构

首先打开一个样例,如下图

对应的html为

大家可以看到,菜单最外面的根节点是一个<li>, 后面跟一个<a>和<ul>, <ul>里面就是包裹的具体菜单。

具体菜单里面是<li>, 如果有子菜单通过<li><a><ul>来递归

二、根据html结构构建data model

根据上面的html结构,我们构建类似如下的SysMenu.

解析菜单时,只需要将相应的字段填充到html标签中即可。

[DatabaseGenerated(DatabaseGeneratedOption.None)]

[DisplayName("MenuID")]

public int ID { get; set; }

public int? ParentID { get; set; }

[DisplayName("名称")]

[StringLength(50)]

public string Name { get; set; }

public string Action { get; set; }

public string Controller { get; set; }

[DisplayName("图标")]

public string IconImage { get; set; }

public MenuTypeOption MenuType { get; set; }

public List<SysMenu> MenuChildren = new List<SysMenu>();

[DisplayName("描述")]

public string Description { get; set; }

其中 MenuTypeOption表示菜单的种类

三、根据data model生成树形结构

以一个多级菜单举例。

这个菜单中每一级对应一个SysMenu.

SysMenu之间有父子关系,通过MenuChildren来实现。

我们建立一个ViewModel,专门存放根菜单(根菜单下面的菜单可以根据MenuChildren来找到,不需要再专门保存)

public class MenuViewModel<T>

{

public IList<T> MenuItems = new List<T>();

}

先增加几笔测试数据

现在我们就来构建这个菜单的树形结构

public static MenuViewModel<SysMenu> CreateMenuModel(string menuName)

{

UnitOfWork unitOfWork = new UnitOfWork();

MenuViewModel<SysMenu> model = new MenuViewModel<SysMenu>();

// 1. 根据menuName获取开始的根菜单

unitOfWork.SysMenuRepository.Get(filter: m => m.Name == menuName).FirstOrDefault();

if (itemRoot != null)

{

// 2. 依次添加枝叶菜单

// 2.1 获取itemRoot的所有子菜单

IEnumerable<SysMenu> menus = unitOfWork.SysMenuRepository.Get(filter: m => m.ParentID == itemRoot.ID);

// 2.2 对每个子菜单进行递归 AddChildNode

foreach (var item in menus)

{

itemRoot.MenuChildren.Add(item);

AddChildNode(item);

}

}

}

//递归执行:找到menu子成员并添加

public static void AddChildNode(SysMenu menu)

{

UnitOfWork unitOfWork = new UnitOfWork();

var menus = unitOfWork.SysMenuRepository.Get(filter: m => m.ParentID == menu.ID);

foreach (var item in menus)

{

menu.MenuChildren.Add(item);

AddChildNode(item);

}

}

四、解析树形结构生成菜单html

第三步组装好树形结构后,我们再将菜单解析出来,添加相应的tag , 拼接出菜单的html

我们先定义一个类TagContainer,用来放tag

public class TagContainer

{

public int OrdinalNum;

public string Name;

public TagBuilder Tb;

public TagContainer ParentContainer;

public List<TagContainer> ChildrenContainers = new List<TagContainer>();

public TagContainer(ref int Num, TagContainer parent)

{

OrdinalNum = Num++;

ParentContainer = parent;

if (parent!=null)

{

parent.ChildrenContainers.Add(this);

}

}

}

说明:

其中OrdinalNum表示记录的序号(构建时,每个TagContainer都有个OrdinalNum作为标记,每产生一个li或ul都加1)

Tb是MVC原生的类,包含用于创建 HTML 元素的类和属性。

构建个类BaseHtmlTagEngine,专门用来处理转换标签的相关工作

其中_TopTagContainer 为放置根菜单的容器, 从 _TopTagContainer 这个节点开始,会将所有的子成员tag进行填充。

public abstract class BaseHtmlTagEngine<T> where T:IItem<T>

{

protected int _CntNumber = 0;

TagContainer _TopTagContainer;

string _OutString;

protected HtmlHelper _htmlHelper;

public BaseHtmlTagEngine(HtmlHelper htmlHelper)

{

_htmlHelper = htmlHelper;

}

public TagContainer TopTagContainer

{

get { return _TopTagContainer; }

}

//…其他相关方法,下面会有详解

}

说明:上面的 _OutString 就是我们最终解析出来的菜单html

具体转换步骤:

1. 将Model转换成带标签的树形结构

在BaseHtmlTagEngine添加方法BuildTreeStruct ,将model转化成带标签的结构

public void BuildTreeStruct(MenuViewModel<T> model)

{

_CntNumber = 0;

try

{

// 1.先设置放置根菜单的容器

_TopTagContainer = new TagContainer(ref _CntNumber, null);

foreach (T mi in model.MenuItems)

{

BuildTagContainer(mi, _TopTagContainer);

}

}

catch (Exception)

{

throw;

}

}

通过 BuildTagContainer 添加tag

为了代码结构更加清晰(另外也可以复用构建其他),我们再添加一个新的类HtmlBuilder继承BaseHtmlTagEngine, 具体的实现方法 BuildTagContainer 及相关的其他方法都放在这个类中

protected void BuildTagContainer(SysMenu item, TagContainer parent)

{

TagContainer tc = FillTag(item, parent);

foreach (SysMenu mmi in item.GetChildren())

{

BuildTagContainer(mmi, tc);

}

}

TagContainer FillTag(SysMenu item, TagContainer tc_parent)

{

//先把本身的菜单项加上(每一个项都以li开始)

TagContainer li_tc = new TagContainer(ref _CntNumber,tc_parent);

li_tc.Name = item.Name;

li_tc.Tb = AddItem(item); //li tag

if (HasChildren(item))

{

TagContainer ui_container = new TagContainer(ref _CntNumber, li_tc);

ui_container.Name = "**";

ui_container.Tb = Add_UL_Tag();

return ui_container;

}

return li_tc;

}

TagBuilder Add_UL_Tag()

{

TagBuilder ul_tag = new TagBuilder("ul");

ul_tag.AddCssClass("dropdown-menu");

return ul_tag;

}

AddItem 将具体的一个菜单项转化成具有标签的完整菜单项

(即li 及 li包含的子tag 及 相关的标签属性(如链接地址)、样式等)

最终返回的TagBuilder如果转化成字符串应该类似如下形式:

{<li class="dropdown"><a class="dropdown-toggle" data-toggle="dropdown" href="/XEngine/"><img class="xxx" src="xxx"></img>MenuTest<b class="caret"></b></a></li>}

AddItem 具体实现

TagBuilder AddItem(SysMenu mi)

{

var li_tag = new TagBuilder("li");

var a_tag = new TagBuilder("a");

var b_tag = new TagBuilder("b");

var image_tag = new TagBuilder("img");

if (mi.IconImage != null)

{

string path = "Images/" + mi.IconImage;

image_tag.MergeAttribute("src", path);

}

b_tag.AddCssClass("caret");

var contentUrl = GenerateContentUrlFromHttpContext(_htmlHelper);

string a_href = GenerateUrlForMenuItem(mi, contentUrl);

a_tag.Attributes.Add("href", a_href);

if (mi.MenuType == MenuTypeOption.Top)

{

li_tag.AddCssClass("dropdown");

a_tag.MergeAttribute("data-toggle", "dropdown");

a_tag.AddCssClass("dropdown-toggle");

}

else

{

li_tag.AddCssClass("dropdown-submenu");

}

a_tag.InnerHtml += image_tag.ToString();

a_tag.InnerHtml += mi.Name;

if (HasChildren(mi))

{

a_tag.InnerHtml += b_tag.ToString();

}

li_tag.InnerHtml = a_tag.ToString();

return li_tag;

}

2. 解析上面的树形结构并转化成html

首先看下最终生成菜单的结构(做了适当简化):

<li class="dropdown">

<a href="xx" data-toggle="dropdown" class="dropdown-toggle">MenuTest </a>

<ul class="dropdown-menu">

<li class="dropdown-submenu">

<a href="xx">Level 1a</a>

<ul class="dropdown-menu">

<li> <a href="xx">Level 2</a> </li>

</ul>

</li>

<li>

<a href="/XEngine/">Level 1b</a>

</li>

</ul>

</li>

对照效果图 :

解析算法:

一直递归这些步骤, 直到移到根节点。这个根节点包含所有的HTML

示例菜单开始的几个过程举例:

1. 获取叶节点 Level 2和 Level 1b, 取第一个叶节点 Level 2

2. 把Level 2的Html加入到上一级的InnerHtml中去,

_OutString设置为上一级的容器的Html, 即

<ul class="dropdown-menu">

<li> <a href="xx">Level 2</a> </li>

</ul>

此为一个完整过程。

向上提升一级:tc = tc.ParentContainer; 递归上面的过程

_OutString设置为上一级的容器的Html, 即

<li class="dropdown-submenu">

<a href="xx">Level 1a</a>

<ul class="dropdown-menu">

<li> <a href="xx">Level 2</a> </li>

</ul>

</li>

向上提升一级:tc = tc.ParentContainer; 递归上面的过程

_OutString设置为上一级的容器的Html, 即

<ul class="dropdown-menu">

<li class="dropdown-submenu">

<a href="xx">Level 1a</a>

<ul class="dropdown-menu">

<li> <a href="xx">Level 2</a> </li>

</ul>

</li>

</ul>

注意此时 Level 1a是有兄弟节点Level 1b的,递归过程中碰到有兄弟节点时我们就要将本身从完整的树形结构移除掉并停止递归:

tc.ParentContainer.ChildrenContainers.Remove(tc);

再重新扫描这棵树(从第一步开始再执行),依次将剩余的叶节点分支往上一直添加到_OutString中去。

这样一直将所有的叶节点分支都添加完后,当tc.ParentContainer为null即已经到了根节点时,处理过程结束,直接输出_OutString到前端就可以了。

具体代码:

public string Build()

{

try

{

while (true)

{

// 获取第一个叶节点

TagContainer tc = GetNoChildNode(_TopTagContainer);

bool PrcComplete = false;

Levelup(tc, ref PrcComplete);

if (PrcComplete)

{

break;

}

}

}

catch (Exception)

{

throw;

}

return _OutString;

}

递归执行移除分支扫描树

private void Levelup(TagContainer tc, ref bool ProcessingComplete)

{

while(tc!=null)

{

if (tc.ParentContainer!=null)

{

if (tc.ParentContainer.Tb!=null)

{

tc.ParentContainer.Tb.InnerHtml += tc.Tb.ToString();

_OutString = tc.ParentContainer.Tb.ToString();

}

else

{

ProcessingComplete = true;

break; //dummy or invalid container

}

if (tc.ParentContainer.ChildrenContainers.Count>1)

{

tc.ParentContainer.ChildrenContainers.Remove(tc);

break;

}

tc = tc.ParentContainer; // moving up the tree

}

else

{

ProcessingComplete = true;

break;

}

}

}

前端使用:

1. 加上命名空间

@using XEngine.Web.Utility.MenuHelper

2. 添加helper

@Html.Raw(Html.GetMenuHtml("MenuTest"))

注意原生的helper返回类型是MvcHtmlString 类型的,表示不应再次进行编码的 HTML 编码的字符串。

而我们返回的类型是string , 因此需要加上@Html.Raw()否则就不能正确显示。

总结

本篇主要讲了两个知识点 : 如何自定义html helper和 TagBuilder的使用。

自定义的html helper 第一个参数必须为 this HtmlHelper类型。

至于生成html tag,使用MVC原生的TagBuilder比较方便,注意方法的返回值要为MvcHtmlString ,如果返回值定义为String,返回的字符窜会被转义,为了防止转义我们可以用@Html.Raw来接收。当然你也可以不用TagBuilder纯手工拼接。

这个示例只要稍加扩展就可以很灵活的实现各种实际项目需求。

例如可以和权限结合起来,先过滤一遍权限,动态生成有权限的看到的菜单等。

欢迎大家多多评论,祝学习进步:)

P.S.

示例中前端直接在_Layout.cshtml中使用。

后端菜单相关的程序结构:

另外公司研发部招聘工程师2名(R语言方向 & .NET开发方向),主要研发数据可视化相关新产品,有兴趣的可以博客园短消息联系我。

base 在苏州高新区

完整目录:

MVC5+EF6 入门完整教程13 -- 动态生成多级菜单的更多相关文章

  1. MVC 5 + EF6 入门完整教程14 -- 动态生成面包屑导航

    上篇文章我们完成了 动态生成多级菜单 这个实用组件. 本篇文章我们要开发另一个实用组件:面包屑导航. 面包屑导航(BreadcrumbNavigation)这个概念来自童话故事"汉赛尔和格莱 ...

  2. MVC5 + EF6 入门完整教程二

    从前端的UI开始 MVC分离的比较好,开发顺序没有特别要求,先开发哪一部分都可以,这次我们主要讲解前端UI的部分. ASP.NET MVC抛弃了WebForm的一些特有的习惯,例如服务器端控件,Vie ...

  3. MVC5 + EF6 入门完整教程1

    https://www.cnblogs.com/miro/p/4030622.html 第0课 从0开始 ASP.NET MVC开发模式和传统的WebForm开发模式相比,增加了很多"约定& ...

  4. MVC5+EF6 入门完整教程九

    前一阵子临时有事,这篇文章发布间隔比较长,我们先回顾下之前的内容,每篇文章用一句话总结重点. 文章一 MVC核心概念简介,一个基本MVC项目结构 文章二 通过开发一个最基本的登录界面,介绍了如何从Co ...

  5. MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用

    摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识. 第二阶段11-20篇将会侧重于专题的讲解,一篇文章解决一个实际问题. 根据园友的反馈, 本篇文章将会先对呼声最高的仓储模式进行讲解. 文 ...

  6. MVC5+EF6 入门完整教程12--灵活控制Action权限

    大家久等了. 本篇专题主要讲述MVC中的权限方案. 权限控制是每个系统都必须解决的问题,也是园子里讨论最多的专题之一. 前面的系列文章中我们用到了 SysUser, SysRole, SysUserR ...

  7. MVC5 + EF6 入门完整教程(转载)--01

    MVC5 + EF6 入门完整教程   第0课 从0开始 ASP.NET MVC开发模式和传统的WebForm开发模式相比,增加了很多"约定". 直接讲这些 "约定&qu ...

  8. MVC5+EF6 入门完整教程

    MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用 MVC5+EF6 入门完整教程10:多对多关联表更新&使用原生SQL@20150521 MVC5+EF6 入门完整教程9:多表 ...

  9. MVC5+EF6 入门完整教程 总目录

    本系列文章会从一个主干开始,逐渐深入,初步规划30篇.初级10篇,中级10篇,综合项目实战10篇 初级10篇 MVC5+EF6 入门完整教程10:多对多关联表更新&使用原生SQL@201505 ...

随机推荐

  1. html5 postMessage解决跨域、跨窗口消息传递

    一些麻烦事儿 平时做web开发的时候关于消息传递,除了客户端与服务器传值还有几个经常会遇到的问题 1.页面和其打开的新窗口的数据传递 2.多窗口之间消息传递 3.页面与嵌套的iframe消息传递 4. ...

  2. Redis系列-冷知识

    下面是一些看了,但觉得用处不大,不记下又可惜的东西. Redis删除过期数据 redis通过expire/expireat(秒为单位)或者pexpire/pexpireat(毫秒为单位)来设置key的 ...

  3. java 锁!

    问题:如何实现死锁. 关键: 1 两个线程ta.tb 2 两个对象a.b 3 ta拥有a的锁,同时在这个锁定的过程中,需要b的锁:tb拥有b的锁,同时在这个锁定的过程中,需要a的锁: 关键的实现难点是 ...

  4. Javascript模块化编程笔记

    最近在读阮一峰的博客http://www.ruanyifeng.com/blog/2012/10/javascript_module.html,随手记录一些重要笔记.  Javascript模块的雏形 ...

  5. Bootstrap~多级导航(级联导航)的实现

    回到目录 在bootstrap官方来说,导航最多就是两级,两级以上是无法实现的,大叔找了一些第三方的资料,终于找到一个不错的插件,使用上和效果上都还不错,现在和大家分享一下 插件地址:http://v ...

  6. ajax图片上传及FastDFS入门案例.

    今天来开始写图片上传的功能, 现在的图片上传都讲求 上传完成后立刻回显且页面不刷新, 这里到底是怎么做的呢? 当然是借助于ajax了, 但是ajax又不能提交表单, 这里我们还要借助一个插件: jqu ...

  7. [Spring框架]Spring AOP基础入门总结二:Spring基于AspectJ的AOP的开发.

    前言: 在上一篇中: [Spring框架]Spring AOP基础入门总结一. 中 我们已经知道了一个Spring AOP程序是如何开发的, 在这里呢我们将基于AspectJ来进行AOP 的总结和学习 ...

  8. salesforce 零基础学习(三十七) DML及Database方法简单描述

    在apex中通过soql查询可以使用两种方式,使用DML语句或者使用Database的方法. 使用DML语句和使用Database类的方法对于我们来说用的都很多,并且都很常见.对于数据库常见的操作:增 ...

  9. JS BOM

    一.window对象 //系统对话框 var flag=confirm("提示语句");//弹出一个对话框 当你点击确定flag=true,点击取消flag=false: var ...

  10. KnockoutJS 3.X API 第四章(14) 绑定语法细节

    data-bind绑定语法 Knockout的声明性绑定系统提供了一种简洁而强大的方法来将数据链接到UI. 绑定到简单的数据属性或使用单个绑定通常是容易和明显的. 对于更复杂的绑定,它有助于更好地了解 ...