標籤:generated 基礎 種類 href return 屬性 編碼 getchild cti
稍微有一定複雜性的系統,多級菜單都是一個必備組件。
本篇專題講述如何產生動態多級菜單的通用做法。
我們不用任何第三方的組件,完全自己構建靈活通用的多級菜單。
需要達成的效果:容易複用,可以根據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中使用。
後端菜單相關的程式結構:
MVC5+EF6 入門完整教程13 -- 動態產生多級菜單