Hire a web Developer and Designer to upgrade and boost your online presence with cutting edge Technologies

Friday, November 18, 2011

ASP.NET - How to Combine Multiple JavaScript and CSS Files and Remove Overheads

When developing a large web application especially if it is Ajax enabled, we often ended up with a larger number of JavaScript and Cascading Stylesheet files and it is quite common that more than one JavaScript file is involved for a single functionality. For example, if you are using DataTable widget of Yahoo User Interface you have to add yahoo-dom-event.js, connection-min.js, dragdrop-min.js, json.js, datasource-beta-min.js, datatable-beta-min.js (as per the example). When a browser encounters an external resource file such as Image, CSS, JS it opens a separate connection to download the file which actually add extra overhead both to server and the browser. To minimize it we can manually combine multiple JS and CSS file into a single large file, but this makes development life harder. Instead we can programmatically combine these files which we will see in the next section.
To combine these resource file we can utilize the Generic Handler (.ashx)  file which comes with the Asp.net, we can also put some directive in the web.config file to conditionally combines the resource files depending upon the settings. First, lets see the web.config file.
<appSettings>
  <add key="UseFileSet" value="true"/>
  <add key="VersionNo" value="1.0"/>
  <add key="FileSet_CSS_Style1" value="Css/StyleSheet1.css,Css/StyleSheet2.css,Css/StyleSheet3.css,Css/StyleSheet4.css" />
  <add key="FileSet_JS_Functionality1" value="Scripts/JScript1.js,Scripts/JScript2.js,Scripts/JScript3.js,Scripts/JScript4.js" />
appSettings>
As you can see there three kind of settings, first whether we will use the file set which is specified in the consequent settings, second the version number which we will discuss later and third different file sets. Each fileset contains the fileset name as the key and files location in comma separated value. Now lets see how do we embed this resource files in an aspx page:
<% if (ResourceHandler.UseFileSet)
   {
%>
    <link type="text/css" href="ResourceHandler.ashx?v=<%= ResourceHandler.VersionNo %>&fileSet=FileSet_CSS_Style1&type=text/css" rel="Stylesheet"/>
    <script type="text/javascript" src="ResourceHandler.ashx?v=<%= ResourceHandler.VersionNo %>&fileSet=JS_Functionality1&type=application/x-javascript">script>
<%
   }
   else
   {
%>
    <link href="Css/StyleSheet1.css" rel="stylesheet" type="text/css" />
    <link href="Css/StyleSheet2.css" rel="stylesheet" type="text/css" />
    <link href="Css/StyleSheet3.css" rel="stylesheet" type="text/css" />
    <link href="Css/StyleSheet4.css" rel="stylesheet" type="text/css" />
    <script type="text/javascript" src="Scripts/JScript1.js">script>
    <script type="text/javascript" src="Scripts/JScript2.js">script>
    <script type="text/javascript" src="Scripts/JScript3.js">script>
    <script type="text/javascript" src="Scripts/JScript4.js">script>
<%
   }
%>
As you can see we are using Asp.net syntax to conditionally add these resource file depending upon the settings. When we are in development mode we can turn it off  from the web.config file. But the problem with using generic resource handler is the files never gets cached in the browser. So our generic handler must also inject proper caching headers along with combining files. Now lets see how the generic handler is implemented:
<%@ WebHandler Language="C#" Class="ResourceHandler" %>
using System;
using System.IO;
using System.Web;
using System.Configuration;

public class ResourceHandler : IHttpHandler
{
    public static bool UseFileSet
    {
        get
        {
            return Convert.ToBoolean(ConfigurationManager.AppSettings["UseFileSet"]);
        }
    }

    public static string VersionNo
    {
        get
        {
            return ConfigurationManager.AppSettings["VersionNo"];
        }
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }

    public void ProcessRequest (HttpContext context)
    {
        string fileSet = context.Request.QueryString["fileSet"];

        //Basic Validation
        if (string.IsNullOrEmpty(fileSet))
        {
            return;
        }

        string contetType = context.Request.QueryString["type"];

        if (string.IsNullOrEmpty(contetType))
        {
            return;
        }

        string files = ConfigurationManager.AppSettings["FileSet_" + fileSet];

        if (string.IsNullOrEmpty(files))
        {
            return;
        }

        //Get the list of files specified in the FileSet
        string[] fileNames = files.Split(',');

        if ((fileNames != null) && (fileNames.Length > 0))
        {
            //Write each files in the response
            for (int i = 0; i < fileNames.Length; i++)
            {
                context.Response.Write(File.ReadAllText(context.Server.MapPath(fileNames[i])));
            }

            //Set the content type
            context.Response.ContentType = contetType;

            // Cache the resource for 7 Days
            CacheUtility.Cache(TimeSpan.FromDays(7));
        }
    }
}
As we can see the Handler contains some static property which we used when embedding in the aspx files.  The file merging is done in the ProcessRequest method. In this method we are reading the physical files which is specified in the web.config fileset and writing it in the Asp.net Response object and at the end we are using a helper class CacheUtility to cache the response. Now let see how the this class is implemented:
using System;
using System.Web;
using System.Reflection;

public static class CacheUtility
{
    public static void Cache(TimeSpan duration)
    {
        HttpCachePolicy cache = HttpContext.Current.Response.Cache;

        FieldInfo maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
        maxAgeField.SetValue(cache, duration);

        cache.SetCacheability(HttpCacheability.Public);
        cache.SetExpires(DateTime.Now.Add(duration));
        cache.SetMaxAge(duration);
        cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
    }
}
As you can see we are not only using the regular caching methods of Asp.net, we are also using some reflection code to set the proper cache-control header. You can find the reason why the reflection is used from these two links Client-side caching for script methods access in ASP.NET AJAX and ASP.NET AJAX under the hood secrets. If you want to know more about the cache-control header then read Caching Details. If any of the files get modified then update the version number in web.config, it will ensure that the latest files get downloaded due to the query string.

No comments:

Post a Comment