Всем доброго времени суток. С недавних пор занимаюсь активной разработкой на ASP.NET MVC 3 & Razor «непростого» веб-приложения и вот сегодня наткнулся на проблему, которая опытными разработчиками, может быть, уже исследована и решена, но вот новичкам информация ниже, думаю и надеюсь, окажется полезной.
Пусть в приложении есть пара представлений: View.cshtml и ViewWithSide.cshtml, а ещё есть две мастер-страницы: Layout.cshtml и LayoutWithSide.cshtml, причём первая является мастер-страницей для второй. Как можно догадаться по именам файлов, XxxWithSide.cshtml добавляет в странице боковую панель, формат вывода которой определён в мастер странице, а внутренности — в представлении. В главной мастер-странице Layout помимо основной разметки определён вывод секции «navigation», которая задаётся в представлениях.
И вот когда в коде ViewWithSide определена секция «navigation», а в LayoutWithSide нет, потому что эта секция должна обрабатываться «следующей» мастер-страницей (Layout), то при попытке открыть ViewWithSide в приложении будет выдана ошибка: The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/LayoutWithSide.cshtml": «navigation» (Секция «navigation» определена, но нигде не выведена в мастер-странице).
Идея решения этой проблемы довольно проста: нужно передать вывод этой секции «следующей» мастер-странице, а там пусть сами разбираются.
Layout.cshtml
LayoutWithSide.cshtml
View.cshtml
ViewWithSide.cshtml
Вполне естественно, я ожидал, что Razor и ASP.NET MVC сами разберутся и выведут секцию в той мастер-странице, где она нужна. Но увы и ах… Однако есть проблема, есть идея как решить — надо решать.
В моём случае секция «navigation» является не просто необязательной, а если она не определена, то не выводится ещё некоторый кусок разметки. По этой причине просто завести одноимённую секцию в LayoutWithSide и вывести в неё переданное из представления содержимое не прокатило бы.
Я попробовал объявление секции завернуть внутрь
Беглый и поверхностный поиск по MSDN и интернету ничего полезного не выдал. Зато в доступных внутри представления методах сразу был подмечен метод DefineSection(string name, SectionWriter action)
Раз не удалось завернуть в if объявление секции в стиле Razor, можно попробовать завернуть создание секции из C# кода. Получилось так:
И этот код успешно отработал и выполнил поставленную задачу.
Конечно, я на этом не остановился, не писать жетак многа букав такой код для каждой секции, которую нужно передать мастер-странице.
У нас в распоряжении естьмодные удобные расширяющие методы C#. В результате написал следующий класс:
И после его подключения к проекту достаточно вызвать метод, передав ему имена нужных секций. Тогда LayoutWithSide.cshtml будет выглядеть так:
А если надо передать мастер-странице несколько секций, то можно вызвать
Описание проблемы
Пусть в приложении есть пара представлений: View.cshtml и ViewWithSide.cshtml, а ещё есть две мастер-страницы: Layout.cshtml и LayoutWithSide.cshtml, причём первая является мастер-страницей для второй. Как можно догадаться по именам файлов, XxxWithSide.cshtml добавляет в странице боковую панель, формат вывода которой определён в мастер странице, а внутренности — в представлении. В главной мастер-странице Layout помимо основной разметки определён вывод секции «navigation», которая задаётся в представлениях.
И вот когда в коде ViewWithSide определена секция «navigation», а в LayoutWithSide нет, потому что эта секция должна обрабатываться «следующей» мастер-страницей (Layout), то при попытке открыть ViewWithSide в приложении будет выдана ошибка: The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/LayoutWithSide.cshtml": «navigation» (Секция «navigation» определена, но нигде не выведена в мастер-странице).
Идея решения этой проблемы довольно проста: нужно передать вывод этой секции «следующей» мастер-странице, а там пусть сами разбираются.
Немного кода
Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<title>@ViewBag.Title</title>
</head>
<body>
<div>
@if (IsSectionDefined("navigation")) { <div class="navigation">@RenderSection("navigation")</div> }
@RenderBody()
</div>
</body>
</html>
LayoutWithSide.cshtml
@{
Layout = "~/Views/Shared/Layout.cshtml";
}
<div class="side">@RenderSection("side", required:false)</div>
<div class="main">@RenderBody()</div>
View.cshtml
@{
ViewBag.Title = "view";
Layout = "~/Views/Shared/Layout.cshtml";
}
@section navigation {
<a href="@Url.Action("Page1")">page1</a> |
<a href="@Url.Action("Page2")">page2</a>
}
<h2>viewWithoutSide</h2>
<div>Main content</div>
ViewWithSide.cshtml
@{
ViewBag.Title = "viewWithSide";
Layout = "~/Views/Shared/LayoutWithSide.cshtml";
}
@section navigation {
<a href="@Url.Action("Page1")">page1</a> |
<a href="@Url.Action("Page2")">page2</a>
}
@section side {
<strong>side content</strong>
}
<h2>viewWithSide</h2>
<div>Main content</div>
Вполне естественно, я ожидал, что Razor и ASP.NET MVC сами разберутся и выведут секцию в той мастер-странице, где она нужна. Но увы и ах… Однако есть проблема, есть идея как решить — надо решать.
Поиски решения
В моём случае секция «navigation» является не просто необязательной, а если она не определена, то не выводится ещё некоторый кусок разметки. По этой причине просто завести одноимённую секцию в LayoutWithSide и вывести в неё переданное из представления содержимое не прокатило бы.
Я попробовал объявление секции завернуть внутрь
if (IsSectionDefined("navigation"))
, авось… Особых надежд на этот метод не возлагал — он и не заработал (анализатор просто не ожидает такой конструкции и ругается на неё «Parser Error»).Беглый и поверхностный поиск по MSDN и интернету ничего полезного не выдал. Зато в доступных внутри представления методах сразу был подмечен метод DefineSection(string name, SectionWriter action)
Раз не удалось завернуть в if объявление секции в стиле Razor, можно попробовать завернуть создание секции из C# кода. Получилось так:
if (IsSectionDefined("navigation"))
{
DefineSection("navigation", delegate() { Write(RenderSection("navigation")); });
}
И этот код успешно отработал и выполнил поставленную задачу.
Решение
Конечно, я на этом не остановился, не писать же
У нас в распоряжении есть
public static class WebPageHelpers
{
public static void PropagateSection(this WebPageBase page, string sectionName)
{
if (page.IsSectionDefined(sectionName))
{
page.DefineSection(sectionName, delegate() { page.Write(page.RenderSection(sectionName)); });
}
}
public static void PropagateSections(this WebPageBase page, params string[] sections)
{
foreach (var s in sections)
PropagateSection(page, s);
}
}
И после его подключения к проекту достаточно вызвать метод, передав ему имена нужных секций. Тогда LayoutWithSide.cshtml будет выглядеть так:
@{
Layout = "~/Views/Shared/Layout.cshtml";
this.PropagateSection("navigation");
}
<div class="side">@RenderSection("side", required:false)</div>
<div class="main">@RenderBody()</div>
А если надо передать мастер-странице несколько секций, то можно вызвать
this.PropagateSections("section1", "section2", "section3")
, ну вы поняли…