稍微有一定复杂性的系统,多级菜单都是一个必备组件。
本篇专题讲述如何生成动态多级菜单的通用做法。
我们不用任何第三方的组件,完全自己构建灵活通用的多级菜单。
需要达成的效果:容易复用,可以根据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标签
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;
}
}
为了代码结构更加清晰(另外也可以复用构建其他),我们再添加一个新的类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中使用。
后端菜单相关的程序结构: