112

I want to keep all of my JavaScript code in one section; just before the closing body tag in my master layout page and just wondering the best to go about it, MVC style.

For example, if I create a DisplayTemplate\DateTime.cshtml file which uses jQuery UI's DateTime Picker than I would embed the JavaScript directly into that template but then it will render mid-page.

In my normal views I can just use @section JavaScript { //js here } and then @RenderSection("JavaScript", false) in my master layout but this doesn't seem to work in display/editor templates - any ideas?

eth0
  • 4,977
  • 3
  • 34
  • 48
  • 4
    for anyone coming to this later - there is a nuget package for handling this: http://nuget.org/packages/Forloop.HtmlHelpers/ – Russ Cam Jun 17 '13 at 20:20

8 Answers8

198

You could proceed with a conjunction of two helpers:

public static class HtmlExtensions
{
    public static MvcHtmlString Script(this HtmlHelper htmlHelper, Func<object, HelperResult> template)
    {
        htmlHelper.ViewContext.HttpContext.Items["_script_" + Guid.NewGuid()] = template;
        return MvcHtmlString.Empty;
    }

    public static IHtmlString RenderScripts(this HtmlHelper htmlHelper)
    {
        foreach (object key in htmlHelper.ViewContext.HttpContext.Items.Keys)
        {
            if (key.ToString().StartsWith("_script_"))
            {
                var template = htmlHelper.ViewContext.HttpContext.Items[key] as Func<object, HelperResult>;
                if (template != null)
                {
                    htmlHelper.ViewContext.Writer.Write(template(null));
                }
            }
        }
        return MvcHtmlString.Empty;
    }
}

and then in your _Layout.cshtml:

<body>
...
@Html.RenderScripts()
</body>

and somewhere in some template:

@Html.Script(
    @<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>
)
Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 3
    As a dictionary is un-ordered how would I do first in first out? The order it outputs is random (presumably because of the Guid).. – eth0 Mar 25 '11 at 15:56
  • Perhaps you could set up a static integer field and use Interlocked.Increment() in place of the GUID to get ordering, but even then I think a dictionary never guarantees ordering. On second thoughts, maybe a static field is dodgy as it might get kept across page displays. Instead could add an integer to the Items dictionary, but you'd have to put a lock around it. – Mark Adamson May 30 '11 at 20:49
  • I started using this solution recently, but I can't seem to stuff two scripts in a single @Html.Script() line, because I'm not sure how HelperResult works. Is it not possible to do 2 script blocks in 1 Html.Script call? – Langdon Jun 03 '11 at 21:01
  • I solved it for now by changing the Func parameter in the Script helper method to params. You have to add funky commas between your scripts, but it's better than calling Script multiple times. – Langdon Jun 06 '11 at 14:48
  • Anyone come up with a safe way to ensure script order? – one.beat.consumer Dec 06 '11 at 23:33
  • @one.beat.consumer, see my answer below. – eth0 Dec 19 '11 at 10:06
  • @Darin, Has this been helped or made obsolete with the release of MVC4? Specifically with Bundling and `Scripts.Render()`? – Tim Meers Sep 11 '12 at 14:59
  • 3
    @TimMeers, what do you mean? For me all this has always been obsolete. I wouldn't use those helpers at all. I never had the need to include any scripts in my partial views. I would simply stick to the standard Razor `sections`. In MVC4 Bundling could indeed be used as well as it helps reducing the size of scripts. – Darin Dimitrov Sep 11 '12 at 15:05
  • @Darin, makes much more sense now. I needed to look at `sections`, not helpers for this. Thanks for pointing me in the right direction. – Tim Meers Sep 11 '12 at 15:16
  • The .Scripts(...) method takes a Func parameter. This means that in the calling code, the Developer would always have to write **@** and for different scripts the only thing they'd change is the src attribute ("xxx.js"). I tried to have a override that just took the "xxx.js" string and generate the Function object, but as yet have had no luck. Any suggestions here? – DrGriff Dec 20 '12 at 16:40
  • Thank you! Great piece of code! I would like it to behave a bit more like '@Section scripts' where you put in a block of code instead of a single line. – Talon Jun 06 '13 at 10:46
  • 5
    This approach doesn't work if you want to place your scripts or styles in `head` tag instead of at the end of the `body` tag, because `@Html.RenderScripts()` will be executed before your partial view and therefore before `@Html.Script()`. – Maksim Vi. Aug 13 '13 at 18:15
  • Note that this can be easily used in asp.net core by replacing `IHtmlString` and `MvcHtmlString` with `HtmlString`. Also `this HtmlHelper` has to be replaced with `this IHtmlHelper` – Auroratic Sep 27 '17 at 07:18
  • You can wrap multiple scripts in a single div `@Html.Script(@
    )`
    – Chris F Carroll Oct 08 '18 at 17:55
  • I'm trying to achieve the same in .NET Core. I did some modifications but basically everything is the same. There is only one problem: _Layout is executed first, so @Html.RenderScripts() is executed before the partial view, which means that it is executed before @Html.Script() so scripts are not rendered at the end of the body. Any suggestions? –  Oct 19 '18 at 12:55
  • Is there any unit test code example that anyone can recommend for this ? – love2code Nov 17 '19 at 19:56
  • @MaksimVi.To include code in the `head` of a page, define a Head section in your layout template, then move your script tags to a `@section Head { ... }` block in your view. Works like `@section Scripts { ... }` in the body. – Suncat2000 Jan 07 '21 at 15:34
44

Modified version of Darin's answer to ensure ordering. Also works with CSS:

public static IHtmlString Resource(this HtmlHelper HtmlHelper, Func<object, HelperResult> Template, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type]).Add(Template);
    else HtmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, HelperResult>>() { Template };

    return new HtmlString(String.Empty);
}

public static IHtmlString RenderResources(this HtmlHelper HtmlHelper, string Type)
{
    if (HtmlHelper.ViewContext.HttpContext.Items[Type] != null)
    {
        List<Func<object, HelperResult>> Resources = (List<Func<object, HelperResult>>)HtmlHelper.ViewContext.HttpContext.Items[Type];

        foreach (var Resource in Resources)
        {
            if (Resource != null) HtmlHelper.ViewContext.Writer.Write(Resource(null));
        }
    }

    return new HtmlString(String.Empty);
}

You can add JS and CSS resources like this:

@Html.Resource(@<script src="@Url.Content("~/Scripts/jquery-1.4.4.min.js")" type="text/javascript"></script>, "js")
@Html.Resource(@<link rel="stylesheet" href="@Url.Content("~/CSS/style.css")" />, "css")

And render JS and CSS resources like this:

@Html.RenderResources("js")
@Html.RenderResources("css")

You could do a string check to see if it starts with script/link so you don't have to explicitly define what each resource is.

KyleMit
  • 30,350
  • 66
  • 462
  • 664
eth0
  • 4,977
  • 3
  • 34
  • 48
  • Thanks eth0. I compromised on this issue, but I'll have to check this out. – one.beat.consumer Dec 19 '11 at 21:00
  • I know this almost 2 years ago, but is there a way to check if the css/js file already exists and not render it? Thanks – CodingSlayer Mar 10 '14 at 17:44
  • 1
    ok. Not sure how effecient it is, but currently i am doing this: var httpTemplates = HtmlHelper.ViewContext.HttpContext.Items[Type] as List>; var prevItem = from q in httpTemplates where q(null).ToString() == Template(null).ToString() select q; if (!prevItem.Any()){//Add Template} – CodingSlayer Mar 10 '14 at 18:17
  • @imAbhi thanks, just what I needed, looks like a 1 for-loop of bundles with item.ToString so I would think it should be fast enough – Kunukn Apr 13 '15 at 12:52
42

I faced the same problem, but solutions proposed here work good only for adding reference to the resource and are not very suitable for inline JS code. I found a very helpful article and wrapped all my inline JS (and also script tags) in

@using (Html.BeginScripts())
{
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.18.min.js")" type="text/javascript"></script>
    <script>
    // my inline scripts here
    <\script>
}

And in the _Layout view placed @Html.PageScripts() just before closing 'body' tag. Works like a charm for me.


The helpers themselves:
public static class HtmlHelpers
{
    private class ScriptBlock : IDisposable
    {
        private const string scriptsKey = "scripts";
        public static List<string> pageScripts
        {
            get
            {
                if (HttpContext.Current.Items[scriptsKey] == null)
                    HttpContext.Current.Items[scriptsKey] = new List<string>();
                return (List<string>)HttpContext.Current.Items[scriptsKey];
            }
        }

        WebViewPage webPageBase;

        public ScriptBlock(WebViewPage webPageBase)
        {
            this.webPageBase = webPageBase;
            this.webPageBase.OutputStack.Push(new StringWriter());
        }

        public void Dispose()
        {
            pageScripts.Add(((StringWriter)this.webPageBase.OutputStack.Pop()).ToString());
        }
    }

    public static IDisposable BeginScripts(this HtmlHelper helper)
    {
        return new ScriptBlock((WebViewPage)helper.ViewDataContainer);
    }

    public static MvcHtmlString PageScripts(this HtmlHelper helper)
    {
        return MvcHtmlString.Create(string.Join(Environment.NewLine, ScriptBlock.pageScripts.Select(s => s.ToString())));
    }
}
Vitani
  • 1,594
  • 1
  • 14
  • 28
John.W.Harding
  • 521
  • 4
  • 4
13

I liked the solution posted by @john-w-harding, so I combined it with the answer by @darin-dimitrov to make the following probably overcomplicated solution that lets you delay rendering any html (scripts too) within a using block.

USAGE

In a repeated partial view, only include the block one time:

@using (Html.Delayed(isOnlyOne: "MYPARTIAL_scripts")) {
    <script>
        someInlineScript();
    </script>
}

In a (repeated?) partial view, include the block for every time the partial is used:

@using (Html.Delayed()) {
    <b>show me multiple times, @Model.Whatever</b>
}

In a (repeated?) partial view, include the block once, and later render it specifically by name one-time:

@using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    <b>show me once by name</b>
    <span>@Model.First().Value</span>
}

To render:

@Html.RenderDelayed(); // the "default" unidentified blocks
@Html.RenderDelayed("one-time", false); // render the specified block by name, and allow us to render it again in a second call
@Html.RenderDelayed("one-time"); // render the specified block by name
@Html.RenderDelayed("one-time"); // since it was "popped" in the last call, won't render anything

CODE

public static class HtmlRenderExtensions {

    /// <summary>
    /// Delegate script/resource/etc injection until the end of the page
    /// <para>@via https://stackoverflow.com/a/14127332/1037948 and http://jadnb.wordpress.com/2011/02/16/rendering-scripts-from-partial-views-at-the-end-in-mvc/ </para>
    /// </summary>
    private class DelayedInjectionBlock : IDisposable {
        /// <summary>
        /// Unique internal storage key
        /// </summary>
        private const string CACHE_KEY = "DCCF8C78-2E36-4567-B0CF-FE052ACCE309"; // "DelayedInjectionBlocks";

        /// <summary>
        /// Internal storage identifier for remembering unique/isOnlyOne items
        /// </summary>
        private const string UNIQUE_IDENTIFIER_KEY = CACHE_KEY;

        /// <summary>
        /// What to use as internal storage identifier if no identifier provided (since we can't use null as key)
        /// </summary>
        private const string EMPTY_IDENTIFIER = "";

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        public static Queue<string> GetQueue(HtmlHelper helper, string identifier = null) {
            return _GetOrSet(helper, new Queue<string>(), identifier ?? EMPTY_IDENTIFIER);
        }

        /// <summary>
        /// Retrieve a context-aware list of cached output delegates from the given helper; uses the helper's context rather than singleton HttpContext.Current.Items
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="defaultValue">the default value to return if the cached item isn't found or isn't the expected type; can also be used to set with an arbitrary value</param>
        /// <param name="identifier">optional unique sub-identifier for a given injection block</param>
        /// <returns>list of delayed-execution callbacks to render internal content</returns>
        private static T _GetOrSet<T>(HtmlHelper helper, T defaultValue, string identifier = EMPTY_IDENTIFIER) where T : class {
            var storage = GetStorage(helper);

            // return the stored item, or set it if it does not exist
            return (T) (storage.ContainsKey(identifier) ? storage[identifier] : (storage[identifier] = defaultValue));
        }

        /// <summary>
        /// Get the storage, but if it doesn't exist or isn't the expected type, then create a new "bucket"
        /// </summary>
        /// <param name="helper"></param>
        /// <returns></returns>
        public static Dictionary<string, object> GetStorage(HtmlHelper helper) {
            var storage = helper.ViewContext.HttpContext.Items[CACHE_KEY] as Dictionary<string, object>;
            if (storage == null) helper.ViewContext.HttpContext.Items[CACHE_KEY] = (storage = new Dictionary<string, object>());
            return storage;
        }


        private readonly HtmlHelper helper;
        private readonly string identifier;
        private readonly string isOnlyOne;

        /// <summary>
        /// Create a new using block from the given helper (used for trapping appropriate context)
        /// </summary>
        /// <param name="helper">the helper from which we use the context</param>
        /// <param name="identifier">optional unique identifier to specify one or many injection blocks</param>
        /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
        public DelayedInjectionBlock(HtmlHelper helper, string identifier = null, string isOnlyOne = null) {
            this.helper = helper;

            // start a new writing context
            ((WebViewPage)this.helper.ViewDataContainer).OutputStack.Push(new StringWriter());

            this.identifier = identifier ?? EMPTY_IDENTIFIER;
            this.isOnlyOne = isOnlyOne;
        }

        /// <summary>
        /// Append the internal content to the context's cached list of output delegates
        /// </summary>
        public void Dispose() {
            // render the internal content of the injection block helper
            // make sure to pop from the stack rather than just render from the Writer
            // so it will remove it from regular rendering
            var content = ((WebViewPage)this.helper.ViewDataContainer).OutputStack;
            var renderedContent = content.Count == 0 ? string.Empty : content.Pop().ToString();

            // if we only want one, remove the existing
            var queue = GetQueue(this.helper, this.identifier);

            // get the index of the existing item from the alternate storage
            var existingIdentifiers = _GetOrSet(this.helper, new Dictionary<string, int>(), UNIQUE_IDENTIFIER_KEY);

            // only save the result if this isn't meant to be unique, or
            // if it's supposed to be unique and we haven't encountered this identifier before
            if( null == this.isOnlyOne || !existingIdentifiers.ContainsKey(this.isOnlyOne) ) {
                // remove the new writing context we created for this block
                // and save the output to the queue for later
                queue.Enqueue(renderedContent);

                // only remember this if supposed to
                if(null != this.isOnlyOne) existingIdentifiers[this.isOnlyOne] = queue.Count; // save the index, so we could remove it directly (if we want to use the last instance of the block rather than the first)
            }
        }
    }


    /// <summary>
    /// <para>Start a delayed-execution block of output -- this will be rendered/printed on the next call to <see cref="RenderDelayed"/>.</para>
    /// <para>
    /// <example>
    /// Print once in "default block" (usually rendered at end via <code>@Html.RenderDelayed()</code>).  Code:
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show at later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Print once (i.e. if within a looped partial), using identified block via <code>@Html.RenderDelayed("one-time")</code>.  Code:
    /// <code>
    /// @using (Html.Delayed("one-time", isOnlyOne: "one-time")) {
    ///     <b>show me once</b>
    ///     <span>@Model.First().Value</span>
    /// }
    /// </code>
    /// </example>
    /// </para>
    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="isOnlyOne">extra identifier used to ensure that this item is only added once; if provided, content should only appear once in the page (i.e. only the first block called for this identifier is used)</param>
    /// <returns>using block to wrap delayed output</returns>
    public static IDisposable Delayed(this HtmlHelper helper, string injectionBlockId = null, string isOnlyOne = null) {
        return new DelayedInjectionBlock(helper, injectionBlockId, isOnlyOne);
    }

    /// <summary>
    /// Render all queued output blocks injected via <see cref="Delayed"/>.
    /// <para>
    /// <example>
    /// Print all delayed blocks using default identifier (i.e. not provided)
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>show me later</b>
    ///     <span>@Model.Name</span>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @using (Html.Delayed()) {
    ///     <b>more for later</b>
    ///     etc
    /// }
    /// </code>
    /// -- then later --
    /// <code>
    /// @Html.RenderDelayed() // will print both delayed blocks
    /// </code>
    /// </example>
    /// </para>
    /// <para>
    /// <example>
    /// Allow multiple repetitions of rendered blocks, using same <code>@Html.Delayed()...</code> as before.  Code:
    /// <code>
    /// @Html.RenderDelayed(removeAfterRendering: false); /* will print */
    /// @Html.RenderDelayed() /* will print again because not removed before */
    /// </code>
    /// </example>
    /// </para>

    /// </summary>
    /// <param name="helper">the helper from which we use the context</param>
    /// <param name="injectionBlockId">optional unique identifier to specify one or many injection blocks</param>
    /// <param name="removeAfterRendering">only render this once</param>
    /// <returns>rendered output content</returns>
    public static MvcHtmlString RenderDelayed(this HtmlHelper helper, string injectionBlockId = null, bool removeAfterRendering = true) {
        var stack = DelayedInjectionBlock.GetQueue(helper, injectionBlockId);

        if( removeAfterRendering ) {
            var sb = new StringBuilder(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId)
#endif
                );
            // .count faster than .any
            while (stack.Count > 0) {
                sb.AppendLine(stack.Dequeue());
            }
            return MvcHtmlString.Create(sb.ToString());
        } 

        return MvcHtmlString.Create(
#if DEBUG
                string.Format("<!-- delayed-block: {0} -->", injectionBlockId) + 
#endif
            string.Join(Environment.NewLine, stack));
    }


}
Community
  • 1
  • 1
drzaus
  • 24,171
  • 16
  • 142
  • 201
  • Weird. I don't remember copying the answer to [this other thread](http://stackoverflow.com/a/18790172/1037948), but I did a slightly better writeup there... – drzaus Nov 20 '15 at 20:25
12

Install the Forloop.HtmlHelpers nuget package - it adds some helpers for managing scripts in partial views and editor templates.

Somewhere in your layout, you need to call

@Html.RenderScripts()

This will be where any script files and script blocks will be outputted in the page so I would recommend putting it after your main scripts in the layout and after a scripts section (if you have one).

If you're using The Web Optimization Framework with bundling, you can use the overload

@Html.RenderScripts(Scripts.Render)

so that this method is used for writing out script files.

Now, anytime you want to add script files or blocks in a view, partial view or template, simply use

@using (Html.BeginScriptContext())
{
  Html.AddScriptFile("~/Scripts/jquery.validate.js");
  Html.AddScriptBlock(
    @<script type="text/javascript">
       $(function() { $('#someField').datepicker(); });
     </script>
  );
}

The helpers ensure that only one script file reference is rendered if added multiple times and it also ensures that script files are rendered out in an expected order i.e.

  1. Layout
  2. Partials and Templates (in the order in which they appear in the view, top to bottom)
Russ Cam
  • 124,184
  • 33
  • 204
  • 266
5

This post really helped me so I thought I would post my implementation of the basic idea. I've introduced a helper function that can return script tags for use in the @Html.Resource function.

I also added a simple static class so that I can use typed variables to identify a JS or CSS resource.

public static class ResourceType
{
    public const string Css = "css";
    public const string Js = "js";
}

public static class HtmlExtensions
{
    public static IHtmlString Resource(this HtmlHelper htmlHelper, Func<object, dynamic> template, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null) ((List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type]).Add(template);
        else htmlHelper.ViewContext.HttpContext.Items[Type] = new List<Func<object, dynamic>>() { template };

        return new HtmlString(String.Empty);
    }

    public static IHtmlString RenderResources(this HtmlHelper htmlHelper, string Type)
    {
        if (htmlHelper.ViewContext.HttpContext.Items[Type] != null)
        {
            List<Func<object, dynamic>> resources = (List<Func<object, dynamic>>)htmlHelper.ViewContext.HttpContext.Items[Type];

            foreach (var resource in resources)
            {
                if (resource != null) htmlHelper.ViewContext.Writer.Write(resource(null));
            }
        }

        return new HtmlString(String.Empty);
    }

    public static Func<object, dynamic> ScriptTag(this HtmlHelper htmlHelper, string url)
    {
        var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
        var script = new TagBuilder("script");
        script.Attributes["type"] = "text/javascript";
        script.Attributes["src"] = urlHelper.Content("~/" + url);
        return x => new HtmlString(script.ToString(TagRenderMode.Normal));
    }
}

And in use

@Html.Resource(Html.ScriptTag("Areas/Admin/js/plugins/wysiwyg/jquery.wysiwyg.js"), ResourceType.Js)

Thanks to @Darin Dimitrov who supplied the answer in my question here.

Community
  • 1
  • 1
Chris
  • 7,996
  • 11
  • 66
  • 98
2

The answer given in Populate a Razor Section From a Partial using the RequireScript HtmlHelper follows the same pattern. It also has the benefit that it checks for and suppresses duplicate references to the same Javascript URL, and it has an explicit priority parameter that can be used to control ordering.

I extended this solution by adding methods for:

// use this for scripts to be placed just before the </body> tag
public static string RequireFooterScript(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredFooterScripts(this HtmlHelper html) { ... }

// use this for CSS links
public static string RequireCSS(this HtmlHelper html, string path, int priority = 1) { ... }
public static HtmlString EmitRequiredCSS(this HtmlHelper html) { ... }

I like Darin's & eth0's solutions though since they use the HelperResult template, which allows for script and CSS blocks, not just links to Javascript and CSS files.

Community
  • 1
  • 1
Martin_W
  • 1,582
  • 1
  • 19
  • 24
1

@Darin Dimitrov and @eth0 answers to use with bundle extention usage :

@Html.Resources(a => new HelperResult(b => b.Write( System.Web.Optimization.Scripts.Render("~/Content/js/formBundle").ToString())), "jsTop")
Erkan
  • 96
  • 5