原文: 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來使用。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuADO1YWY1QjN3kTM2EjY5UGN0QGMmNTYwQmNmNWZ4ADMfdWbp9CXt92Yu4GZjlGbh5SZslmZxl3Lc9CX6MHc0RHaiojIsJye.png)
二、前端引入類的命名空間:
@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結構,我們建構類似如下的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>
對照效果圖 :
解析算法:
一直遞歸這些步驟, 直到移到根節點。這個根節點包含所有的HTML
示例菜單開始的幾個過程舉例:
1. 擷取葉節點 Level 2和 Level 1b, 取第一個葉節點 Level 2
2. 把Level 2的Html加入到上一級的InnerHtml中去,
_OutString設定為上一級的容器的Html, 即
此為一個完整過程。
向上提升一級:tc = tc.ParentContainer; 遞歸上面的過程
_OutString設定為上一級的容器的Html, 即
注意此時 Level 1a是有兄弟節點Level 1b的,遞歸過程中碰到有兄弟節點時我們就要将本身從完整的樹形結構移除掉并停止遞歸:
tc.ParentContainer.ChildrenContainers.Remove(tc);
再重新掃描這棵樹(從第一步開始再執行),依次将剩餘的葉節點分支往上一直添加到_OutString中去。
這樣一直将所有的葉節點分支都添加完後,當tc.ParentContainer為null即已經到了根節點時,處理過程結束,直接輸出_OutString到前端就可以了。
具體代碼:
public string Build()
while (true)
// 擷取第一個葉節點
TagContainer tc = GetNoChildNode(_TopTagContainer);
bool PrcComplete = false;
Levelup(tc, ref PrcComplete);
if (PrcComplete)
{
break;
}
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);
tc = tc.ParentContainer; // moving up the tree
else
ProcessingComplete = true;
break;
前端使用:
1. 加上命名空間
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開始