70

I am wondering what the best practice is for including javascript files inside partial views. Once rendered this will end up as a js include tag in the middle of my page's html. From my point of view this isn't a nice way of doing this. They belong in the head tag and as such should not prevent the browser from rendering the html in one go.

An example: I am using a jquery picturegallery plugin inside a 'PictureGallery' partial view as this partial view will be used on several pages. This plugin only needs to be loaded when this view is used and I don't want to have to need to know which plugins each partial view is using...

Thanks for your answers.

kiamlaluno
  • 24,790
  • 16
  • 70
  • 85
Peter
  • 13,204
  • 14
  • 66
  • 109
  • 20
    The best practice is to actually have javascript includes at the bottom of the html page, to increase the performance of the browser while rendering the page. – Eduardo Scoz May 26 '09 at 21:11
  • 2
    I've never heard of this... Can you give some source to backup this statement? – Peter May 26 '09 at 21:15
  • 2
    I think the actual issue is to be able to change the js files included in the master page, not where they are positioned as such. Sometimes - if a js file is essential to the page before it loads - the top is the correct place to have it. This is an interesting question. – edeverett May 26 '09 at 21:17
  • 2
    I've looked it up and indeed, the yahoo performance team confirms that the most performant way is to include them right before the closing body tag. Thanks for this info. Still, as edeverett pointed out, I want to know some way of adding them to a central place, not throughout the page. – Peter May 26 '09 at 21:20
  • YSlow is a really nice plugin for web performance analysis. Here's the docs for javascript: http://developer.yahoo.com/performance/rules.html#js_bottom – Eduardo Scoz May 26 '09 at 21:24
  • If it's ok, I'd like to add my thoughts on how this would ideally work and how I had it when I worked in a Perl house. Im a designer/front end guy so excuse the vagueness. I had an array of js files on the Stack (roughly equivalent to the ViewData) as various includes were included these files would update the array with the URLs of the js files they needed if they were'nt there already. Then when the equivalent of the master page loaded I looped through this array to print the script tags. – edeverett May 26 '09 at 21:28
  • You should add this as an answer with some code – Peter May 26 '09 at 21:34
  • I don't have the code ;-) HTML, JS and CSS are my thing not .NET and C#. I don't have the knowledge to say what should be stored where as what and I'm also not sure of the order things happen as the page loads in .NET. I know what I want, but in this case not how to get it. If someone can interpret this into best MVC .NET practices I'd be happy. – edeverett May 26 '09 at 22:11
  • It should be simple enough to create an array of scripts in your viewdata that you can loop over and spit out in one central place (read: Site.Master). BUT I would recommend against that because the output of javascript is not of concern to the controller, it is of concern to the view. – Charlino May 27 '09 at 02:28

9 Answers9

28

Seems very similar to this question: Linking JavaScript Libraries in User Controls

I'll repost my answer that that question here.

I would definitely advise against putting them inside partials for exactly the reason you mention. There is a high chance that one view could pull in two partials that both have references to the same js file. You've also got the performance hit of loading js before loading the rest of the html.

I don't know about best practice but I choose to include any common js files inside the masterpage and then define a separate ContentPlaceHolder for some additional js files that are specific to a particular or small number of views.

Here's an example master page - it's pretty self explanatory.

<%@ Master Language="C#" Inherits="System.Web.Mvc.ViewMasterPage" %>
<head runat="server">
    ... BLAH ...
    <asp:ContentPlaceHolder ID="AdditionalHead" runat="server" />
    ... BLAH ...
    <%= Html.CSSBlock("/styles/site.css") %>
    <%= Html.CSSBlock("/styles/ie6.css", 6) %>
    <%= Html.CSSBlock("/styles/ie7.css", 7) %>
    <asp:ContentPlaceHolder ID="AdditionalCSS" runat="server" />
</head>
<body>
    ... BLAH ...
    <%= Html.JSBlock("/scripts/jquery-1.3.2.js", "/scripts/jquery-1.3.2.min.js") %>
    <%= Html.JSBlock("/scripts/global.js", "/scripts/global.min.js") %>
    <asp:ContentPlaceHolder ID="AdditionalJS" runat="server" />
</body>

Html.CSSBlock & Html.JSBlock are obviously my own extensions but again, they are self explanatory in what they do.

Then in say a SignUp.aspx view I would have

<asp:Content ID="signUpContent" ContentPlaceHolderID="AdditionalJS" runat="server">
    <%= Html.JSBlock("/scripts/pages/account.signup.js", "/scripts/pages/account.signup.min.js") %>
</asp:Content>

HTHs, Charles

Ps. Here is a follow up question I asked about minifying and concatenating js files: Concatenate & Minify JS on the fly OR at build time - ASP.NET MVC

EDIT: As requested on my other answer, my implementation of .JSBlock(a, b) as requested

public static MvcHtmlString JSBlock(this HtmlHelper html, string fileName)
{
    return html.JSBlock(fileName, string.Empty);
}

public static MvcHtmlString JSBlock(this HtmlHelper html, string fileName, string releaseFileName)
{
    if (string.IsNullOrEmpty(fileName))
        throw new ArgumentNullException("fileName");

    string jsTag = string.Format("<script type=\"text/javascript\" src=\"{0}\"></script>",
                                 html.MEDebugReleaseString(fileName, releaseFileName));

    return MvcHtmlString.Create(jsTag);
}

And then where the magic happens...

    public static MvcHtmlString MEDebugReleaseString(this HtmlHelper html, string debugString, string releaseString)
    {
        string toReturn = debugString;
#if DEBUG
#else
        if (!string.IsNullOrEmpty(releaseString))
            toReturn = releaseString;
#endif
        return MvcHtmlString.Create(toReturn);
    }
Community
  • 1
  • 1
Charlino
  • 15,532
  • 3
  • 54
  • 72
  • Thanks for the detailed explanation. Last night I was thinking about overriding the base page and adding a Scripts/Css collection property which would then be filled by some htmlhelpers. The page would then spit them out in a contentplaceholder just like yours. Would this work or would you advise against it? – Peter May 27 '09 at 06:45
  • Sorry, I don't follow what you mean. "Overriding the base page" - Do you mean overriding System.Web.Mvc.Controller or System.Web.Mvc.ViewPage or simply creating your own custom viewdata model? "filled by some htmlhelpers" That throws me too... if you're using custom HtmlHelpers (from inside a view) then just use the ContentPlaceHolder method mentioned above. Without more info I would advise you to simply follow the above example as it is... even with more info I'd probably still advise you to do the above. ;-D – Charlino May 27 '09 at 09:06
  • I'll see if I can make a working example by the end of the day and post it here as an answer, if it works, I'd like opinions. I would override the ViewPage and add a property called Scripts which is a collection. Inside a partial view you would then call an htmlhelper to add a script Html.JSBlock("path/to/script.js" new{some options}). This helper adds the script to the Scripts collection of the parent page. In the parent page's view, inside the headplaceholder, it calls another htmlhelper which renders the script tags from the Scripts property. Or is that bad design? – Peter May 27 '09 at 09:40
  • To answer my own comment, yes it is bad design. I didn't know untill just know but html helpers should just render html. They should not do data access – Peter May 27 '09 at 09:48
1

The reason you would put a script at the bottom of the page is to ensure the dom has been loaded before attempting any manipulation. This can also be achieved in something like the $(document).ready(callback); jQuery method.

I share the view of not putting embedded JavaScript in the html. Instead I use an Html helper to create an empty div to use as a server instruction. The helper sig is Html.ServerData(String name, Object data);

I use this ServerData method for instructions from the server to the client. The div tag keeps it clean and the "data-" attribute is valid html5.

For the loadScript instruction I may do something like this in my ascx or aspx:

<%= Html.ServerData("loadScript", new { url: "pathTo.js" }) %>

Or add another helper to do this which looks a little cleaner:

<%= Html.LoadScript("~/path/to.js") %>

The html output would be:

<div name="loadScript" data-server="encoded json string">

Then I have a jQuery method that can find any server data tag: $(containingElement).serverData("loadScript"); // returns a jQuery like array of the decoded json objects.

The client may look something like this:

var script = $(containingelement").serverData("loadScript");
$.getScript(script.url, function () {
    // script has been loaded - can do stuff with it now
});

This technique is great for user controls or scripts that need to be loaded within ajax loaded content. The version I wrote is a little more involved handling caching scripts so they load only once per full page load and contain callbacks and jQuery triggers so you can hook into it when it is ready.

If anyone is interested in the full version of this (from MVC to jQuery extension) I would be happy to show off this technique in more detail. Otherwise - hopes it gives someone a new way of approaching this tricky problem.

jhorback
  • 813
  • 8
  • 12
  • Man I would love to have a drink with you and talk this over :) Your idea seems really nice. It doesn't keep data (like scriptpaths) in codebehind, yet it preserves them in the html waiting to be loaded by jquery. The fact that you've improved the getScript method will only load the same script once and you could even hook in a minifier! The cherry on the cake would be to make the jquery code that loads the scripts, a little bit more transparent so it would render itself ;-) Too bad we can't really use the html5 datasets just yet though, see http://tinyurl.com/2vfeunh – Peter Jun 07 '10 at 08:31
  • Thanks, I agree. I use something like a $.scriptLoaded method which is used on the client as a callback. It also takes an optional array of dependencies to load. The script works the same for stylesheets. The serverData technique can be used for more than just loading scripts - really for any server instruction to the client. I use it for basic plugin initialization. The client method that retrieves the server data removes that element from the dom after retrieved as to keep it clean. – jhorback Jun 08 '10 at 00:49
  • The reason to put them at the bottom of the page is not to ensure the dom has loaded. The reason is because they block parallel downloads as clearly stated in the Yahoo guide. Putting scripts at the bottom of the page will not ensure the dom has properly loaded either. – mattmanser Jul 13 '10 at 14:08
1

Today I've created my own solution which fits the bill perfectly. Whether it is good design or not, is for you all to decide but thought I should share either way!

Below is my HtmlExtensions class which allows you to do this in your masterpage:

<%=Html.RenderJScripts() %>

My HtmlExtensions class:

public static class HtmlExtensions
{
    private const string JSCRIPT_VIEWDATA = "__js";

    #region Javascript Inclusions

    public static void JScript(this HtmlHelper html, string scriptLocation)
    {
        html.JScript(scriptLocation, string.Empty);
    }

    public static void JScript(this HtmlHelper html, string scriptLocationDebug, string scriptLocationRelease)
    {
        if (string.IsNullOrEmpty(scriptLocationDebug))
            throw new ArgumentNullException("fileName");

        string jsTag = "<script type=\"text/javascript\" src=\"{0}\"></script>";

#if DEBUG
        jsTag = string.Format(jsTag, scriptLocationDebug);
#else
        jsTag = string.Format(jsTag, !string.IsNullOrEmpty(scriptLocationRelease) ? scriptLocationRelease : scriptLocationDebug);
#endif

        registerJScript(html, jsTag);
    }

    public static MvcHtmlString RenderJScripts(this HtmlHelper html)
    {
        List<string> jscripts = html.ViewContext.TempData[JSCRIPT_VIEWDATA] as List<string>;
        string result = string.Empty; ;
        if(jscripts != null)
        {
            result = string.Join("\r\n", jscripts);
        }
        return MvcHtmlString.Create(result);
    }

    private static void registerJScript(HtmlHelper html, string jsTag)
    {
        List<string> jscripts = html.ViewContext.TempData[JSCRIPT_VIEWDATA] as List<string>;
        if(jscripts == null) jscripts = new List<string>();

        if(!jscripts.Contains(jsTag))
            jscripts.Add(jsTag);

        html.ViewContext.TempData[JSCRIPT_VIEWDATA] = jscripts;
    }

    #endregion

}

What is going on?
The class above will extend the HtmlHelper with methods to add javascript links to a collection that is being stored by the HtmlHelper.ViewContext.TempData collection. At the end of the masterpage I put the <%=Html.RenderJScripts() %> which will loop the jscripts collection inside the HtmlHelper.ViewContext.TempData and render these to the output.

There is a downside however,.. You have to ensure that you don't render the scripts before you've added them. If you'd want to do this for css links for example, it wouldn't work because you have to place these in the <head> tag of your page and the htmlhelper would render the output before you've even added a link.

Peter
  • 13,204
  • 14
  • 66
  • 109
  • 1
    Is tempdata the right place for this? I tempdata is a place to store data which will survive a redirect. I don't think you'll need this data on any subsequent page requests – Martin Booth Feb 20 '11 at 04:37
  • Good point, it's not. Really curious about what would be a good place then. – Peter Feb 21 '11 at 07:21
  • It looks like this is the best option, since the answer with the most votes does not work in partial views (asp:content is not allowed in partial views -> Content controls have to be top-level controls in a content page or a nested master page that references a master page.) Still I'd rather have them in the header, but we would need a pre-registering mechanism like the horrific ajax script manager for that. – Louis Somers Jun 09 '12 at 11:55
0

hejdig.

Playing around with Aspnetmvc3 I decided, for now, to just include my javascript file in the partial

<script src="@Url.Content("~/Scripts/User/List.js")" type="text/javascript"></script>

This js file is then specific for my /user/List.schtml file.

/OF

LosManos
  • 6,117
  • 5
  • 44
  • 81
0

So, this is kind of an old question, but I found it while trying to solve a recent issue. I asked a question and answered it here. Here is a copy of that answer. It is similar to the OP's answer, but avoids using TempData.

One solution is to implement an Html helper extension function which will only load a script once. See below. (this can work for CSS and other included files, too).

Implement an extension for the HtmlHelper class and a private backing class as a singleton per HttpContext.

public static class YourHtmlHelperExtensionClass 
{
    private class TagSrcAttrTracker
    {
        private TagSrcAttrTracker() { }

        public Dictionary<string, string> sources { get; } = new Dictionary<string, string>();

        public static TagSrcAttrTrackerInstance {
            get {
                IDictionary items = HttpContext.Current.Items;
                const string instanceName = "YourClassInstanceNameMakeSureItIsUniqueInThisDictionary";

                if(!items.Contains(instanceName)) 
                    items[instanceName] = new TagSrcAttrTracker();

                return items[instanceName] as TagSrcAttrTracker;
            }
        }
    }

    public static MvcHtmlString IncludeScriptOnlyOnce(this HtmlHelper helper, string urlOfScript) 
    {
        if(TagSrcAttrTracker.Instance.sources.ContainsKey(urlOfScript))
            return null;

        TagSrcAttrTracker.Instance.sources[urlOfScript] = urlOfScript;

        TagBuilder script = new TagBuilder("script");
        scriptTag.MergeAttribute("src", urlOfScript);

        return MvcHtmlString.Create(script.ToString());
    }
}

Then, separate the JavaScript and other code into separate files.

Example .js file contents

class MyClass{
    myFunction() {
        constructor(divId){
            this._divId = divId
        }

        doSomething() {
            // do something with a div with id == divId
        }
    }
}

Example .cshtml file contents for partial view

<link rel="stylesheet" type="text/css" href="~/Content/CustomCSS/MyPartialView.css"/>

<div id="@ViewBag.id">
    Some content!  Yay!
</div>

@Html.IncludeScriptOnlyOnce("/Scripts/CustomScripts/MyPartialView.js")

Example .cshtml file that consumes the partial view

...
<body>
    <h1>Here is some content!</h1>
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_1"} })
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_2"} })
    @Html.Partial("/Views/MyPartial.cshtml", new ViewDataDictionary() { { "id", "id_3"} })
</body>
<script>
$().ready(
    const id1Functionality = new MyClass("id_1") // forgive the poor naming :-)
    const id2Functionality = new MyClass("id_3")
    const id3Functionality = new MyClass("id_2")

    id1Functionality.doSomething();
    id2Functionality.doSomething();
    id3Functionality.doSomething();
)
</script>

The partial view may have been included more than once and the JavaScript is packaged with the partial view, but the .js file is only included in the page once, hence no complaining by the browser that MyClass was declared more than once.

IronMonkey
  • 41
  • 6
0

This seems like a similar (although not entirely) question. Is it bad practice to return partial views that contain javascript?

Community
  • 1
  • 1
Allen Rice
  • 18,062
  • 13
  • 77
  • 111
  • I've read the entire post, but it doesn't answer my question. Putting them all in one js file will be a performance nightmare as several scripts (or plugins if you will) will be loaded even when they are not used. – Peter May 26 '09 at 21:24
0

My preference will be to create a plugin.master page that inherits from your main Site.master. The idea is that you stuff these plugins into plugin.master and make the 8 or so pages that will use this partial view to inherit from plugin.master.

Pita.O
  • 1,806
  • 1
  • 18
  • 27
  • 1
    I sense a management nightmare. Everytime I decide to remove or add a plugin to a view I need to change the masterpage? – Peter May 26 '09 at 21:28
  • You make object inheritance sound like a terrible thing, then. The naive approach of using only one MasterPage for the entire site is actually an under-leveraging of the power and intent of MasterPages. An averagely complex site commonly has up to 4 Masterpages for good reasons, one of which being the kind of need you currently have. There has to be another reason why that sounds like a problem to you. And BTW, the phrase, "Everytime time I need to ..." obviously underlines your misunderstanding of this suggestion. Try and prototype your site, it's easier to group stuff together that way. – Pita.O May 28 '09 at 17:21
0

The preferred approach is to put scripts at the bottom, however, if you can't avoid that then it's reasonable to put them in the middle.

Browsers usually load the various page elements in parallel, however, while the browser is downloading the Javascript file, it won't download any other page elements in parallel until the Javascript is done downloading. This means that your images and what not will have to wait so it's generally considered better to move scripts at the bottom but if your script is small then I wouldn't worry about it.

aleemb
  • 28,346
  • 17
  • 92
  • 111
-1

The designers of MVC went through a lot of trouble to prevent us from using code-behinds, but by creating a file named <viewname>.aspx.cs and modify the inherits attribute of the aspx page accordingly, it is still possible to get them.

I'd say, the best place to put the include would be in the Page_Load handler in the codebehind (using Page.ClientScript.RegisterClientScriptInclude).

erikkallen
  • 31,744
  • 12
  • 81
  • 116
  • Thanks for the suggestion but I really want to stick to the pattern and not use code-behinds. Surely there must be other ways of accomplishing this? – Peter May 26 '09 at 21:11
  • Why was I downvoted? I think I made it clear that I know that it's breaking the rules, but that I can justify why I did it. Breaking the rules is OK if you know what you're doing. – erikkallen May 27 '09 at 12:42
  • Can you not call `RegisterClientScriptInclude` from the .aspx file? Or does it have to be called during `PageLoad`? – Drew Noakes Aug 22 '10 at 20:11
  • 1
    Sure you can but we are talking about ASP.NET MVC which does not work with Pages but with Controllers. – Peter Dec 02 '10 at 18:23