[Blazor][筆記]透過 component 來實現無限層的左方Menu列表

之前,小喵在實現樹狀結構的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++)
                    {
                        @:&nbsp;
                    }
                    @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++)
                {
                    @:&nbsp;
                }
                @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的方式,其實更為靈活,程式碼也可以很簡單且易懂。小喵以此篇記錄筆記,順便提供給有需要的人參考。

 


以下是簽名:


Microsoft MVP
Visual Studio and Development Technologies
(2005~2019/6) 
topcat
Blog:http://www.dotblogs.com.tw/topcat