My previous post had the weakness that only JavaScript and CSS required for viewing the page was served. This potentially results in having to serve lots of small files or serving many compressed and minified files varying only a little.
Here’s a way to always serve all JavaScript and CSS needed by the whole site, no matter which page you happen to visit. All JavaScript and CSS can then be compressed, minified and cached.
First I created an Interface that all my user controls and pages implement:
public interface IWebArtifact { IEnumerable<string> GetRequiredScripts(); IEnumerable<string> GetRequiredStyleSheets(); }
My image gallery would then, for example, require bxSlider:
public override IEnumerable<string> GetRequiredScripts() { return new List<string> { "~/content/js/jquery.bxSlider.js" }; }
This is where it can get a little hairy. On the first request made to the application, I want to call GetRequiredScripts() and GetRequiredStyleSheets() on everything that implements IWebArtifact and put all the URLs into something I can use. I implemented this in global.asax on Application_BeginRequest(). This is what I ended up with:
protected void Application_BeginRequest(Object source, EventArgs e) { FirstRequestInitialization.Initialize(); } static class FirstRequestInitialization { private static bool s_InitializedAlready = false; private static Object s_lock = new Object(); // Initialize only on the first request public static void Initialize() { if (s_InitializedAlready) { return; } lock (s_lock) { if (s_InitializedAlready) { return; } var app = HttpContext.Current.Application; var scripts = new List<string>(); var csss = new List<string>(); app["Scripts"] = scripts; app["CSSs"] = csss; GetAllScriptsAndStylesFromUserControlsAndPagesAndPopulateGlobalScriptandStyleLists(scripts, csss); s_InitializedAlready = true; } } } public static IEnumerable<string> ScriptList { get { return (HttpContext.Current.Application["Scripts"] as List<string>).AsEnumerable(); } } public static IEnumerable<string> StyleList { get { return (HttpContext.Current.Application["CSSs"] as List<string>).AsEnumerable(); } } private static void GetAllScriptsAndStylesFromUserControlsAndPagesAndPopulateGlobalScriptandStyleLists(List<string> scriptList, List<string> cssList) { var controltype = typeof(IWebArtifact); var types = Assembly.GetAssembly(controltype).GetTypes().Where( t => controltype.IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface ); foreach (var control in types) { var obj = Activator.CreateInstance(control) as IWebArtifact; var scripts = obj.GetRequiredScripts() ?? new List<string>(); var csss = obj.GetRequiredStyleSheets() ?? new List<string>(); foreach (var script in scripts) { if (!scriptList.Contains(script)) scriptList.Add(script); } foreach (var css in csss) { if (!cssList.Contains(css)) cssList.Add(css); } } }
Getting these lists is now a breeze. I use them with SquishIt to serve all JavaScript and all CSS as two small cacheable files like this:
private IJavaScriptBundle JavaScriptBundle = Bundle.JavaScript(); private ICssBundle CssBundle = Bundle.Css(); protected void Page_PreRender(object sender, EventArgs e) { JavaScriptBundle.Add("~/content/js/jquery-1.5.1.js"); //"hack" to always get jquery in first foreach (var url in Global.ScriptList.Distinct()) JavaScriptBundle.Add(url); foreach (var url in Global.StyleList.Distinct()) CssBundle.Add(url); Squish(); } private void Squish() { JavaScriptLiteral.Text = JavaScriptBundle.Add("~/content/js/common.js") .WithMinifier(JavaScriptMinifiers.Yui) .RenderOnlyIfOutputFileMissing() .Render("~/content/js/squished_#.js"); CssLiteral.Text = CssBundle.Add("~/content/skin/common/css/common.css") .WithCompressor(CssCompressors.YuiCompressor) .RenderOnlyIfOutputFileMissing() .Render("~/content/skin/common/css/squished_#.css"); }