MVC5+EF6 入门完整教程13 -- 动态生成多级菜单

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 原文:MVC5+EF6 入门完整教程13 -- 动态生成多级菜单稍微有一定复杂性的系统,多级菜单都是一个必备组件。 本篇专题讲述如何生成动态多级菜单的通用做法。 我们不用任何第三方的组件,完全自己构建灵活通用的多级菜单。
原文: MVC5+EF6 入门完整教程13 -- 动态生成多级菜单

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

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

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

需要达成的效果:容易复用,可以根据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获取开始的根菜单

 SysMenu itemRoot = 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 在苏州高新区

完整目录:

目录
相关文章
|
2月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
41 7
|
前端开发 安全 Dubbo
Spring MVC & Boot & Cloud 技术教程汇总(长期更新)
Java成神之路技术整理(长期更新) 以下是Java技术栈微信公众号发布的关于 Spring/ Spring MVC/ Spring Boot/ Spring Cloud 的技术干货,本文长期更新。
338 0
|
前端开发 数据库 存储
MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用
原文:MVC5+EF6 入门完整教程11--细说MVC中仓储模式的应用   摘要: 第一阶段1~10篇已经覆盖了MVC开发必要的基本知识。 第二阶段11~20篇将会侧重于专题的讲解,一篇文章解决一个实际问题。
1316 0
|
XML 前端开发 定位技术
MVC 5 + EF6 入门完整教程14 -- 动态生成面包屑导航
原文:MVC 5 + EF6 入门完整教程14 -- 动态生成面包屑导航 上篇文章我们完成了 动态生成多级菜单 这个实用组件。 本篇文章我们要开发另一个实用组件:面包屑导航。 面包屑导航(BreadcrumbNavigation)这个概念来自童话故事"汉赛尔和格莱特",当汉赛尔和格莱特穿过森林时,不小心迷路了,但是他们发现在沿途走过的地方都撒下了面包屑,让这些面包屑来帮助他们找到回家的路。
1242 0
|
Web App开发 前端开发
MVC5+EF6 入门完整教程12--灵活控制Action权限
原文:MVC5+EF6 入门完整教程12--灵活控制Action权限 大家久等了。 本篇专题主要讲述MVC中的权限方案。 权限控制是每个系统都必须解决的问题,也是园子里讨论最多的专题之一。 前面的系列文章中我们用到了 SysUser, SysRole, SysUserRole 这几个示例表。
1227 0
|
SQL 安全 测试技术
MVC5+EF6 完整教程17--升级到EFCore2.0
原文:MVC5+EF6 完整教程17--升级到EFCore2.0 EF Core 2.0上周已经发布了,我们也升级到core 文章内容基于vs2017,请大家先安装好vs2017(15.3). 本篇文章主要讲下差异点,跟之前一样的就不再重复了。
1339 0
|
前端开发 .NET 开发框架
MVC 5 + EF6 完整教程16 -- 控制器详解
原文:MVC 5 + EF6 完整教程16 -- 控制器详解 Controller作为持久层和展现层的桥梁, 封装了应用程序的逻辑,是MVC中的核心组件之一。 本篇文章我们就来谈谈 Controller, 主要讨论两个方面: Controller运行机制简介 Controller数据传递方式...
1109 0
|
3月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
47 0
|
6月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
191 0
|
6月前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
79 0