2
votes

Following is the exact scenario in my ASP.NET MVC application:

There are two layout pages which are quite identical to each other. But one is having angular related attributes in "" tag, whereas other is a non-angular layout. In order to avoid duplicate markup in both razor layout files, I thought to create a partial view and share it across the layout pages.

Following is the partial view (razor), I have named it as "_LayoutPartial":

_LayoutPartial.cshtml

<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("Application name", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Home", "Index", "Home")</li>
                <li>@Html.ActionLink("About", "About", "Home")</li>
                <li>@Html.ActionLink("Contact", "Contact", "Home")</li>
            </ul>
            @Html.Partial("_LoginPartial")
        </div>
    </div>
</div>
<div class="container body-content">
    @RenderBody()
    <hr />
    <footer>
        <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
    </footer>
</div>

The above partial view is shared in "_Layout.cshtml", and "_AngularLayout.cshtml" which are as per below:

_Layout.cshtml

<body>
    @Html.Partial("_LayoutPartial")

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

_AngularLayout.cshtml

<body ng-app="myAngularLab">
    @Html.Partial("_LayoutPartial")

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

When I try to run the MVC application, I get following error:

The file "~/Views/Shared/_LayoutPartial.cshtml" cannot be requested directly because it calls the "RenderBody" method.

The error message is very obvious, and it seems that we can use RenderBody method only in the master page, and not anywhere else. But I am keen to know how we can have two identical layout pages (with a little differences as shown in the example) by writing common code instead of keeping duplicate code in both layout pages?

2
Can you take out this code <div class="container body-content"> @RenderBody() <hr /> <footer> <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p> </footer> </div> FROM common file and put it individually in both the Angular and _Layout file? - Unbreakable
I am a beginner, just wondering if that would make any difference - Unbreakable
Yes, that works. Basically, if we put RenderBody in _Layout page it should work by design. But the reason I am not preferring to do at this stage is because currently the layout page doesn't have much contents, but as we grow our website the layout page is bound to become more complex and then it will be a lot of duplicate contents in both the files if we copy everything starting from RenderBody into both layout files and it will defeat the purpose. - Nirman
The other option is to create multiple partial views and take out most of the things except "RenderBody" into respective partial views, and to put RenderBody in the layout pages. But that will involve number of partial views, so just thinking if there is any more efficient way than this? - Nirman
@Nirman What about using "nested layout"? I often use multiple master layout as shared layouts with other child layouts referencing it, there's no need to repeat @Scripts & @Styles declaration in child layouts. In this case, ng-app="myAngularLab" placed on a div instead of body. - Tetsuya Yamamoto

2 Answers

2
votes

I think you need to use "nested layout" by putting one layout page as "master page" for other layouts, similar to webforms counterpart:

_BaseLayout.cshtml

<html>
<head>
    <!-- other header tags (link, meta, title etc.) -->

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")

    <!-- JS & CSS includes if available -->
</head>
<body>
    <div class="navbar navbar-inverse navbar-fixed-top">
        <!-- other elements -->
    </div>
    <div class="container body-content">
        @RenderBody()
        <!-- other elements -->
    </div>

    @RenderSection("scripts", required: false)
</body>
</html>

_Layout.cshtml

@{ Layout = "~/Views/Shared/_BaseLayout.cshtml"; } // put reference to base layout here

<div>
    <!-- Non-Angular layout -->
    @RenderBody()
</div>

_AngularLayout.cshtml

@{ Layout = "~/Views/Shared/_BaseLayout.cshtml"; } // put reference to base layout here

<div ng-app="myAngularLab">
    <!-- Angular layout -->
    @RenderBody()
</div>

Advantages using "nested layout":

  1. Eliminates the need to repeat @Styles.Render & @Scripts.Render, also for @RenderSection (they're inserted automatically for every pages referencing the base layout).

  2. Eliminates the need to use more than one body tag, just replace with div tag to hold view page contents.

The file "X" cannot be requested directly because it calls the "RenderBody" method certainly originated from Html.Partial which called directly from child layouts, where a layout page with RenderBody method can't be requested directly as a template.

If you want to set default layout for a nested layout, put one of the child layout reference in _ViewStart.cshtml:

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

Another note: Partial views are intended to use in view pages, not in layout pages. Layout pages are specifically used as placeholder for view pages - not for directly accessed by action methods.

0
votes

Try this in _Layout.cshtml:

<body>
    @Html.Partial("_LayoutPartial")

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/bootstrap")
    @RenderSection("scripts", required: false)
   <div class="container body-content">
    @RenderBody()
    <hr />
    <footer>
        <p>&copy; @DateTime.Now.Year - My ASP.NET Application</p>
    </footer>
</div>
</body>

and remove the @RenderBody() from _LayoutPartial.cshtml.

@RenderBody() is commonly used in layout pages, it renders the portion of a content page that is not within a named section.

Hope this is helpful:)