locked
Problem with writing LINQ for component view (Dropdown menu) RRS feed

  • Question

  • User1255309776 posted

    Hi guys,

    My website will have 3 levels of dropdown menu on navbar. Categories -> Subcategories -> NestedCategories

    All these data must be fetched from database using good LINQ on InvokeAsync() action. In default view I use view model named MenusModel.

    @model IEnumerable<Samirad.Models.ViewModels.MenusModel>
    @using Samirad.Models.ViewModels
    @using Samirad
    
    <ul class="dropdown-menu" id="catmenu">
        @foreach (MenusModel item in Model)
        {
            <li>
    
                <div class="dropdown-item family">
                    <a href="">@item.Category.Name</a>
    
                    <ul class="submenu">
                        @if (item.Subcategories != null && item.Subcategories.Count() > 0)
                        {
                            foreach (Subcategory component in item.Category.Subcategories)
                            {
                                <li>
                                    <a class="dropdown-item">@component.Name</a>
                                    <ul class="secondsub">
                                        @if (item.NestedCategories != null && item.NestedCategories.Count() > 0)
                                        {
                                            foreach (NestedCategory element in component.NestedCategories)
                                            {
                                                <li><a class="dropdown-item">@element.Name</a></li>
                                            }
                                        }
    
                                    </ul>
                                </li>
                            }
    
                        }
                    </ul>
                       
    
                </div>
    
    
            </li>
        }
       
    </ul>
    

    And this is my InvokeAsync() action where I cannot define NestedCategories  in the right way.

    namespace Samirad.ViewComponents
    {
        public class MenuViewComponent: ViewComponent
        {
            private readonly SamirDbContext _samirDbContext;
    
            public MenuViewComponent(SamirDbContext samirDbContext)
            {
                _samirDbContext = samirDbContext;
            }
    
            public async Task<IViewComponentResult> InvokeAsync()
            {
                var menu = await _samirDbContext.Categories.Include(x => x.Subcategories).ThenInclude(y => y.NestedCategories).
                                                          Select(x => new MenusModel()
                                                          {
                                                              Category = x,
                                                              Id = x.Id,
                                                              Subcategories = x.Subcategories.Select(y => new Subcategory()
                                                              {
                                                                  Id = y.Id,
                                                                  Name = y.Name
                                                              }),
                                                              NestedCategories = ......
    
                                                          }).ToListAsync();
    return View(menu); } } }

    I cannot reach subcategories in order to get nestedcategories from there to define them to Nestedcategories of the viemodel (MenusModel).

    Please, help me with this. 

    Thanks in advance

    Friday, August 28, 2020 6:51 PM

Answers

  • User585649674 posted

    A cancellation token comes into play, when the task has been cancelled. Please check database timeout for a query and request timeout in IIS. Please give the stacktrace of exception.

    I have slightly corrected the linq statement. Getting the data altogether from database will result in a subquery for number of categories and another set of subqueries for each of the sub category. Better way is the cascading dropdown.

    Select(x => new MenusModel()
                                                              {
                                                                  Category = x,
                                                                  Id = x.Id,
                                                                  Subcategories = x.Subcategories.Select(w=> new Subcategory {
    Id = w.Id,
    name= w.Name,
    NestedCategories = w.NestedCategories.Select(z => new NestedCategory() { Id = z.Id, Name = z.Name }
    }), Subcategory = x.Subcategories.FirstOrDefault(), }).ToListAsync();



    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, August 31, 2020 10:08 AM

All replies

  • User475983607 posted

    IMHO, view components are not a good choice for a cascading dropdowns design.  Use one of the many JavaScript libraries.  Do a Google search for "cascading dropdown".

    https://www.learnrazorpages.com/razor-pages/ajax/cascading-dropdowns

    Friday, August 28, 2020 7:42 PM
  • User1255309776 posted

    No, this is not that cascading dropdown discussed on the link, I use actually ajax/javascript while building, say for ex. create Post view where category-subcategories are chosen from dropdown menu. But this is just to see on navbar when hovering, so I need to get them from database only just to read and click to get to needed page for user. I could do in two levels in my previous project, but cannot add the next sub-level (nestedcategory) with LINQ, so I'm trying to overcome this issue as it must be possible to solve.

    Friday, August 28, 2020 8:41 PM
  • User1255309776 posted

    Just look at the image from this link, you'll get what I mean.

    https://prnt.sc/u7qjly

    I print screened on hover, and you can see submenus in 3 levels.

    Friday, August 28, 2020 9:37 PM
  • User303363814 posted

    You haven't shown your table design but it looks like you have separate tables for categories, subcategories and nestedcategories.  You would be better to have a single table called, say, MenuOptions.  Each MenOption has an Id, a Name and a ParentId.  The ParentId tells you which MenuOption this one is part of.

    I like to have a root item (which does not get shown on the user interface) whose ParentId is itself (normally 1, for convenience)  Then your top level menu items are all those whose ParentId is equal to the id of the root item.  Your table will have values something like

    Id Name ParentId
    1 root 1
    2 Home 1
    3 Contact 1
    4 Head Office 3
    5 Branches 3
    6 Help 1
    7 Chapter 1 Help 6
    8 Chapter 2 Help 6
    9 Chapter 1 - Sub Help 7
    10 Chapter 1 -More Help 7
    11 Chapter 2 - More information 8
    12 Chapter 1 - Event more help 7
    13 Chapter 3 Help 6
    14 About Us 1

    Your linq statement just retrieves the entirety of this table.

    (I don't do Razor - or whatever your UI language is - so this is pure guesswork)

    To display the menu your outer loop would be something like

    @foreach (MenusModel item in Model.where(mi => mi.ParentId == rootItemId)) // I assume you can filter here???

    (rootItemId would be 1 for the example I have shown)

    The subcategory level would be

    @foreach (MenusModel subItem in (Model.where(mi => mi.ParentId == item.Id))

    The nested category level would be

    @foreach (MenusModel nestedItem in (Model.where(mi => mi.ParentId == subItem.Id))

    (and you could keep going down, down, down)

    (BTW - the basic issue is that linq and relational database are built around the concept of, well 'relational' data.  What you have is something different, 'hierarchical' data.  It makes life a little ugly)

    PS - Instead of having a 'dummy' root item you can have a rule that any MenuItem with a ParentId of 0 is considered a top-level menu item.  Or you can have the convention that anything with Id == ParentId is a top level item

    PPS - I normally like to have an Order field as well so that I can control the order that elements appear in the menu.  It is just a matter of appending an .OrderBy clause to the .Where clause for each level

    Friday, August 28, 2020 10:32 PM
  • User1255309776 posted

    I could write it somehow with this way, but got new error which couldn't understand.

      public async Task<IViewComponentResult> InvokeAsync()
            {
                    var menu = await _samirDbContext.Categories.Include(x => x.Subcategories).ThenInclude(y => y.NestedCategories).
                                                              Select(x => new MenusModel()
                                                              {
                                                                  Category = x,
                                                                  Id = x.Id,
                                                                  Subcategories = x.Subcategories,
                                                                  Subcategory = x.Subcategories.FirstOrDefault(),
                                                                  NestedCategories = x.Subcategories.FirstOrDefault().NestedCategories.Select(z => new NestedCategory()
                                                                  {
                                                                      Id = z.Id,
                                                                      Name = z.Name
                                                                  })
    
                                                              }).ToListAsync();
                    
                    return View(menu);
                                                                                           
            }

    The Error is: nvalidOperationException: variable 'ct' of type 'System.Threading.CancellationToken' referenced from scope '', but it is not defined

    I don't have any 'ct' variable, how this exception occured? Have you ever faced such exception?

    Saturday, August 29, 2020 1:26 PM
  • User585649674 posted

    A cancellation token comes into play, when the task has been cancelled. Please check database timeout for a query and request timeout in IIS. Please give the stacktrace of exception.

    I have slightly corrected the linq statement. Getting the data altogether from database will result in a subquery for number of categories and another set of subqueries for each of the sub category. Better way is the cascading dropdown.

    Select(x => new MenusModel()
                                                              {
                                                                  Category = x,
                                                                  Id = x.Id,
                                                                  Subcategories = x.Subcategories.Select(w=> new Subcategory {
    Id = w.Id,
    name= w.Name,
    NestedCategories = w.NestedCategories.Select(z => new NestedCategory() { Id = z.Id, Name = z.Name }
    }), Subcategory = x.Subcategories.FirstOrDefault(), }).ToListAsync();



    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Monday, August 31, 2020 10:08 AM