SharePoint 2010 Script On Demand–give me my scripts right now!

Hi all! May be you are already familiar with sharepoint 2010 script on demand feature. Recently I was playing with it and want to show some examples and explanations how it works.

There is a server control, that is responsible for rendering scripts on demand - ScriptLink .This control has 4 significant properties: LoadAfterUI, Name, OnDemand and Localizable.

  • LoadAfterUI (if true) means that your script link (or script) will be rendered at the vary end of the <form> tag (when all other html elements already loaded). if this property equals to false, script will be inserted right after <form> tag. Internally, methods RegisterStartupScript and RegisterClientScriptBlock used. The only difference between these two methods is where each one emits the script block. RegisterClientScriptBlock() emits the script block at the beginning of the Web Form (right after the <form runat="server"> tag), while RegisterStartupScript() emits the script block at the end of the Web Form (right before the </form> tag).
  • Name - name of script file (or path to that file – read below). This property uses by javascript as key that uniquely identify loaded script.
  • Localizable – if true, sharepoint try to find your script under “_layouts/1033/”
  • OnDemand – the most interesting property, indicates if we need this script load immediately, or if it should be downloaded on demand

Ok, some examples. Imagine we have script file myscript.js directly inside the layouts folder and we want to load it (OnDemand = false) . Its easy:

<SharePoint:ScriptLink runat="server" ID="sl" Localizable="False" LoadAfterUI="False" Name="myscript" OnDemand="False"></SharePoint:ScriptLink>

This action produces this output:

<script src="/_layouts/myscript.js?rev=7KqI9%2FoL9hClomz1RdzTqg%3D%3D" type="text/javascript"></script>

Simple script inclusion. Note, that sharepoint adds query parameter (rev) to our script. This is one of the advantages of using ScriptLink control – this rev attribute calculated based on your script content (SPUtility.MakeBrowserCacheSafeLayoutsUr method), so when you update your script, rev is changed, and browser picks up new version of your script (without ugly manual ctrl+f5).

Next (OnDemand="True"):

<SharePoint:ScriptLink runat="server" ID="sl" Localizable="False" LoadAfterUI="True" Name="myscript" OnDemand="True"></SharePoint:ScriptLink>

And this produces:

RegisterSod("myscript.js", "\u002f_layouts\u002fmyscript.js?rev=7KqI9\u00252FoL9hClomz1RdzTqg\u00253D\u00253D");

What is RegisterSod? RegisterSod is a function, that simply mark your script as need to be loaded. First parameter is a key (and its must to be unique), the second is the path to script. Path calculated as “/_layouts/” plus Name property of ScriptLink. Here is a code in the method:

function RegisterSod(key, url)
{ULSxSy:;
	key=NormalizeSodKey(key);
	var sod=new Sod(url);
	_v_dictSod[key]=sod;
}

Simply add script to a dictionary and that’s it. Ok, I registered script on demand, but it is not loaded yet, how to load and start using it? You need to request your script using one of the functions – EnsureScriptFunc or LoadSodByKey, or LoadSod. First accepts script key, type and function that will be executed when script is loaded as arguments, second accepts script key and function, and the third sod object. I prefer to use LoadSodByKey:

LoadSodByKey("myscript.js", function () {
        alert("my script loaded, I can start using it right now!");
    });

LoadSodByKey is complicated function. It dynamically adds script tag into the <head> of html and bind script loaded event. What happens in the script load handler? When script loaded handler push your function into the queue using ExecuteOrDelayUntilScriptLoaded method.

What is ExecuteOrDelayUntilScriptLoaded? This is another significant part of script on demand framework. This function accepts two arguments – the first is a function, that must run when the script registered with key, specified in the second argument will be loaded, it must be the same key, as you use in RegisterSod. Internally ExecuteOrDelayUntilScriptLoaded tracks function execution by using g_ExecuteOrWaitJobs dictionary. This dictionary contains key value pairs, where value is an object, that keeps information about this script – if it is loaded, functions queue. It means that  ExecuteOrDelayUntilScriptLoaded checks loaded value, and if specified script has been already loaded, execute function immediately, if not, simply adds it to a waiting list (queue).

Ok, for example I have 2 functions that wait, until “myscript.js” loaded, but how they may to know, when it actually loads? NotifyScriptLoadedAndExecuteWaitingJobs intended for this purpose. If you open any out of the box sharepoint script, you can find at the end of the script file something like this:

if (typeof(NotifyScriptLoadedAndExecuteWaitingJobs)=="function")
{
	NotifyScriptLoadedAndExecuteWaitingJobs("core.js");
}

And this also significant part of script on demand framework. This means that when our script loaded and interpreted by browser, script calls  NotifyScriptLoadedAndExecuteWaitingJobs by itself to execute all javascript functions, that is waiting while this script is loaded. Here is a code:

function NotifyEventAndExecuteWaitingJobs(eventName)
{ULSxSy:;
	if(!g_ExecuteOrWaitJobs)
		return;
	var eventInfo=g_ExecuteOrWaitJobs[eventName];
	if (eventInfo==null || typeof(eventInfo)=="undefined")
	{
		eventInfo=new Object();
		eventInfo.notified=true;
		eventInfo.jobs=new Array();
		g_ExecuteOrWaitJobs[eventName]=eventInfo;
	}
	else
	{
		if (eventInfo.jobs !=null)
		{
			for (var i=0; i < eventInfo.jobs.length; i++)
			{
				var func=eventInfo.jobs[i];
				func();
			}
		}
		eventInfo.notified=true;
		eventInfo.jobs=new Array();
	}
}

May be you can ask a question – why script file by itself should call this function? We can place call to this function in LoadSod(or LoadSodByKey), so when load event fired, we call NotifyScriptLoadedAndExecuteWaitingJobs and that’s it. I don’t know the correct answer why microsoft not doing this. Only two (weak) arguments come to my mind: first of all calling NotifyScriptLoadedAndExecuteWaitingJobs in the downloaded script ensures that script loaded and interpreted by browser without errors, the second thing that in some files NotifyScriptLoadedAndExecuteWaitingJobs calls not at the end, but in the middle of script file! (it means that we can use methods from file, that is not fully loaded). And also, another caveat is that you can’t load javascript file from CDN using script on demand model, because you need modify script code (add call of NotifyScriptLoadedAndExecuteWaitingJobs ). Another caveats that you may have a problem when your script not directly in layouts folder, but in subfolder. LoadSod method internally truncate path to file name only. For example if you are trying to load in this way:

RegisterSod("myproject/myscript.js", "/_layouts/myproject/myscript.js");
    LoadSodByKey("myproject/myscript.js", function () {
             alert("loaded!");
        });

This will not work, because your key will be truncated to “myscript.js” by LoadSodByKey, but you have already registered using “myproject/myscript.js” key for this script. Why microsoft truncate it I also don’t know, and it’s very weird behaviour. And there is even more caveats. If you are using uppercase letters in your script keys, you may have a trouble, because key internally lower-cased and this will case to strange errors.

If you are interested in how sharepoint loads its out of the box scripts (after registering them using RegisterSod method) – consider _spPreFetch() function in init.js. This function is called right after document load event and it responsible for loading all out of the box on demand scripts.

I was trying to provide some explanation about script on demand sharepoint 2010 feature, and now some real examples.

1. First scenario – we have jquery in “layouts” subfolder, we need to download it on demand and using ScriptLink control.

First of all add ScriptLink:

<SharePoint:ScriptLink runat="server" ID="sl" Localizable="False" LoadAfterUI="True" Name="scriptondemand/jquery.min.js" OnDemand="True"></SharePoint:ScriptLink>

This will produce this output:

RegisterSod("scriptondemand/jquery.min.js", "\u002f_layouts\u002fscriptondemand\u002fjquery.min.js?rev=K66SLHOHJ6cDhtQdIif\u00252B9w\u00253D\u00253D");

Note that we registered sod with key “scriptondemand/jquery.min.js”, “_layouts” added automatically to script’s path. Next add this code to masterpage:

ExecuteOrDelayUntilScriptLoaded(function () {
       alert("jQuery loaded!");
    }, "scriptondemand/jquery.min.js");

    function loadjQ() {
        LoadSodByKey("scriptondemand/jquery.min.js", null);
    }
    _spBodyOnLoadFunctionNames.push("loadjQ");

We need to use _spBodyOnLoadFunctionNamesto make sure that RegisterSod executed (this inline script in the body, when body loaded, script 100% executed). Why pass null as second parameter of LoadSodByKey? I explained this fact above -  LoadSodByKey truncate internally key to “jquery.min.js” and when script will be loaded in load event this method insert something like this:

ExecuteOrDelayUntilScriptLoaded(refToYourFunction, "jquery.min.js")

but we haven’t script on demand with “jquery.min.js” key, we have “scriptondemand/jquery.min.js”. That’s why we pass null and call ExecuteOrDelayUntilScriptLoaded with "scriptondemand/jquery.min.js". And the last step – modify jquery file – add this lines at the end:

if (typeof (NotifyScriptLoadedAndExecuteWaitingJobs) != "undefined")
    NotifyScriptLoadedAndExecuteWaitingJobs("scriptondemand/jquery.min.js");

2. Second scenario – we need to load jquery on demand, but without using ScriptLink.

This is the easiest scenario – we need this javascript in the masterpage:

 RegisterSod("jquery.min.js", "/_layouts/scriptondemand/jquery.min.js");
    LoadSodByKey("jquery.min.js", function () {
        $(function () {
            alert("jQuery loaded!");
        });
    });

We registered sod and immediately start downloading it. You must also update code in the jquery file to much key name:

if (typeof (NotifyScriptLoadedAndExecuteWaitingJobs) != "undefined")
    NotifyScriptLoadedAndExecuteWaitingJobs("jquery.min.js");

Note: why we can access to such methods as RegisterSod or LoadSodByKey without waiting when init.js will be loaded (because methods declared in init.js)? Because init.js not on demand script. It always loads synchronously as inline script. Evidence you can find using reflector and digging into ScriptLink control, method RegisterForControl:

 Page page2 = ctrl as Page;
    if ((string.Compare(name, "core.js", StringComparison.OrdinalIgnoreCase) == 0) || (string.Compare(name, "bform.js", StringComparison.OrdinalIgnoreCase) == 0))
    {
        Register(page, "init.js", true, false);
        RegisterOnDemand(page, "SP.UI.Dialog.js", false);
        RegisterOnDemand(page, "SP.js", false);
    }

and in v4.master inclusion:

<SharePoint:ScriptLink language="javascript" name="core.js" OnDemand="true" runat="server"/>

init.js not on demand means that we can access to such method as RegisterSod, ExecuteOrDelayUntilScriptLoaded, LoadSodByKey directly, wihout calling to ExecuteOrDelayUntilScriptLoaded(myfunc, “init.js”). init.js can’t be on demand script, simply because it contains all on demand logic )

3. Third scenario – we need to download jquery from CDN, but we want to use such methods as ExecuteOrDelayUntilScriptLoaded in our code.

This is the most interesting scenario. I described above that we can’t use out of the box on demand model for cdn scripts, cause of specific implementation of code, that executed when script load event fires. We need to write custom dynamic javascript loader. I wrote my own (as simple as possible). Here is a code:

JSLoader = function (pathToScript, successCallback) {
    var script = document.createElement("script");
    script.async = true;
    script.type = "text/javascript";
    script.src = pathToScript;
    var loaded = false;
    var succcessFunction = successCallback;
    this.load = function () {
        script.onload = script.onreadystatechange = function () {
            if ((script.readyState && script.readyState !== "complete" && script.readyState !== "loaded") || loaded) {
                return;
            }
            loaded = true;
            script.onload = script.onreadystatechange = null;
            succcessFunction();
        };
        var s = document.getElementsByTagName("script")[0];
        s.parentNode.insertBefore(script, s);
    };
};

To use it in conjunction with ExecuteOrDelayUntilScriptLoaded you need to call NotifyScriptLoadedAndExecuteWaitingJobs when script load event will be fired:

<script type="text/javascript" src="/_layouts/scriptondemand/jsLoader.js"></script>
<script type="text/javascript">
    var jsloader = new JSLoader("http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js", function () {
        NotifyScriptLoadedAndExecuteWaitingJobs("jquery");
    });
    jsloader.load();
    
    ExecuteOrDelayUntilScriptLoaded(function () {
        $(function () {
            alert("jQuery loaded from cdn!");
        });
    }, "jquery");
</script>

and that’s it. You no need to add ScriptLink, RegisterSod and so on because you download jquery from cdn using custom loader. Of course, I love this approach very much.

Conclusion:

Most of sharepoint scripts (about 90%) load on demand, which means asynchronously and after document load, and this a very powerful and a good solution. We, as developers, can include our custom scripts using ScriptLink control. What is all pros and cons of using it?

  • pros: easy to use, on demand support, and the main big advantage – this control automatically track file hash based on content, when we change content, hash recalculated and browsers gets updated script (not old, forgot about ctrl+f5)
  • cons: all cons related to OnDemand property (if it true). Preferable way to place your scripts directly in layouts (to avoid potential bugs), if you want to place your script into subfolder, additional javascript required and you should know how all this works internally (to avoid potential bugs). Bugs may appear if you are using upper-cased characters in name attribute. You can’t use ScriptLink to download scripts from CDNs on demand.

That’s it, hope this helps and sorry for my English.

Additional reading:

Sample project you can download here (43.97 kb) (uncomment desired lines to make it work).