MVC5+EF6 入門完整教程13 -- 動態生成多級菜單

来源:http://www.cnblogs.com/miro/archive/2016/05/30/5541086.html
-Advertisement-
Play Games

稍微有一定複雜性的系統,多級菜單都是一個必備組件。 本篇專題講述如何生成動態多級菜單的通用做法。 我們不用任何第三方的組件,完全自己構建靈活通用的多級菜單。 需要達成的效果:容易復用,可以根據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標簽

我們使用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獲取開始的根菜單

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中使用。

後端菜單相關的程式結構:

另外公司研發部招聘工程師2名(R語言方向 & .NET開發方向),主要研發數據可視化相關新產品,有興趣的可以博客園短消息聯繫我。

base 在蘇州高新區

完整目錄:


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一般,我們看到術語“索引”和“鍵”交換使用,但實際上這兩個是不同的。索引是存儲在資料庫中的一個物理結構,鍵純粹是一個邏輯概念。鍵代表創建來實施業務規則的完整性約束。索引和鍵的混淆通常是由於資料庫使用索引來實施完整性約束。 接下來我們看看資料庫中的主鍵約束、唯一鍵約束和唯一索引的區別。 SQL> se ...
  • MongoDB是一個開源的文檔資料庫,支持高性能、高可用性、自動縮放。 在MongoDB中,一條記錄就是一個文檔,是由欄位和值對構成一個數據結構,類似於JSON對象。欄位的值可以包括其他文檔、數組和文檔的數組。 數據結構如下所示: ...
  • 腳本最好都放在/usr/local/sbin中 腳本的執行 sh -x 腳本.sh -x可以查看執行過程 1在腳本中使用變數 使用變數的時候,需要使用$符號: #!/bin/bash ##把命令賦值為變數,需要使用反引號 d=`date +"%H:%M:%S"` echo "The script b ...
  • grep是UNIX和LINUX中使用最廣泛的命令之一。grep允許對文本文件進行模式查找。如果找到匹配模式, grep列印包含模式的所有行。grep支持基本正則表達式,也支持其擴展集。grep有三種變形,即: grep:標準grep命令,這裡主要討論此格式; Egrep:等同於grep -E,擴展g ...
  • 算術運算符 + - * / % 表示加減乘除和取餘運算+= -= *= /= 同 C 語言中的含義 位操作符 > >>= 表示位左右移一位操作& &= | |= 表示按位與、位或操作~ ! 表示非操作^ ^= 表示異或操作 關係運算符 = == != 表示大於、小於、大於等於、小於等於、等於、不等於 ...
  • OSI 七層模型通過七個層次化的結構模型使不同的系統不同的網路之間實現可靠的通訊,因此其最主要的功能就是幫助不同類型的主機實現數據傳輸 。 OSI 七層模型通過七個層次化的結構模型使不同的系統不同的網路之間實現可靠的通訊,因此其最主要的功能就是幫助不同類型的主機實現數據傳輸 。 完成中繼功能的節點通 ...
  • 上一篇我們已經可以獲取各種FileHandler的實例和對應的元數據。本篇,我們做一個稍微完整的文件管理器。 1、修改介面IFileHandler,傳入文件名 2、修改具體的FileHandler。 3、修改主函數 運行結果: 可以看到,對每一個具體的文件,均找到了正確的處理實例進行處理。avi文件 ...
  • 首先我們都知道引用類型預設值都是null,而值類型的預設值都有非null。為什麼引用類型可以為空?因為引用類型變數都是保存一個對象的地址引用(就像一個url對應一個頁面),而引用類型值為null的時候是變數值指向了一個空引用(如同一個空的url)那為什麼值不能有空值呢?其實很簡單,因為如int值範圍... ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...