Razor, nested layouts and sections

Razor, nested layouts: Thank you. You really did save my day. You both make it really easy to reuse page templates inside actual site layouts, so to speak. It's easy to get such a structure for your site:

_MasterLayout.cshtml

<html>
    <head><title>@ViewBag.Title</title></head>
    <body>
        <h1>From the master layout</h1>
        @RenderBody()
    </body>
</html>

_SubLayout.cshtml

@{
    Layout = "~/Views/Shared/_MasterLayout.cshtml";
}

<h2>From the sub-layout</h2>
@RenderBody()

PageItself.cshtml

@{
    Layout = "~/Views/Shared/_SubLayout.cshtml";
}

<h3>From the page itself!</h3>

And it works just as expected, ouputting:

From the master layout

From the sub-layout

From the page itself!

Great. But there is one thing that really annoys me with nested layouts: Sections.

First of all, any section defined in the master layout cannot be defined by the page itself. That mean the sub-layout must redefined every section of the master layout and re-render it for the page itself. That means this boilerplate code:

_SubLayout.cshtml

@Section PageMenu
{
    @RenderSection("PageMenu")
}

It's not all that bad. But ASP.NET MVC has been about avoiding duplication and boilerplate code from the very start. The _ViewStart.cshtml is a great example of this kind of philosophy. And I honestly expected more on this front from the ASP.NET MVC team.

The real fun begins if you have default content for sections. Here are multiple use cases for sections in a nested layout structure:

  • The master layout has default content and re-definable by the page itself only.
  • The master layout has default content, you want to provide default content in the sub-layout and also allow the page itself to redefine the section.
  • The master layout has default content but you want to override it in the sub-layout without enabling the page itself to redefine the section.

Now you can imagine how problematic that can be if in the previous example we have to re-render the section from the sub-layout. Lets find a solution for each case.

Default content in master layout, page itself can redefine

First on: A master layout defining a section only to be overridden by the page itself (not the sub-layout):

_SubLayout.cshtml

@if (IsSectionDefined("s1"))
{
    DefineSection("s1", () => Write(RenderSection("s1")));
}

How does it work? We first check if the page itself defines the section. If it does, we define the section. Now DefineSection() is not the usual @section syntax, but it does serve the same purpose. The second argument is the tricky one. We need to write the content of the section directly to the response using the Write method. The DefineSection takes an Action (well not really an Action, but the delegate does have the same signature), so we use a lambda to do you bidding here.

Default content in master and sub layouts, page itself can redefine

We can simply extend our previous example to make this one work:

_SubLayout.cshtml

@helper RenderSectionS1 ()
{
    <div>Default s1 from sub-layout</div>
}

@if (IsSectionDefined("s1"))
{
    DefineSection("s1", () => Write(RenderSection("s1")));
}
else
{
    DefineSection("s1", () => Write(RenderSectionS1()));
}

"Simply extend" may not be entirely accurate. This is still very complicated. The same Write method is used. I used the @helper to define the default content of the section because I like it better that stuffing angle brackets in a lambda. The alternative would have been:

_SubLayout.cshtml

@if (IsSectionDefined("s1"))
{
    DefineSection("s1", () => Write(RenderSection("s1")));
}
else
{
    DefineSection("s1", () => @<div>Default s1 from sub-layout</div>);
}

This version is shorter and might work well when the default content is only a message. But I find the () => @<div></div> syntax somewhat confusing and strange. But that's just me.

Default content in master layout, sub-layout can redefine.

This version does not allow the page itself to define the section. The solution is pretty simple: the plain old @section syntax.

_SubLayout.cshtml

@section s1
{
    <div>Default s1 from sub-layout</div>
}

What if I don't to remember this weird syntax?

Defining a section conditionally leads to a very confusing syntax. The ifs, the lambda, the Write method. It's easy to get something wrong. All is not lost, you can always write extension methods. Marcin Doboz did just that in this blog post.

Using Marcin's extension methods along with the @helper syntax yields reasonable results.

But in the end, what would really statisfy me from a readability standpoint would be the following syntax:

Not valid cshtml

@if (!IsSectionDefined("s1"))
{
    @section s1
    {
        <div>Default content</div>
    }
}

But conditional @section declarations are not allowed. They are only allowed at the root of the page.

asp.net-mvcc#razor
Posted by: Bryan Menard
Last revised: 02 Nov, 2011 12:55 PM History

Comments

No comments yet. Be the first!

No new comments are allowed on this post.