Как и обещал в предыдущем посте 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 фреймворка. Более подробно можно ознакомиться тут:
Все. Запускаем. И видим похожую картинку после заполнения:
ПС:
Нужно еще создать контроллер для работы с моделью.
Подключение к БД
Класс работы с 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();
}
}
}
Следующие статьи уже создаются…
Автор: struggleendlessly