Pull to refresh

Razor: вывод секций в мастер-страницах мастер-страниц

Reading time4 min
Views21K
Всем доброго времени суток. С недавних пор занимаюсь активной разработкой на 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
<!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")); });
}

И этот код успешно отработал и выполнил поставленную задачу.

Решение


Конечно, я на этом не остановился, не писать же так многа букав такой код для каждой секции, которую нужно передать мастер-странице.
У нас в распоряжении есть модные удобные расширяющие методы C#. В результате написал следующий класс:
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"), ну вы поняли…
Tags:
Hubs:
Total votes 11: ↑6 and ↓5+1
Comments5

Articles