Marcin On ASP.NET

Keeping my eye on the dot

Razor, Nested Layouts and Redefined Sections

Razor, Nested Layouts and Redefined Sections

  • Comments 10

In a recent post I introduced a technique for dealing with optional Razor sections and default content. In this post I will expand upon that technique and describe a way of working with sections across nested layout hierarchies. If you are not familiar with sections, layout pages, or my technique then go ahead and read that post to catch up.

One aspect of the relationship between layout pages and sections in Razor that a fair number of people might find surprising is that a section defined in a content page can only be used in its immediate layout. There is implicit scoping going on that prevents certain use cases. Take the following example:

<!DOCTYPE html>
<html>
<body>
@RenderSection("SubSection")
@RenderBody()
</body>
</html>
@{
    Layout = "~/Views/_MasterLayout.cshtml";
}
<div>
@section SubSection {
<h1>Title</h1>
}
@RenderBody()
@RenderSection("ContentSection")
</div>
@{
    Layout = "~/Views/_SubLayout.cshtml";
}
<div>
<p>Main content</p>
@section ContentSection {
<div>Footer</div>
}
</div>

In the above example you can certainly call RenderSection("SubSection") in _MasterLayout.cshtml, as well as call RenderSection("ContentSection") in _SubLayout.cshtml. However, it is impossible to call RenderSection("ContentSection") from _MasterLayout.cshtml because the file rendering the section and the file defining the section are not directly related. Essentially sections are limited to the Content-Layout scope and are not accessible to other layout pages outside of that scope.

Redefining sections

You can work around this by essentially redefining the section in the intermediate layout. 

@{
    Layout = "~/Views/_MasterLayout.cshtml";
}
<div>
@section SubSection {
<h1>Title</h1>
}
@RenderBody()
@section ContentSection {
  @RenderSection("ContentSection", required: false)
}
</div>

Now you are able to reference “ContentSection” from _MasterLayout.cshtml. However you should be aware that you are not overriding “ContentSection" in the same sense as overriding methods in a child class. You are actually defining a new section named “ContentSection” in the SubLayout-MasterLayout scope that renders the section named “ContentSection” from the Content-SubLayout scope. The fact that the names match is incidental. The names certainly do not have to match.

Things get even more complicated when you want to use the IsSectionDefined method to conditionally provide default content for optional sections. Because it’s necessary to propagate “ContentSection” by redefining the section in _SubLayout.cshtml you can no longer depend on IsSectionDefined returning the expected result.

Conditionally redefining sections via RedefineSection

Fortunately not everything is lost. What you want to do is to conditionally redefine a section. Building on the RenderSection helper method from my previous post I created the RedefineSection more helper methods to aid in this scenario:

public static class SectionExtensions {
    private static readonly object _o = new object();
    public static HelperResult RenderSection(this WebPageBase page,
                            string sectionName,
                            Func<object, HelperResult> defaultContent) {
        if (page.IsSectionDefined(sectionName)) {
            return page.RenderSection(sectionName);
        }
        else {
            return defaultContent(_o);
        }
    }

    public static HelperResult RedefineSection(this WebPageBase page,
                            string sectionName) {
        return RedefineSection(page, sectionName, defaultContent: null);
    }

    public static HelperResult RedefineSection(this WebPageBase page,
                            string sectionName,
                            Func<object, HelperResult> defaultContent) {
        if (page.IsSectionDefined(sectionName)) {
            page.DefineSection(sectionName,
                               () => page.Write(page.RenderSection(sectionName)));
        }
        else if (defaultContent != null) {
            page.DefineSection(sectionName,
                               () => page.Write(defaultContent(_o)));
        }
        return new HelperResult(_ => { });
    }
}

The RedefineSection method conditionally redefines a section (that is it redefines in only if a section was already defined in a content page). The second overload also allows you to provide a default content template that will be used if the content page did not define the section. Using this code you can write the following pages:

<!DOCTYPE html>
<html>
<body>
@RenderSection("TitleSection", required: false)
@RenderBody()
</body>
</html>
@{
    Layout = "~/Views/_MasterLayout.cshtml";
}
<div>
@this.RedefineSection("TitleSection",
                      @<h1>Default SubLayout title</h1>)
@RenderBody()
</div>
@{
    Layout = "~/Views/_SubLayout.cshtml";
}
@section TitleContent {
<h1>Title</h1>
}
<p>Main content</p>

In the above example Content.cshtml defines the “TitleContent” section. _SubLayout.cshtml redefines that section but also provides some default markup in case the content page does not have the “TitleContent” section defined. Finally, _MasterLayout.cshtml consumes the section indicating that it is optional – this means that the entire site will still work even if the content page does not define the section and the intermediate layout does not provide a default value.

Hope you find the above technique useful in your complex layout pages. Please let me know if you encounter any issues and whether these methods are valuable enough that they should be added to the framework for the next version.

  • Pretty cool!  I hope to make use of that in my MVC CMS.   Pretty cool!

  • Thanks for the great addition.  For my app, www.takeoffvideo.com, we had defined a base master page that had some nice helpers, such as providing code for jquery's document.ready function.  This way, in a content page, we didn't have to write out the script tags and the $(document).ready container.

    I had tried to achieve the same thing when upgrading some pages to Razor.  I had 2 layout levels, so I couldn't get that document.ready helper to not render when the content page didn't define it.  With your code it was a snap.

    So yes, I would love to see it in the framework.  I think once people get into Razor, they'll start needing such functionality.  Keep up the good work.

  • Your post is really interesting, and I have a little side problem, related probably to the limitations you mention in this post.

    Let's suppose I want to create a 'boxed content', i.e. a box partial view where the developer can pass a custom content, in the form of direct html text, or better, a different partial page.

    @*_box.cshtml*@

    <div class="boxstyle">

       @RenderSection("Content", false)

    </div>

    Just with this simple partial page, if I try to call it with

    @Html.Partial("_box")

    I get the strange error:

    Server Error in '/' Application.

    --------------------------------------------------------------------------------

    The file "~/Views/Shared/_box.cshtml" cannot be requested directly because it calls the "RenderSection" method.

    I would like to invoke it like

    @Html.Partial("_box", new BoxModel("_content")),

    where (in the box) I use it as in

    @RenderSection(Model.Content).

    This way is impossible to follow, because of the error: there can be a lot of alternative solutions, such as yours, but I feel that this one would be the more flexible.

    Do you know what error is this? And how could we implement such a flexible solution?

    Thanks in advance

    Andrea Bioli

  • @Andrea Bioli,

    I have a application where I should use the same solution you were looking for. Could you, please, share a small example, to be more detailed on the solution you found?

    Thanks.

  • This is awesome! thanks for much for the extension method code, worked like a charm

  • Hello,

    I am really sorry but I could not use section rendering inside partial view. Could you please give me a sample?

    Scenerio;

    Layout has

    @RenderSection("PanelScripts", false)

    cshtml page has

    @Html.Partial("ContentView", item)

    contentview.cshtml  has

    @section PanelScripts {

    ...

    }

    Thanks in advance.

  • Nuri,

    sections do not work between a page and a partial page. That is because a RenderPartial call ends up executing a brand new Razor page.

  • Based on your example I have added more overloads including redefining all the sections that were used in the page (useful for intermediar layouts which want to promote all pages' sections towards its layout). Sadly it needs a bit of reflection to work.

    private static PropertyInfo PreviousSectionWriters =
       typeof(WebPageBase).GetProperty("PreviousSectionWriters",
          BindingFlags.Instance | BindingFlags.NonPublic);
    
    public static HelperResult RedefineSections(this WebPageBase page)
    {
       var sections = (Dictionary<string, SectionWriter>)PreviousSectionWriters
          .GetValue(page, null);
       if (sections != null)
          foreach (var item in sections)
             page.RedefineSection(item.Key);
       return new HelperResult(_ => { });
    }

    My all other overloads are:

    public static HelperResult RenderSection(this WebPageBase page,
          string sectionName,
          Func<object, HelperResult> defaultContent)
    {
       if (page.IsSectionDefined(sectionName))
          return page.RenderSection(sectionName);
       else return defaultContent(_o);
    }
    
    public static HelperResult RenderSection(this WebPageBase page,
          string sectionName,
          string defaultContent)
    {
       if (page.IsSectionDefined(sectionName))
          return page.RenderSection(sectionName);
       else return new HelperResult(a => a.Write(defaultContent));
    }
    
    public static HelperResult RenderSection(this WebPageBase page,
          string sectionName,
          MvcHtmlString defaultContent)
    {
       if (page.IsSectionDefined(sectionName))
          return page.RenderSection(sectionName);
       else return new HelperResult(a => a.Write(defaultContent));
    }
    
    public static HelperResult RedefineSection(this WebPageBase page,
          string sectionName)
    {
       return RedefineSection(page, sectionName, defaultContent: null);
    } public static HelperResult RedefineSections(this WebPageBase page, string sectionNames) { HelperResult a = null; foreach (var sectionName in (sectionNames ?? "").Split(',')) { a = RedefineSection(page, sectionName, defaultContent: null); } return a; } public static HelperResult RedefineSection(this WebPageBase page, string sectionName, Func<object, HelperResult> defaultContent) { if (page.IsSectionDefined(sectionName)) { page.DefineSection(sectionName, () => page.Write(page.RenderSection(sectionName))); } else if (defaultContent != null) { page.DefineSection(sectionName, () => page.Write(defaultContent(_o))); } return new HelperResult(_ => { }); }

    Now Razor Engine Rocks!

    Thank you for sharing your ideas!

  • Why are you returning an empty HelperResult?

    Why can't you return void and call it in a code block (or at least return null)?

    Marcin: I'm returning an empty HelperResult so that you can use better syntax: @this.RedefineSection("name", @<div>markup</div>). If the method returned void you would have to do this: @{ this.RedefineSection("name", ...); }
  • @Marcin Doboz

    "Nuri,

    sections do not work between a page and a partial page. That is because a RenderPartial call ends up executing a brand new Razor page."

    Is there a workaround??

Page 1 of 1 (10 items)
Leave a Comment
  • Please add 8 and 4 and type the answer here:
  • Post