ASP.NET MVC 4 RAZOR Dynamic multi-level menu from the database

  • Tutorial
As promised in the previous DropDownList post , Set “value” for the default option in MVC 4 , today I will talk about building a dynamic multi-level menu with infinite nesting, stored in the MsSQL database. I remember at one time on PCP this was also a task for a couple of days. But for the MVC 4 with the RAZOR engine, I barely figured it out, although in the end, as always, there was nothing complicated or supernatural. The article is designed for those who do not know how. If you know how to do better - share it. Let's get started.

This manual assumes that you are already operating on the knowledge gained from reading these articles: Entity Framework in an ASP.NET MVC application . Or these: ASP.NET MVC 4 Tutorials

1) First you need to understand the structure of the database. This is the main thing. The theory can be found in the article Hierarchical data structures in relational databases . We will use the simplest structure possible, called the "structure with reference to the ancestor."

The SQL code looks something like this:
CREATE TABLE "CATALOG" (
  "ID" INTEGER NOT NULL PRIMARY KEY,
  "NAME" VARCHAR(200) CHARACTER SET WIN1251 NOT NULL,
  "PARENT_ID" INTEGER
);


Create a model in VS 2012:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace zf2.Models
{
    public class NewsM
    {
        public int NewsMID { get; set; }
        public int ParentID { get; set; }
        public string Title { get; set; }
        public string AddTitle { get; set; }
        public string Description { get; set; }
        public string Content { get; set; }
        public DateTime ModDate { get; set; }
    }
}

In general, only the first three fields are basic. But for clarity, I brought a working version used on my site.

2) The controller.
        public ActionResult NewsA(int id = 1) //id статьи для полного отображения
        {
            ViewBag.Menu = db.NewsMs.ToList(); //получаем модель, с которой будем строить меню.
            ViewBag.Id = id;
            return View();
        }


3) Partial Views (Partial view scripts)
If you have not encountered them yet - no big deal. They differ from ordinary scripts only in that they are not called automatically. This is a view for views so to speak.

Go to the folder Views-Shared-Right-click-Add-View: check the box "Create as a partial view." Enter the name "_Menu". Why is underscore used? Yes, just for convenience and eliminating name matches. Since partial scripts are searched in all directories of the Shared type and the corresponding controller with various extensions. Here is what gives out if you set an invalid script name:
Не удалось найти частичное представление "_gMenu" или ни один обработчик представлений не поддерживает места поиска. Выполнялся поиск в следующих местах:
~/Views/Home/_gMenu.aspx
~/Views/Home/_gMenu.ascx
~/Views/Shared/_gMenu.aspx
~/Views/Shared/_gMenu.ascx
~/Views/Home/_gMenu.cshtml
~/Views/Home/_gMenu.vbhtml
~/Views/Shared/_gMenu.cshtml
~/Views/Shared/_gMenu.vbhtml

I think it’s clear.
Move on.
In the "_Menu.cshtml" copy the following code:
@{
    List menuList = ViewBag.Menu;
}

@helper RenderMenuItem(List menuList, zf2.Models.NewsM mi)
{
    foreach (var cp in menuList.Where(p => p.ParentID == mi.NewsMID))
    {
        @:
  • @Html.ActionLink(cp.Title, ViewContext.RouteData.GetRequiredString("action"), new { id=cp.NewsMID }) if(menuList.Count(p=>p.ParentID == cp.NewsMID) > 0) { @:
      } @RenderMenuItem(menuList,cp) if(menuList.Count(p=>p.ParentID == cp.NewsMID) > 0) { @:
    } else { @:
  • } } }

    This is where all the magic lies.

    @foreach (var mp in menuList.Where(p => p.ParentID == 0))- parses and displays the names with ParentID = 0.

    @RenderMenuItem(menuList,mp)- we call the view helper, which already recursively completes all nesting for each “root” item. - this is the view helper itself, inside of which recursion is organized. - create links. I use standard routing. The controller name is substituted automatically. Action name and parameter Id - specify "manually". That is, we get the name of the action. Similarly, you can get the name of the controller. - set the parameter Id. - Link name Next, create another partial view script called "_Content". We will display the contents of the selected article on the transmitted Id. The code is:

    @helper RenderMenuItem(List menuList, zf2.Models.NewsM mi)

    @Html.ActionLink(mp.Title, ViewContext.RouteData.GetRequiredString("action"), new { id=mp.NewsMID })
    ViewContext.RouteData.GetRequiredString("action")
    new { id=mp.NewsMID }
    mp.Title




    @{
        List menuList = ViewBag.Menu;
    }
    @ViewBag.Id
    @foreach (var mp in menuList.Where(p => p.NewsMID == ViewBag.Id))
    {
        @mp.Content
        @mp.AddTitle
        @mp.Description
    }
    


    4) The main view script. I have it called like the action name in the controller - NewsA.cshtml
    In it, we just call our partial view scripts and display the header.
    @{
        ViewBag.Title = "NewsA";
    }
    @{
        List menuList = ViewBag.Menu;
    }
    
    @Html.Partial("_Menu")
    @Html.Partial("_Content")


    ,  - это использование Bootstrap - грубо говоря CSS фреймворка. Более подробно можно ознакомиться тут: 

    Все. Запускаем. И видим похожую картинку после заполнения:
    image

    ПС:
    Нужно еще создать контроллер для работы с моделью.
    Подключение к БД
    Класс работы с Entity Framework
    И начальное заполнение таблицы.
    Как сделать первые три пункта описано в мануалах, ссылка на которые в начале статьи.
    Код для начального заполнения:
    using System; using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Web; using zf2.Models; namespace zf2.DAL { public class ZfInitializer : DropCreateDatabaseIfModelChanges { protected override void Seed(ZfContext context) { var newsMs = new List { new NewsM { NewsMID = 1, ParentID = 0, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 2, ParentID = 0, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 3, ParentID = 1, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 4, ParentID = 1, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 5, ParentID = 2, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 6, ParentID = 3, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, new NewsM { NewsMID = 7, ParentID = 2, Title = "Carson", AddTitle = "Carson", Description = "Carson", Content = "Carson" , ModDate = DateTime.Parse("2005-09-01") }, }; newsMs.ForEach(s => context.NewsMs.Add(s)); context.SaveChanges(); } } }


    Следующие статьи уже создаются...

    Also popular now: