Search code examples
c#asp.net-mvc-4model-view-controllerasp.net-core-mvc

Nested child sub menus not getting generated in Asp.Net Core MVC project


i am trying to generate a 3 level nested menus for creating a navigation bar for my application. I am written below code. But the code only returns top menu and middle menu. Third menu named bottom is not showing. My project is in Asp.Net Core MVC.

SQL Table - MenuMasterDemo

SQL Table named MenuMasterDemo with below records

MenuMasterDemo.cs

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.ComponentModel.DataAnnotations.Schema;
    using Microsoft.EntityFrameworkCore;
    
    namespace SampleApp.Models;
    
    [Table("MenuMasterDemo")]
    public partial class MenuMasterDemo
    {
        [Key]
        [Column("MenuID")]
        public long MenuId { get; set; }
    
        [Unicode(false)]
        public string? Menu { get; set; }
    
        [Column("ParentMenuID")]
        public long? ParentMenuId { get; set; }
    
        // Property to hold child items
        [NotMapped]
        public List<MenuMasterDemo>? Children { get; set; }
    
    }

AppMenuViewComponent

using Microsoft.AspNetCore.Mvc;
using SampleApp.Data;

namespace SampleApp.ViewComponents
{
    public class AppMenuViewComponent : ViewComponent
    {
        private readonly SampleContext _context;

        public AppMenuViewComponent(SampleContext context)
        {
            _context = context;
        }
        public IViewComponentResult Invoke()
        {
            return View(_context.MenuMasterDemos.ToList());
        }

    }
}

AppMenuViewComponent - Default.cshtml

@model IEnumerable<MenuMasterDemo>

<!-- Side navigation menu -->
<div class="d-flex flex-column align-items-start align-items-md-center px-3 pt-2 text-white">
    <ul class="nav flex-column nav-pills">
        @foreach (var item in Model.Where(x => x.ParentMenuId == null)) // Filter top-level menus
        {
            <li class="nav-item">
                <a class="nav-link" data-bs-toggle="collapse" href="#submenu-@item.MenuId" role="button" aria-expanded="false" aria-controls="submenu-@item.MenuId">@item.Menu</a>
                <div class="collapse" id="submenu-@item.MenuId">
                    <ul class="nav flex-column ms-3">
                        <!-- Invoke RenderSubMenuItemsViewComponent for each top-level menu item -->
                        @await Component.InvokeAsync("RenderSubMenuItems", new { model = Model, parentMenuId = item.MenuId })
                    </ul>
                </div>
            </li>
        }
    </ul>
</div>

RenderSubMenuItemsViewComponent

using Microsoft.AspNetCore.Mvc;
using SampleApp.Data;
using SampleApp.Models;

namespace SampleApp.ViewComponents
{
    public class RenderSubMenuItemsViewComponent : ViewComponent
    {
        private readonly SampleContext _context;

        public RenderSubMenuItemsViewComponent(SampleContext context)
        {
            _context = context;
        }
        public IViewComponentResult Invoke(IEnumerable<MenuMasterDemo> model, long parentMenuId)
        {

            var submenuItems = model.Where(x => x.ParentMenuId == parentMenuId).ToList();

            foreach (var submenuItem in submenuItems)
            {
                submenuItem.Children = Invoke(model, submenuItem.MenuId) as List<MenuMasterDemo>;
            }


            return View(submenuItems);
        }
    }
}

RenderSubMenuItemsViewComponent - Default.cshtml

@model IEnumerable<MenuMasterDemo>

    <ul class="nav flex-column ms-3">
        @foreach (var subItem in Model)
        {
            <li class="nav-item">
                <a class="nav-link" href="#">@subItem.Menu</a>
                @if (subItem.Children != null && subItem.Children.Any())
                {
                    <!-- Recursively render children -->
                    @await Component.InvokeAsync("RenderSubMenuItems", new { model = subItem.Children, parentMenuId = subItem.MenuId })
                }
            </li>
        }
    </ul>

_Layout.cshtml

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - SampleApp</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
    <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
    <link rel="stylesheet" href="~/SampleApp.styles.css" asp-append-version="true" />

</head>
<body>
    <div class="container-fluid">
        <div class="row">
            <div>
                @await Component.InvokeAsync("AppMenu")
            </div>
            <div class="col py-3">
                @RenderBody()
            </div>
        </div>
    </div>

    <script src="~/lib/jquery/dist/jquery.min.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Solution

  • I used mock data in my side and the code below worked well.

    I reproduced your issue at first using your code submenuItem.Children = Invoke(model, submenuItem.MenuId) as List<MenuMasterDemo>;, after using submenuItem.Children = model.Where(x => x.ParentMenuId == submenuItem.MenuId).ToList();, the bottom menu could be seen. That's because the Invoke method is called by the component and should return View. But what we required there is only setting the Children property.

    using Microsoft.AspNetCore.Mvc;
    using System.ComponentModel.DataAnnotations.Schema;
    using System.ComponentModel.DataAnnotations;
    
    namespace WebAppMvc2.ViewComponents
    {
        public class AppMenuViewComponent : ViewComponent
        {
            public IViewComponentResult Invoke()
            {
                var list = new List<MenuMasterDemo> { 
                    new MenuMasterDemo{
                        MenuId = 1,
                        Menu = "top",
                        ParentMenuId = null
                    },
                    new MenuMasterDemo
                    {
                        MenuId = 2,
                        Menu = "middle1",
                        ParentMenuId = 1
                    },
                    new MenuMasterDemo
                    {
                        MenuId = 3,
                        Menu = "middle2",
                        ParentMenuId = 1
                    },
                    new MenuMasterDemo
                    {
                        MenuId = 4,
                        Menu = "bottom1",
                        ParentMenuId = 2
                    },
                    new MenuMasterDemo
                    {
                        MenuId = 5,
                        Menu = "bottom2",
                        ParentMenuId = 3
                    }
                };
                return View(list);
            }
    
        }
    
        public class RenderSubMenuItems : ViewComponent 
        {
            public IViewComponentResult Invoke(IEnumerable<MenuMasterDemo> model, long parentMenuId)
            {
                var submenuItems = model.Where(x => x.ParentMenuId == parentMenuId).ToList();
                foreach (var submenuItem in submenuItems)
                {
                    //submenuItem.Children = Invoke(model, submenuItem.MenuId) as List<MenuMasterDemo>;
                    submenuItem.Children = model.Where(x => x.ParentMenuId == submenuItem.MenuId).ToList();
                }
                return View(submenuItems);
            }
        }
    
        public class MenuMasterDemo
        {
            public long MenuId { get; set; }
    
            public string? Menu { get; set; }
    
            public long? ParentMenuId { get; set; }
    
            public List<MenuMasterDemo>? Children { get; set; }
    
        }
    }
    

    enter image description here