Search code examples
asp.netasp.net-mvcpartial-viewsasp.net-mvc-layout

How can I place code in the <head> or <body> element from a PartialView so as to avoid code repetition?


I'm retrieving statistics information from the database in the form and shape of <script> tags for all my Views, some of which will go in the <head>, others in the <body>.

Some of this information is shared by all Views, like Google Analytics but other information is specific to certain Views only like order details which is only available in the Order Confirmation View.

The end result must be:

<head>
    <script><!-- Google Analytics --></script>
</head>

I am using named sections in the Layout to achieve what I want:

<head>
    @RenderSection("headScripts", required: false)
</head>

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

A sample View code is:

@if ((Model.Scripts.Head != null) && (Model.Scripts.Head.Count() != 0))
{

    <text>
    @section headScripts
    {
        @foreach (var script in Model.Scripts.Head)
        {
            @Html.Raw(@script.Code);
        }
    }
    </text>

}

All view models are inheriting from a base class with the Scripts field and the code I pasted above is replicated in all my views, which is a problem.

I tried to move the code to a PartialView, starting with this line right below the </body> in my Layout:

@{Html.RenderAction("Index", "Scripts");}

And in my ScriptsController:

public ActionResult Index()
{
    Scripts scripts = new Scripts();
    scripts.Head = whatever comes from the database;
    scripts.Body = whatever comes from the database;    

    return PartialView("/Views/Shared/scripts.cshtml", scripts);
}

Everything worked, the Model is correctly populated and available in the scripts Partial View but unfortunately @section cannot be called in a PartialView so the <script> tags are not displayed.

Any workaround to have @if ((Model.Scripts.Head != null) && (Model.Scripts.Head.Count() != 0)) and the rest of the code in one common place used by all Views?


Solution

  • Maybe do it like this

    <head>
        @RenderSection("headScripts", required: false)
        @Html.RenderAction("HeadScripts", "Scripts")
    </head>
    
    <body>
        @RenderSection("bodyScripts", required: false)
        @Html.RenderAction("BodyScripts", "Scripts")
    </body>
    

    Then in your scripts controller you would have two methods for each call

    public ActionResult HeadScripts()
    {
        Scripts scripts = new Scripts();
        scripts.Head = whatever comes from the database;    
    
        return PartialView("/Views/Shared/scripts.cshtml", scripts);
    }
    
    public ActionResult BodyScripts()
    {
        Scripts scripts = new Scripts();
        scripts.Body = whatever comes from the database;    
    
        return PartialView("/Views/Shared/scripts.cshtml", scripts);
    }
    

    Hope this helps

    EDIT: Also in the PartialView you won't need the @section anymore.

    @if ((Model.Scripts.Head != null) && (Model.Scripts.Head.Count() != 0))
    {
        @foreach (var script in Model.Scripts.Head)
        {
            @Html.Raw(@script.Code);
        }
    }
    

    EDIT 2: Using a BaseController with a ViewBag

    public class BaseController : Controller
    {
        protected override void OnActionExecuting(ActionExecutingContext filterContext)
        {
           ViewBag.HeadStart = whatever comes from the database;
           ViewBag.HeadEnd = whatever comes from the database;
           ViewBag.BodyStart = whatever comes from the database;
           ViewBag.BodyEnd = whatever comes from the database;
        }
    }
    

    Then in every controller you'll inherit from this base controller

    public class HomeController : BaseController
    {
        // some methods here
    }
    

    And finally in the view

    <head>
        @if (ViewBag.HeadStart != null)
        {
            @foreach (var script in ViewBag.HeadStart)
            {
               @Html.Raw(@script.Code);
            }
        }
    
        @RenderSection("headScripts", required: false)
        @* do the same for end *@
    </head>
    <body>
    @* same thing here as well *@
    </body>