稍微有一定複雜性的系統,多級菜單都是一個必備組件。 本篇專題講述如何生成動態多級菜單的通用做法。 我們不用任何第三方的組件,完全自己構建靈活通用的多級菜單。 需要達成的效果:容易復用,可以根據model動態產生。 文章提綱 概述要點 && 理論基礎 詳細步驟 一、分析多級目錄的html結構 二、根據 ...
稍微有一定複雜性的系統,多級菜單都是一個必備組件。
本篇專題講述如何生成動態多級菜單的通用做法。
我們不用任何第三方的組件,完全自己構建靈活通用的多級菜單。
需要達成的效果:容易復用,可以根據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獲取開始的根菜單
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中使用。
後端菜單相關的程式結構:
另外公司研發部招聘工程師2名(R語言方向 & .NET開發方向),主要研發數據可視化相關新產品,有興趣的可以博客園短消息聯繫我。
base 在蘇州高新區
完整目錄:
- MVC5+EF6 入門完整教程13--動態生成多級菜單 @20160530
- MVC5+EF6 入門完整教程12--靈活控制Action許可權 @20160504
- MVC5+EF6 入門完整教程11--細說MVC中倉儲模式的應用 @20150914
- MVC5+EF6 入門完整教程10:多對多關聯表更新&使用原生SQL@20150521
- MVC5+EF6 入門完整教程9:多表數據載入@20150212
- MVC5+EF6 入門完整教程8 :不丟失數據進行資料庫結構升級 @20141215
- MVC5+EF6 入門完整教程7 :排序過濾分頁 @20141201
- MVC5+EF6 入門完整教程6 :分部視圖(Partial View) @20141117
- MVC5+EF6 入門完整教程5 :UI的一些改造 @20141113
- MVC5+EF6 入門完整教程4 :EF基本的CRUD @20141104
- MVC5+EF6 入門完整教程3 :EF完整開發流程 @20141027
- MVC5+EF6 入門完整教程2 :從前端UI開始 @20141021
- MVC5+EF6 入門完整教程1 :從0開始