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

от автора

Как и обещал в предыдущем посте 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> 

<div class="row">, <div class="span3"style="background-color: #e6e6e6;"> — это использование 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();         }     } } 

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

ссылка на оригинал статьи http://habrahabr.ru/post/174531/