Pull to refresh

ASP.NET MVC 4 RAZOR Динамическое многоуровневое меню из БД

Reading time 5 min
Views 23K
Как и обещал в предыдущем посте DropDownList, Задать «value» для default option в MVC 4, сегодня расскажу про построение динамического многоуровневого меню с бесконечной вложенностью, хранящееся в БД MsSQL. Помню в свое время на ПХП это тоже было задачкой на пару дней. Но для MVC 4 с движком RAZOR — еле разобрался, хотя в итоге как всегда ничего сложного или сверхъестественного. Статья рассчитана на тех, кто делать это не умеет. Если Вы знаете как сделать лучше — поделитесь. Приступим.

Сей мануал предполагает, что Вы уже оперируете знаниями, полученными при ознакомлении с этими статьями: Entity Framework в приложении ASP.NET MVC. Или этими: ASP.NET MVC 4 Tutorials

1) Сначала нужно разобраться со структурой БД. Это главное. С теорией можно ознакомиться в статье Иерархические структуры данных в реляционных БД. Мы будем использовать максимально простую структуру, называемой «структура со ссылкой на предка».

SQL код выглядит приблизительно так так:
CREATE TABLE "CATALOG" (
  "ID" INTEGER NOT NULL PRIMARY KEY,
  "NAME" VARCHAR(200) CHARACTER SET WIN1251 NOT NULL,
  "PARENT_ID" INTEGER
);


Создаем модель в 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; }
    }
}

В общем основными являются только первые три поля. Но для наглядности я привел работающий вариант, используемый у меня на сайте.

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

            return View();
        }


3) Partial Views (Частичные скрипты вида)
Если Вы с ними еще не сталкивались — ничего страшного. От обычных скриптов они отличаются лишь тем, что не вызываются автоматически. Это вьюшки для вьюшек так сказать.

Заходим в папочку Views-Shared-Правая кнопка мыши-Добавить-Представление: ставим галочку «Создать как частичное представление». Вводим имя "_Menu". Почему используется нижнее подчеркивание? Да просто для удобства и исключения совпадений имен. Так как частичные скрипты ищутся во всех каталогах вида Shared и соответствующего контроллера с различными расширениями. Вот что выдает если задать не правильное имя скрипта:
Не удалось найти частичное представление "_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

Думаю понятно.
Идем дальше.
В "_Menu.cshtml" копируем следующий код:
@{
    List<zf2.Models.NewsM> menuList = ViewBag.Menu;
}

<ul class="menu">  
    
@foreach (var mp in menuList.Where(p => p.ParentID == 0)){
    
<li>
        @Html.ActionLink(mp.Title, ViewContext.RouteData.GetRequiredString("action"), new { id=mp.NewsMID })
        @if( menuList.Count(p=>p.ParentID == mp.NewsMID ) > 0){
            @:<ul>
        }  
        
        @RenderMenuItem(menuList,mp)
       
        @if( menuList.Count(p=>p.ParentID == mp.NewsMID ) > 0){
            @:</ul>
        }
       
 </li>
}
</ul>


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

      if(menuList.Count(p=>p.ParentID == cp.NewsMID) > 0)
      {
          @:</ul>
      }
      else
      {
          @:</li>
      }
    }
}

Тут и кроется вся магия.

@foreach (var mp in menuList.Where(p => p.ParentID == 0)) — разбирает и выводит имена с ParentID = 0.

@RenderMenuItem(menuList,mp) — вызываем помощника вида, который уже рекурсивно достраивает все вложенности для каждого «рутовского» пункта.

@helper RenderMenuItem(List<zf2.Models.NewsM> menuList, zf2.Models.NewsM mi) — это и есть сам помощник вида, внутри которого и организована рекурсия.

@Html.ActionLink(mp.Title, ViewContext.RouteData.GetRequiredString("action"), new { id=mp.NewsMID }) — создаем ссылки. У меня используется стандартная маршрутизация.Имя контроллера подставляется автоматически. Имя экшена и параметр Id — указываем «вручную».
Тоесть ViewContext.RouteData.GetRequiredString("action") — получаем имя экшена. Аналогично можно получить имя контроллера.
new { id=mp.NewsMID } — задаем параметр Id.
mp.Title — Имя ссылки

Далее создаем еще один Частичный скрипт вида с названием "_Content".
В нем будем отображать содержимое выбранной статьи по переданному Id.
Код такой:
@{
    List<zf2.Models.NewsM> menuList = ViewBag.Menu;
}
@ViewBag.Id
@foreach (var mp in menuList.Where(p => p.NewsMID == ViewBag.Id))
{
    @mp.Content
    @mp.AddTitle
    @mp.Description
}


4) Основной скрипт вида. У меня он называется как и имя экшена в контроллере — NewsA.cshtml
В нем мы просто вызываем наши частичные скрипты вида и выводим заголовок.
@{
    ViewBag.Title = "NewsA";
}
@{
    List<zf2.Models.NewsM> menuList = ViewBag.Menu;
}

<div class="row">
  <div class="span3"style="background-color: #e6e6e6;">
      @Html.Partial("_Menu")
  </div>
  <div class="span6" style="background-color: #e6e6e6;">
      @Html.Partial("_Content")
  </div>
</div>


,  - это использование 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<ZfContext> { protected override void Seed(ZfContext context) { var newsMs = new List<NewsM> { 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(); } } }


Следующие статьи уже создаются...
Tags:
Hubs:
-1
Comments 11
Comments Comments 11

Articles