Udostępnij za pośrednictwem


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.

Comments

  • Anonymous
    December 15, 2010
    Pretty cool!  I hope to make use of that in my MVC CMS.   Pretty cool!

  • Anonymous
    December 28, 2010
    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.

  • Anonymous
    January 01, 2011
    The comment has been removed

  • Anonymous
    January 10, 2011
    @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.

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

  • Anonymous
    January 29, 2011
    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&quot;, false) cshtml page has @Html.Partial("ContentView&quot;, item) contentview.cshtml  has @section PanelScripts { ... } Thanks in advance.

  • Anonymous
    January 30, 2011
    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.

  • Anonymous
    March 11, 2011
    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!

  • Anonymous
    April 12, 2011
    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&quot;, @<div>markup</div>). If the method returned void you would have to do this: @{ this.RedefineSection("name", ...); }

  • Anonymous
    November 25, 2012
    @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??