稍微有一定複雜性的系統,多級選單都是一個必備元件。
本篇專題講述如何生成動態多級選單的通用做法。
我們不用任何第三方的元件,完全自己構建靈活通用的多級選單。
需要達成的效果:容易複用,可以根據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中使用。
後端選單相關的程式結構:
另外公司研發部招聘工程師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開始