之前,小喵在實現樹狀結構的Menu時,通常是撰寫遞迴的方式,來展開樹狀結構。不過由於Blazor的Component特性,是可以將Component應用在頁面的某個部分,這讓小喵在上次遇到Blazor套用AdminLTE遇到無法展開收合問題時,開始思考是否可以透過開發一個MenuNode的Component,並在裡面在套用自己(MenuNode)的方式,去實現以前要用遞迴才能實現的樹狀結構。這一篇就來看這是怎麼做的。
緣起
之前,小喵在實現樹狀結構的Menu時,通常是撰寫遞迴的方式,來展開樹狀結構。不過由於Blazor的Component特性,是可以將Component應用在頁面的某個部分,這讓小喵在上次遇到Blazor套用AdminLTE遇到無法展開收合問題時,開始思考是否可以透過開發一個MenuNode的Component,並在裡面在套用自己(MenuNode)的方式,去實現以前要用遞迴才能實現的樹狀結構。這一篇就來看這是怎麼做的。
樹狀結構資料表Schema
小喵的無限層樹狀結構,他的資料是放在資料表中,他的相關 Table Schema 如下:
其實我的機制還不只是Menu這個,還有包含使用者的授權,為了描述的單純化,我在這一篇裡面不會貼出資料存取的部分,這個 Schema只是提供大家參考,知道這個無限層的Menu他在資料庫裡面的大概樣子。
他的起始節點,也就是根Node的NodeId=0,這個規劃上的,他子節點們的ParentId=0,然後在繼續往下長下去,大概是這樣的概念。
單一節點 MenuNode Component
小喵將單一節點寫成一個Component,需要透過參數傳入該Component的節點代號。從節點代號,透過資料庫取得以下幾個東西
- 該節點的物件(MenuVM)
- 是否有子節點(blHasSubMenu)
- 他的子節點集合(List<MenuPVM> oSubMenuPVMs)
另外,也會透過參數,傳入該節點是樹狀結構的第幾層(iLevel)。如果有子節點,則會計算他的下一層是第幾層(NextLevel),透過iLevel傳到子節點的 Component。
MenuNode的相關程式碼如下:
Html的部分:
@using PCAT_Blazor.Data
@using PCAT_Blazor.DAOs
@inject MenuDao MenuService
@inject NavigationManager iNavigationManager
@if (blHasSubMenu)
{
<!--有子資料夾 Start-->
<li class="nav-item has-treeview @menuopen @activeCSS">
@if (iNodeId != 0)
{
<a href="" class="nav-link" @onclick="chgMenuOpen">
<i class="nav-icon fa fa-@oMenu.iFontAwesome"></i>
<p>
@for (var i = 1; i < iLevel; i++)
{
@:
}
@oMenu.sText
<i class="right fas fa-angle-left"></i>
</p>
</a>
}
<ul class="nav nav-treeview">
@foreach (var tSubMenuP in oSubMenuPVMs)
{
<MenuNode iNodeId="@tSubMenuP.NodeId" iLevel="@NextLevel" ActiveNodes="@ActiveNodes"></MenuNode>
}
</ul>
</li>
<!--有子資料夾 End-->
}
else
{
<!--無子資料夾 Start-->
<li class="nav-item">
<a href="@oMenu.sRouterLink" class="nav-link @activeCSS">
<i class="fa fa-@oMenu.iFontAwesome nav-icon"></i>
<p>
@for (var i = 1; i < iLevel; i++)
{
@:
}
@oMenu.sText
</p>
</a>
</li>
<!--無子資料夾 End-->
}
C#程式碼的部分如下:
@code {
/// <summary>
/// 節點代號
/// </summary>
[Parameter]
public int iNodeId { get; set; } = 0;
/// <summary>
/// 第幾層
/// </summary>
[Parameter]
public int iLevel { get; set; } = 0;
[Parameter]
public string ActiveNodes { get; set; } = "";
[CascadingParameter] protected Task<AuthenticationState> AuthStat { get; set; }
/// <summary>
/// 下一層
/// </summary>
private int NextLevel = 0;
/// <summary>
/// 該節點物件
/// </summary>
private MenuVM oMenu = new MenuVM();
/// <summary>
/// 是否有子節點
/// </summary>
private bool blHasSubMenu = false;
/// <summary>
/// 子節點們
/// </summary>
private List<MenuPVM> oSubMenuPVMs = new List<MenuPVM>();
/// <summary>
/// 是否展開子節點
/// 不展開:空字串
/// 展開:menu-open
/// </summary>
private string menuopen = "";
private string activeCSS = "";
private string UsrId = "";
/// <summary>
/// Component初始化
/// </summary>
protected async override void OnInitialized()
{
var user = (await AuthStat).User;
if (user.Identity.IsAuthenticated)
{
UsrId = user.Identity.Name;
oMenu = await MenuService.getMenuAsync(iNodeId);
blHasSubMenu = await MenuService.chkHasSubMenuPByNodeIdUsrIdAsync(iNodeId, UsrId);
if (blHasSubMenu)
{
oSubMenuPVMs = await MenuService.getSubMenuPsByParentIdUserIdAsync(iNodeId,UsrId);
}
if (iNodeId == 0)
{
menuopen = "menu-open";
}
if (ActiveNodes != "")
{
string[] ANs = ActiveNodes.Split(",");
foreach (string tAN in ANs)
{
if (tAN == iNodeId.ToString())
{
activeCSS = "active";
menuopen = "menu-open";
}
}
}
NextLevel = iLevel + 1;
StateHasChanged();
base.OnInitialized();
}
}
/// <summary>
/// 展開或收合
/// </summary>
private void chgMenuOpen()
{
if (menuopen == "")
{
menuopen = "menu-open";
}
else
{
menuopen = "";
}
}
}
NavMenu.razor
上述的節點Component,我們要應用在 NavMenu.razor 這個 Component中,並且給他初始的根結點相關參數,另外,小喵會依據當下的網址,去對照出當下瀏覽的網址是否是屬於哪個節點,要設定這些節點的class顯示active
小喵也會讓它已登入的情況下,才顯示該節點。所以程式碼中會有取得當下登入帳號的部分。
相關程式碼如下:
Html的部分:
@using PCAT_Blazor.DAOs
@inject NavigationManager NavigationManager
@inject MenuDao MenuService
<AuthorizeView>
<Authorized>
<nav class="mt-2">
<ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">
<MenuNode iNodeId="0" iLevel="0" ActiveNodes="@ActiveNodes"></MenuNode>
</ul>
</nav>
</Authorized>
<NotAuthorized>
</NotAuthorized>
</AuthorizeView>
C#程式碼的部分:
@code {
private string ActiveNodes = "";
private string sLink = "";
private string UsrId = "";
[CascadingParameter] protected Task<AuthenticationState> AuthStat { get; set; }
protected override async Task OnInitializedAsync()
{
var user = (await AuthStat).User;
if (user.Identity.IsAuthenticated)
{
sLink = "/" + NavigationManager.Uri.Replace(NavigationManager.BaseUri, "");
UsrId = user.Identity.Name;
if (sLink != "/")
{
ActiveNodes = await MenuService.getActiveMenuBysLinkAsync(sLink);
}
StateHasChanged();
}
await base.OnInitializedAsync();
}
}
執行結果示範
相關的執行結果示範,如下影片
末記
原本還在傷腦筋Blazor套用AdminLTE的Theme,在render-mode改成Server後,Menu的部分無法展開收合,在思考解決這問題過程中,突然想到或許可以用 Component 中使用自己 Component的方式來呈現,這樣的做法,說實在的比起寫遞迴方式產生Menu的方式,其實更為靈活,程式碼也可以很簡單且易懂。小喵以此篇記錄筆記,順便提供給有需要的人參考。
以下是簽名:
- 歡迎轉貼本站的文章,不過請在貼文主旨上加上【轉貼】,並在文章中附上本篇的超連結與站名【topcat姍舞之間的極度凝聚】,感恩大家的配合。
- 小喵大部分的文章會以小喵熟悉的語言VB.NET撰寫,如果您需要C#的Code,也許您可以試著用線上的工具進行轉換,這裡提供幾個參考
Microsoft MVP Visual Studio and Development Technologies (2005~2019/6) | topcat Blog:http://www.dotblogs.com.tw/topcat |