0 Comments
tl;dr

Let’s have an hypothetical example where you need to have two different layouts in your MVC application based on the fact that you are in a secured context or an anonymous context. Based on those contexts, if you are in a secured context a menu should be in the header as well as some other controls and in an anonymous context a simple logo should be displayed (before you say it, I already know that the partial _LoginPartial.cshtml does that, I’m just using this as an example). So given the default _Layout.cshtml in ASP.NET MVC 5:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>@ViewBag.Title - My ASP.NET Application</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <div class="navbar-inner">
            <div class="container">
                <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                @Html.ActionLink("Application name", "Index", "Home", null, new { @class = "brand" })
                <div class="nav-collapse collapse">
                    <ul class="nav">
                        <li>@Html.ActionLink("Home", "Index", "Home")</li>
                        <li>@Html.ActionLink("About", "About", "Home")</li>
                        <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
                    </ul>
                </div>
            </div>
        </div>
    </div>

    <div class="container">
        @RenderBody()
        <hr />
        <footer>
            <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
        </footer>
    </div>

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")    
    @RenderSection("scripts", required: false)
</body>
</html>

 

We can then create any views based on this layout by just adding new ones (and leaving the layout textbox empty), if we decided to use another layout for a given view all there is to do is add this little code snippet at the top of the View :

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

and then the view would be using this _SecuredLayout. Let’s do some modification in the _Layout, since we want to have different controls based on the context, but still want the same layout in the end. So in the top part, I will put everything that has to do with the navigation bar (the div with the class="navbar navbar-inverse navbar-fixed-top") in it’s own partial view and replace it with this:

<header>
    @RenderSection("header")
</header>

So now every view that is using the_Layout.cshtml has to have a section named header where they will define what is to be displayed in the header. So far not good, let’s face it the reason why we use a base layout is to not have to repeat this header section in every view. So I’ll go ahead and create 2 new layouts: _AnonymousLayout and _SecuredLayout. Once this is done all I have to do is assign the base layout and then define the header section, it would look something like this:

<!-- Anonymous Layout -->
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section header{
    <p>Some Anonymous header to be displayed here</p>
}

<!-- Secured Layout -->
@{
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@section header{
    @Html.Partial("_SecuredHeaderPartial") <!-- the header we removed earlier-->
}

 

Once that is done I can have any view use either the secured layout or the anonymous layout without them having to define a header section. Go ahead try it, it won’t work !!! You get the yellow screen of death (YSOD) stating

The "RenderBody" method has not been called for layout page "~/Views/Shared/_AnonymousLayout.cshtml".

that’s an easy one I just go back in both my new layouts and add the @RenderBody() and voila it works. So now I can go in my view and add some JS by defining the scripts section

<!-- index.cshtml -->
@section scripts{
    <script>
        console.log("in index");
    </script>
}

But something odd was happening in Visual Studio the intellisense wasn’t showing me any possible options for section….meh these things happen right ? So let’s refresh our page and see the beauty of JS…. YSOD again

The following sections have been defined but have not been rendered for the layout page "~/Views/Shared/_AnonymousLayout.cshtml": "scripts".

man I can’t catch a break !! Alright so go back in both my layouts and I add the @RenderSection("scripts", required: false) under @RenderBody(), refresh the page and we’re back on track once again. Or so I thought, but a quick glance in the source and something ugly shows up:

image

What is my script tag doing in the middle of a div…….. remember the @RenderSection("scripts", required: false) that we added under @RenderBody(), well this is exactly why. In order to have my script tag at the bottom I should have done this instead in both my layouts

<!-- Anonymous Layout or SecuredLayouts –>
@section scripts
{
    @RenderSection("scripts", required: false)
}

So basically I define my scripts section by telling it to render a section that is called scripts (again), back in my view Intellisense is even offering me the scripts section now and when I look in the source I can see that my script tag is where it should be:

image

 

So long story short (tl;dr):
  1. The new layout mustspecify what Layout to use i.e: @{Layout = "~/Views/Shared/_Layout.cshtml";}
  2. The new layout must have a call to @RenderBody()
  3. In order to have the scripts section (or any other section rendered in _Layout) where it should be in the source view (at the bottom), you must define the section and “tell it” the render a section i.e: @section scripts{@RenderSection("scripts", required: false)}

 

 

Happy coding