Welcome to CSharp Labs

Page Specific Script and Style Minified Bundles in Mvc 4

Friday, July 12, 2013

Generally, as a practice I tend to use as few script and CSS files as possible. Occasionally, a page may have specific JavaScript or vastly different styles that would be unreasonable to include in a main bundle. I have created extension methods that register page specific script and style bundles.

How it Works

During application start-up, script and style files located in the asset folders are validated and added into a bundle:

        /// <summary>
        /// Registers script bundles for views.
        /// </summary>
        /// <param name="bundle">The BundleCollection to add script bundles to.</param>
        public static void RegisterViewScriptBundles(this BundleCollection bundle)
        {
            bundle.RegisterViewBundles(".js");
        }

        /// <summary>
        /// Registers style bundles for views.
        /// </summary>
        /// <param name="bundle">The BundleCollection to add style bundles to.</param>
        public static void RegisterViewStyleBundles(this BundleCollection bundle)
        {
            bundle.RegisterViewBundles(".css");
        }

        /// <summary>
        /// Registers script or style bundles for views.
        /// </summary>
        /// <param name="bundle">The BundleCollection to add script or style bundles to.</param>
        /// <param name="extension">The file extension to use to add files.</param>
        /// <exception cref="ArgumentException"></exception>
        private static void RegisterViewBundles(this BundleCollection bundle, string extension)
        {
            //defines a path to the assets folder:
            const string virtualPath = "~/Content/Assets";

            bool java;
            switch (extension.ToLower())
            {
                case ".js": //JavaScript file
                    java = true;
                    break;
                case ".css": //Cascading Style Sheet file
                    java = false;
                    break;
                default:
                    throw new ArgumentException("Invalid extension specified: '" + extension + "'.");
            }

            string path = HostingEnvironment.MapPath(virtualPath); //get file path of asset folder

            if (Directory.Exists(path)) //if asset folder exists
            {
                string viewsDirectory = HostingEnvironment.MapPath("~/Views"); //get views directory
                
                foreach (string viewPath in Directory.GetDirectories(path)) //get all asset controller file paths
                {
                    string controllerName = (new DirectoryInfo(viewPath)).Name; //get the controller name
                    string controllerDirectory = Path.Combine(viewsDirectory, controllerName); //get the controller directory containing views

                    if (Directory.Exists(controllerDirectory)) //if controller directory exists
                    {
                        //validate each JavaScript or CSS file looking for a corresponding view
                        foreach (string filePath in Directory.GetFiles(viewPath, "*" + extension)) //get all java or css files for the view
                        {
                            string viewFileName = Path.GetFileName(filePath); //get view file name
                            string viewName = Path.GetFileNameWithoutExtension(viewFileName); //get view name
                            string viewFilePath = Path.Combine(controllerDirectory, viewName); //get view file path

                            if (File.Exists(viewFilePath + ".cshtml") || File.Exists(viewFilePath + ".aspx")) //if view file exists
                            {
                                string actualVirtualPropPath = virtualPath + "/" + controllerName + "/" + viewFileName; //setup virtual path to asset

                                if (java) //if java, register ScriptBundle
                                    bundle.Add(new ScriptBundle("~/bundles/assets/" + controllerName + "/" + viewName).Include(actualVirtualPropPath));
                                else //otherwise register StyleBundle
                                    bundle.Add(new StyleBundle("~/Content/Assets/" + controllerName + "/" + viewName).Include(actualVirtualPropPath));
                            }
                        }
                    }
                }
            }
        }

To render page specific scripts and styles, the following extension methods attempt to locate a bundle from the controller and view name:

        /// <summary>
        /// Renders any view specific script bundle for the current page.
        /// </summary>
        /// <param name="page">The page to render scripts for.</param>
        /// <returns>A HTML string containing the link tag or tags for the bundle.</returns>
        public static IHtmlString RenderViewScriptBundle(this WebViewPage page)
        {
            return page.RenderViewScriptBundle(false);
        }

        /// <summary>
        /// Renders any view specific script bundle for the current page.
        /// </summary>
        /// <param name="page">The page to render scripts for.</param>
        /// <param name="force_bundle">true to force render bundle; otherwise, false.</param>
        /// <returns>A HTML string containing the link tag or tags for the bundle.</returns>
        public static IHtmlString RenderViewScriptBundle(this WebViewPage page, bool force_bundle)
        {
            //get the view path:
            string viewPath = ((BuildManagerCompiledView)page.ViewContext.View).ViewPath;

            //get the controller:
            string controller = (new DirectoryInfo(Path.GetDirectoryName(viewPath)).Name);

            //get the view name:
            string view = Path.GetFileNameWithoutExtension(viewPath);

            //create the script bundle virtual path:
            string bundleVirtualPath = "~/bundles/assets/" + controller + "/" + view;

            //locate any available bundle:
            string bundle = BundleTable.Bundles.ResolveBundleUrl(bundleVirtualPath, true);

            if (bundle != null) //if bundle exists
                return Scripts.Render(force_bundle ? bundle : bundleVirtualPath);

            return MvcHtmlString.Empty;
        }

        /// <summary>
        /// Renders any view specific style bundle for the current page.
        /// </summary>
        /// <param name="page">The page to render styles for.</param>
        /// <returns>A HTML string containing the link tag or tags for the bundle.</returns>
        public static IHtmlString RenderViewStyleBundle(this WebViewPage page)
        {
            return page.RenderViewStyleBundle(false);
        }

        /// <summary>
        /// Renders any view specific style bundle for the current page.
        /// </summary>
        /// <param name="page">The page to render styles for.</param>
        /// <param name="force_bundle">true to force render bundle; otherwise, false.</param>
        /// <returns>A HTML string containing the link tag or tags for the bundle.</returns>
        public static IHtmlString RenderViewStyleBundle(this WebViewPage page, bool force_bundle)
        {
            //get the view path:
            string viewPath = ((BuildManagerCompiledView)page.ViewContext.View).ViewPath;

            //get the controller:
            string controller = (new DirectoryInfo(Path.GetDirectoryName(viewPath)).Name);

            //get the view name:
            string view = Path.GetFileNameWithoutExtension(viewPath);

            //create the style bundle virtual path:
            string bundleVirtualPath = "~/Content/Assets/" + controller + "/" + view;

            //locate any available bundle:
            string bundle = BundleTable.Bundles.ResolveBundleUrl(bundleVirtualPath, true);

            if (bundle != null) //if bundle exists
                return Styles.Render(force_bundle ? bundle : bundleVirtualPath);

            return MvcHtmlString.Empty;
        }

These extension methods handle registering and rendering style and script bundles.

Using

Page specific scripts or styles should be named as their corresponding view in a controller sub-directory in  Content/Assets. e.g. "~/Content/Assets/Home/Index.js".

In the BundleConfig.RegisterBundles method, call the register extension methods to add page specific bundles:

    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            ...

            //register page scripts:
            bundles.RegisterViewScriptBundles();
            //register page styles:
            bundles.RegisterViewStyleBundles();
        }
    }

In the layout file for a site, include a call to RenderViewStyleBundle in the head section and RenderViewScriptBundle after other scripts or bundles are rendered as shown below:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")

    @* renders page specific style bundle: *@
    @this.RenderViewStyleBundle()
</head>
<body>
    @RenderBody()

    @Scripts.Render("~/bundles/jquery")
    @RenderSection("scripts", required: false)

    @* renders page specific script bundle: *@
    @this.RenderViewScriptBundle()
</body>
</html>
Caveats

Registering script or style files for views located in the Shared view folder must be placed in a corresponding Shared folder in assets.

Source Code

Download BundleCollectionExtensions and WebViewPageExtensions Classes

Comments