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.

No comments:

Post a Comment