Razor, Nested Layouts and Redefined Sections

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.