Tuesday, February 15, 2011

Dynamic management of JavaScript file resources in ASP.NET

JavaScript file resource management in "classic" ASP.NET (in my case, ASP 3.0 and .NET Framework 4.0) can be tricky.  Ideally, JavaScript files are loaded only when needed and often must be loaded in a specific sequence to avoid null reference errors.  Without careful management, JavaScript files can be loaded in the wrong sequence because the load order can be at the mercy of the .NET Framework.

Consider a scenario where there is a MasterPage with a ScriptManager, a Web Form with a Script tag and a ScriptManagerProxy, and finally a UserControl on that page with a ScriptManagerProxy.  The concept is that the MasterPage ScriptManager loads JavaScript resources required by every page (e.g., jQuery), but the WebForm and UserControl load JavaScript resources only when needed.  The concept has merit but in this case the design is flawed, because the .NET Framework loads the JavaScript resources in the wrong order – the JavaScript methods for the UserControl will run only if the load order is jQuery, Utilities, Root, WebPage, and CustomControl.

Sample code web page design - MasterPage with ScriptManager; WebForm based on MasterPage with Script tag and ScriptManagerProxy; UserControl on WebForm with ScriptManagerProxy and button

A good solution is to dynamically load all JavaScript resources from the MasterPage ScriptManager control.  This provides fine-grained control over when and in what sequence the JS resources are loaded.  The above scenario is refactored so that the MasterPage still has the ScriptManager and always loads the jQuery and Root JS resources, and the WebForm and UserControl load their JS resources in code instead of by using ScriptManagerProxy and/or Script tag controls.

Master Page design:

  1. <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site1.master.cs" Inherits="ScriptManagerDemo.Site1" %>
  2. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  3. <html xmlns="http://www.w3.org/1999/xhtml">
  4. <head runat="server">
  5.     <title></title>
  6.     <asp:ContentPlaceHolder ID="head" runat="server">
  7.     </asp:ContentPlaceHolder>
  8. </head>
  9. <body>
  10.     <form id="form1" runat="server">
  11.     <asp:ScriptManager ID="MasterPageScriptManager" runat="server">
  12.         <Scripts>
  13.             <asp:ScriptReference Path="~/Scripts/jquery-1.4.1.min.js" />
  14.             <asp:ScriptReference Path="~/Scripts/Root.js" />
  15.         </Scripts>
  16.     </asp:ScriptManager>
  17.     <div>
  18.         <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
  19.         </asp:ContentPlaceHolder>
  20.     </div>
  21.     </form>
  22. </body>
  23. </html>

Master Page code-behind:

  1. using System;
  2. using Utilities;
  3.  
  4. namespace ScriptManagerDemo
  5. {
  6.     public partial class Site1 : System.Web.UI.MasterPage
  7.     {
  8.         public ScriptReferenceCollectionAdapter ScriptReferences
  9.         {
  10.             get { return new ScriptReferenceCollectionAdapter(this.MasterPageScriptManager.Scripts); }
  11.         }
  12.     }
  13. }

Web Form design:

  1. <%@ Page Title="" Language="C#" MasterPageFile="~/Site1.Master" AutoEventWireup="true" CodeBehind="Working.aspx.cs" Inherits="ScriptManagerDemo.Working" %>
  2. <%@ MasterType TypeName="ScriptManagerDemo.Site1" %>
  3. <%@ Register src="WebUserControlWorking.ascx" tagname="WebUserControlWorking" tagprefix="uc1" %>
  4. <asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
  5. </asp:Content>
  6. <asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
  7.     <h1>This page is working</h1>
  8.     <uc1:WebUserControlWorking ID="WebUserControlWorking1" runat="server" />
  9. </asp:Content>

Web Form code-behind:

  1. using System;
  2. namespace ScriptManagerDemo
  3. {
  4.     public partial class Working : System.Web.UI.Page
  5.     {
  6.         protected void Page_Load(object sender, EventArgs e)
  7.         {
  8.             this.Master.ScriptReferences.AddScriptAssembly("Utilities", "Utilities.Utilities.js", "jquery-1.4.1.min.js");
  9.             this.Master.ScriptReferences.AddScriptPath("~/Scripts/WebPage.js", "Root.js", true);
  10.             this.Master.ScriptReferences.AddScriptPath("~/Scripts/CustomControl.js", "WebPage.js", true);
  11.         }
  12.     }
  13. }

ScriptReferenceCollectionAdapter class:

  1. using System;
  2. using System.Text;
  3. using System.Collections.ObjectModel;
  4. using System.Web.UI;
  5. using System.Reflection;
  6.  
  7. namespace Utilities
  8. {
  9.     public class ScriptReferenceCollectionAdapter
  10.     {
  11.         private Collection<ScriptReference> ScriptReferences { get; set; }
  12.         private static readonly string _version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
  13.  
  14.         public ScriptReferenceCollectionAdapter(Collection<ScriptReference> scriptReferences)
  15.         {
  16.             ScriptReferences = scriptReferences;
  17.         }
  18.  
  19.         public void AddScriptPath(string path, string afterReferenceName = null, bool forceCacheRefresh = true)
  20.         {
  21.             AddScript(path, null, null, new string[] { afterReferenceName }, forceCacheRefresh);
  22.         }
  23.  
  24.         public void AddScriptPath(string path, string[] afterReferenceNames = null, bool forceCacheRefresh = true)
  25.         {
  26.             AddScript(path, null, null, afterReferenceNames, forceCacheRefresh);
  27.         }
  28.  
  29.         public void AddScriptAssembly(string assembly, string name, string afterReferenceName = null)
  30.         {
  31.             AddScript(null, assembly, name, new string[] { afterReferenceName });
  32.         }
  33.  
  34.         public void AddScriptAssembly(string assembly, string name, string[] afterReferenceNames)
  35.         {
  36.             AddScript(null, assembly, name, afterReferenceNames);
  37.         }
  38.  
  39.         private void AddScript(string path, string assembly, string name, string[] afterReferenceNames, bool forceCacheRefresh = false)
  40.         {
  41.             if (afterReferenceNames == null || afterReferenceNames.Length == 0)
  42.                 ScriptReferences.Add(CreateScriptReference(path, assembly, name, forceCacheRefresh));
  43.             else
  44.             {
  45.                 ScriptReference scriptRef;
  46.                 int si, ai = 0, idx = 0;
  47.                 string refName;
  48.                 do
  49.                 {
  50.                     refName = afterReferenceNames[ai];
  51.                     si = 0;
  52.                     while (si < ScriptReferences.Count && idx == 0)
  53.                     {
  54.                         scriptRef = ScriptReferences[si];
  55.                         if (scriptRef.Name.Contains(refName) || scriptRef.Path.Contains(refName))
  56.                             idx = ScriptReferences.IndexOf(scriptRef) + 1;
  57.                         si++;
  58.                     }
  59.                     ai++;
  60.                 } while (ai < afterReferenceNames.Length && idx == 0);
  61.                 idx = idx == 0 ? ScriptReferences.Count : idx;
  62.                 ScriptReferences.Insert(idx, CreateScriptReference(path, assembly, name, forceCacheRefresh));
  63.             }
  64.         }
  65.  
  66.         private static ScriptReference CreateScriptReference(string path, string assembly, string name, bool forceCacheRefresh)
  67.         {
  68.             if (string.IsNullOrEmpty(path))
  69.                 return new ScriptReference(name, assembly);
  70.             else
  71.             {
  72.                 if (forceCacheRefresh)
  73.                 {
  74.                     path = string.Format("{0}?ver={1}", path, _version);
  75.                 }
  76.                 return new ScriptReference(path);
  77.             }
  78.         }
  79.     }
  80. }

Key Points:

  • Expose an instance of the ScriptReferenceCollectionAdapter class as a public property of the MasterPage (Site1) class.
  • Add a MasterType directive to the WebForm ASPX to gain access to the MasterPage's properties.
  • Load the JavaScript resources in the proper order during the WebForm's Page_Load event.
  • The ScriptReferenceCollectionAdapter provides the necessary methods and logic to load the JS resources in a specific sequence.
  • These code samples can be refactored easily into the MVP design pattern.

A bonus feature to this approach is each JS resource can be loaded with a URL query string to prevent browser caching.  In the code sample above, the query value is set to be the version of the loading assembly, which should change each time the application is deployed.  I got the idea for how to prevent browser caching from this nice blog post Prevent Js and Css Browser Caching Issues with ASP.NET with additional information from this Stack Overflow forum.

Get a code sample of a Visual Studio 2010 .NET 4.0 web application containing all of the above code and additional resources.

Wednesday, February 9, 2011

Javascript Module Pattern and Augmentation

I have been learning about the Module Pattern for JavaScript and have a few notes to share.  Ben Cherry's post on the subject was extremely helpful and my intent here is merely to clarify a few points he made and add the concept of function augmentation.

Consider this code, which demonstrates Global Import, Module Export, and Loose Augmentation per Ben Cherry's post:

  1. /*global jQuery*/
  2. var EXAMPLE = (function (my, $) {
  3.     var _private_string = 'this string is private';
  4.     var _privateFunc;
  5.  
  6.     _privateFunc = function () {
  7.         alert('heads up: ' + _private_string);
  8.     };
  9.  
  10.     my.public_string = 'this string is public';
  11.  
  12.     my.publicFunction = function (input) {
  13.         alert('heads up: ' + my.public_string + input);
  14.     };
  15.  
  16.     return my;
  17. } (EXAMPLE || {}, jQuery));
  18.  
  19. var EXAMPLE = (function (my_root, $) {
  20.     my_root.MyClass = {};
  21.     my_root.MyClass.MySubClassOne = (function () {
  22.         var my = {};
  23.         var _sub_class_one_private = 'very private';
  24.         my.subClassOnePublicFunction = function () {
  25.             return 'public subclass 1';
  26.         };
  27.         return my;
  28.     } ());
  29.     my_root.MyClass.MySubClassTwo = (function () {
  30.         var my = {};
  31.         var _sub_class_two_private = 'also very private';
  32.         my.subClassTwoAugmentation = EXAMPLE.publicFunction;
  33.         EXAMPLE.publicFunction = function (input) {
  34.             my.subClassTwoAugmentation(input);
  35.             alert('EXAMPLE.publicFunction has been augmented');
  36.         };
  37.         return my;
  38.     } ());
  39.     return my_root;
  40. } (EXAMPLE || {}, jQuery));

The first block sets up the EXAMPLE module and the second block uses Loose Augmentation to add additional functions and classes, mimicking the namespacing conventions I am comfortable with from C# development.

Notice the subClassTwoAugmentation method.  This demonstrates how to augment the existing EXAMPLE.publicFunction function with additional code that will run after the original EXAMPLE.publicFunction code has completed.  I needed to learn this trick so that I could chain additional functionality onto an existing event handler in my production code.  I got the idea for this method from Douglas Crockford's JavaScript: The Good Parts in Chapter 4, Augmenting Types, and also from this forum post.

Tuesday, February 8, 2011

Load a Script file from any Location

My organization is developing an ASP.NET application consisting of a parent WAP project with multiple WAP sub-projects.  I ran into a problem loading JavaScript files located in the parent WAP Scripts folder – the sub-project WAPs were not able to load the scripts.

There are two solutions to this problem.  One answer is to use a declarative path in the script tag:

  1. <script type="text/javascript" language="javascript" src="<%=ResolveUrl("~/Scripts/MyScript.js")%>"></script>

A second answer is to use a ScriptManager (or ScriptManagerProxy) control:

  1. <asp:ScriptManagerProxy ID="ScriptManagerProxy1" runat="server">
  2.     <Scripts>
  3.         <asp:ScriptReference Path="~/Scripts/MyScript.js" />
  4.     </Scripts>
  5. </asp:ScriptManagerProxy>

Either solution appears to work for any page in the parent or any sub-project WAP.  However, the ScriptManager solution is a more reliable way to ensure that all scripts are loaded in the correct sequence.

Monday, February 7, 2011

7 Javascript Best Practices I Have Learned

In a previous post I listed three books that have been great resources for learning JavaScript.  Here are seven important things I have learned so far in no particular order:

Global variables can quickly become a major design problem in JavaScript.  As a C# developer, at first glance I would assume this code would result in a global object with two private members:

  1. var MyModule = (function () {
  2.     myPrivateProperty = 1;
  3.     myPrivateFunction = function () {
  4.         alert('private');
  5.     };
  6. } ());

In reality, myPrivateProperty and myPrivateFunction both are global variables.  This code properly declares both variables as private:

  1. var MyModule = (function () {
  2.     var myPrivateProperty = 1;
  3.     var myPrivateFunction = function () {
  4.         alert('private');
  5.     };
  6. } ());

There are style conventions to follow in JavaScript.  The ones I list here come from the JavaScript Patterns book by Stoyan Stefanov.

  • Use consistent indentation (typically 4 spaces) to make your code readable.
  • Put the opening curly bracket on the same line as the previous statement to avoid unexpected behavior from implied semicolons.
  • Capitalize constructors using TitleCase, variables and functions using camelCase (or lower_case for variables to make it easier to distinguish from functions), private variables and functions using an opening underscore (e.g., _camelCase), and use all caps for CONSTANT_VALUES.

Comment your code using an API documentation format.  This will allow for auto-generation of code documentation.  I have adopted the YUIDoc tool conventions.

For loops must be coded carefully for maximum efficiency.  As a C# developer, the following code looked good to me:

  1. function forLoopExample() {
  2.     var item;
  3.     var anArray = document.getElementsByClassName('my_class');
  4.     for (i = 0; i < anArray.length; i++) {
  5.         alert(item = anArray[i]);
  6.     }
  7. }

In reality there are several problems with this design:

  • The i variable is an implied global
  • The for loop queries the live DOM on every pass because it checks the element collection's length repeatedly
  • The use of ++ promotes "excessive trickiness"

Here is a much-improved design:

  1. function forLoopExample() {
  2.     var item, i, max;
  3.     var anArray = document.getElementsByClassName('my_class');
  4.     for (i = 0, max = anArray.length; i < max; i += 1) {
  5.         alert(item = anArray[i]);
  6.     }
  7. }

Eval is evil.  The Javascript eval function is a security risk because it grants too much authority to the evaluated string.  A better approach is to use Douglas Crockford's JSON library to evaluate text with JSON.parse() or by using the jQuery.parseJSON() method.

Minify production JavaScript using a tool like Douglas Crockford's JSMin or an online YUICompressor which removes whitespace, comments, etc. thereby significantly reducing the size of your JavaScript file.

Check your JavaScript code using JSLint which is a code quality tool that checks for many common violations such as implied globals, missing semi-colons, unused variables, unreachable code, and many more.

Good Javascript References

I have been learning Javascript as quickly as I can, focusing in particular on best practices and adaptations for .NET development including AJAX via WebMethods.  I have found these three O'Reilly books to be particularly helpful:

  • JavaScript: The Good Parts by Douglas Crockford – great introduction to the most critical core functionality of JavaScript along with tips on what to adopt and what to avoid.
  • JavaScript Patterns by Stoyan Stefanov – design patterns for JavaScript.  The "Gang of Four" Design Patterns book was paramount to making me a better C# developer and I'm hoping that this book will have the same impact on my JavaScript.
  • JQuery Cookbook by Cody Lindley – a perfect complement to the other two books, giving me real-world solutions to many common scenarios while simultaneously bringing me some familiarity with the jQuery library.

Happy reading!