Razor Nested Layouts with Cascading Sections

I have an MVC3 site using Razor as its view engine. I want my site to be skinnable. Most of the possible skins are similar enough that they can derive from a shared master layout.

Therefore, I am considering this design:

Planned view diagram

However, I would like to be able to call RenderSection in the bottom layer, _Common.cshtml, and have it render a section that is defined in the top layer, Detail.cshtml. This doesn't work: RenderSection apparently only renders sections that are defined the next layer up.

Of course, I can define each section in each skin. For instance, if _Common needs to call RenderSection("hd") for a section defined in Detail, I just place this in each _Skin and it works:

@section hd {
    @RenderSection("hd")
}

This results in some duplication of code (since each skin must now have this same section) and generally feels messy. I'm still new to Razor, and it seems like I might be missing something obvious.

When debugging, I can see the complete list of defined sections in WebViewPage.SectionWritersStack. If I could just tell RenderSection to look through the entire list before giving up, it would find the section I need. Alas, SectionWritersStack is non-public.

Alternatively, if I could access the hierarchy of layout pages and attempt execution of RenderSection in each different context, I could locate the section I need. I'm probably missing something, but I don't see any way to do this.

Is there some way to accomplish this goal, other than the method I've already outlined?


Solution 1:

This is in fact not possible today using the public API (other than using the section redefinition approach). You might have some luck using private reflection but that of course is a fragile approach. We will look into making this scenario easier in the next version of Razor.

In the meantime, here's a couple of blog posts I've written on the subject:

  • http://blogs.msdn.com/b/marcinon/archive/2010/12/08/optional-razor-sections-with-default-content.aspx
  • http://blogs.msdn.com/b/marcinon/archive/2010/12/15/razor-nested-layouts-and-redefined-sections.aspx

Solution 2:

@helper ForwardSection( string section )
{
   if (IsSectionDefined(section))
   {
       DefineSection(section, () => Write(RenderSection(section)));
   }
}

Would this do the job ?

Solution 3:

I'm not sure if this is possible in MVC 3 but in MVC 5 I am able to successfully do this using the following trick:

In ~/Views/Shared/_Common.cshtml write your common HTML code like:

<!DOCTYPE html>
<html lang="fa">
<head>
    <title>Skinnable - @ViewBag.Title</title>
</head>
<body>
@RenderBody()
</body>
</html>

In ~/Views/_ViewStart.cshtml:

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

Now all you have to do is to use the _Common.cshtml as the Layout for all the skins. For instance, in ~/Views/Shared/Skin1.cshtml:

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

<p>Something specific to Skin1</p>

@RenderBody()

Now you can set the skin as your layout in controller or view based on your criteria. For example:

    public ActionResult Index()
    {
        //....
        if (user.SelectedSkin == Skins.Skin1)
            return View("ViewName", "Skin1", model);
    }

If you run the code above you should get a HTML page with both the content of Skin1.cshtml and _Common.cshtml

In short, you'll set the layout for the (skin) layout page.