Pages

Friday, January 23, 2015

Handling JavaScript Dependencies and ASP.Net MVC Framework

In functional or component base programming, tracking dependencies is important.

Methods handling dependency tracking can be handled client side, however, I was using ASP.Net MVC bundling and I wanted server side to handle the JavaScript dependency tracking.

ASP.Net MVC bundling allows extensible points, where I wrote a class with a method that traced a specified directory and included the dependent files into a bundle.

I was using JScript IntelliSense the “Reference” Tag at the top of the JavaScript files, which “if the “path” attribute points to another JS file, any objects or functions defined inside that file-or in a file referenced by that file-will show up in IntelliSense.”

Code below, using the reference tags, I was able to trace dependencies.

I created a class called BundleDependencies and created a reference to this class when registering bundles.

var bundleDependencies = new BundleDependencies();

and calling referencePath method, which returns an array of dependent file names.

This method accepts a relative path off the root to the directory. The files located in the specified directory will be root parent files. The root parent files will not be included in the bundle.

The root parent file is the file that contains the reference. Only reference files will be included in the bundle and not the root parent file. Referenced files that have reference to another file isn't considered a parent file. There can only be one parent file, so all files with a reference will be traversed.

JavaScript file can contain more than one reference:
/// <reference path="Components/DropBox.js" />

/// <reference path="Components/PageTitle.js" />

/// <reference path="Components/RosterList.js" />
and reference a file located in another directory. Must be relative and strictly follow ../ notation:
/// <reference path="../../Application/Model.js" />
Since Visual Studio supports Reference tags, all is need is drag JavaScript file from Solution Explorer and drop on the working JavaScript file.


Bundle Configuration Class

namespace com.hillHigh1980
{
    public class BundleConfig
    {
        public static void RegisterBundles(BundleCollection bundles)
        {
            var bundleDependencies = new BundleDependencies();

            // Script Bundles ================================
            var scriptBundle = new ScriptBundle("~/bundles/Views/Home");
            scriptBundle.Include(bundleDependencies.referencePath("Scripts/Views/Home/Components"));
            scriptBundle.IncludeDirectory("~/Scripts/Views/Home", "*.js", true);
            scriptBundle.Orderer = new NonOrderingBundleOrderer();
            bundles.Add(scriptBundle);

        }
    }
}



BundleDependencies Class

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;

namespace com.hillHigh1980.Models
{
    public class BundleDependencies
    {

        List<string> fileDependencies(string directory, string file, string scriptPath)
        {
            var regularExpression = new Regex(@"^///\s?$", RegexOptions.IgnoreCase);
            Match match;

            List<string> reference;
            string relativePath;
            List<string> directoryParts;
            int countRelativePaths;
            int countFolders;
            var references = new List();
            Func<string,bool> parents = delegate(string parent) { return parent == ".."; };

            var rows = File.ReadAllLines(file);
            foreach (var row in rows)
            {
                match = regularExpression.Match(row);

                if (match.Success)
                {
                    //Parse reference row
                    reference = new List(match.Groups[1].Value.Split('"')[1].Split('/'));
                    countRelativePaths = reference.Count(parents);
                    reference.RemoveRange(0, countRelativePaths);

                    //Parse directory
                    directoryParts = directory.Split('\\').ToList();
                    countFolders = directoryParts.Count() - 1;

                    //Create path where reference is located.
                    directoryParts.RemoveRange(countFolders - countRelativePaths + 1, countRelativePaths);
                    directoryParts.AddRange(reference);

                    //Complete path to reference file.
                    file = string.Join(@"\", directoryParts.ToArray());

                    //Create relative path, which will be added to bundle.
                    relativePath = "~/" + file.Substring(file.IndexOf(scriptPath)).Replace(@"\", "/");

                    //Creating directory path, don't need file.
                    directoryParts.RemoveAt(directoryParts.Count() - 1);
                    directory = string.Join(@"\", directoryParts.ToArray());

                    //Specified refernce may contain additional references.
                    references.AddRange(fileDependencies(directory, file, scriptPath));

                    references.Add(relativePath);
                }
                else
                {
                    //Limit to the first line. All done if not found.
                    break;
                }
            }

            return references;
        }

        //Locates all files within a specified directory.
        //All sub-directories are searched if includeSubDirectories is set true.

        public string[] referencePath(string viewPath, bool includeSubDirectories = false, string scriptPath = "Scripts")
        {
            var references = new List();
            List<string> result;
            string[] directories;

            if (includeSubDirectories)
            {
                directories = Directory.GetDirectories(HttpContext.Current.Server.MapPath(viewPath));
            }
            else
            {
                directories = new[] {HttpContext.Current.Server.MapPath(viewPath)};
            }


            foreach (var directory in directories)
            {
                foreach (var file in Directory.GetFiles(directory))
                {
                    result = fileDependencies(directory, file, scriptPath);
                    references.AddRange(result);
                }
            }

            var responses = references.ToArray();

            return responses;
        }

    }
}
}


NonOrderingBundleOrderer Class


using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Optimization;

namespace com.hillHigh1980.Models
{
    class JSFileTransformer : IBundleTransform
    {
        public void Process(BundleContext context, BundleResponse response)
        {

        }
    }

    class NonOrderingBundleOrderer : IBundleOrderer
    {
        public IEnumerable OrderFiles(BundleContext context, IEnumerable files)
        {
            var count = files.Count();
            var arry = files.ToArray();
            var j = 1;
            BundleFile hold = arry[0];
            BundleFile temp;

            for (int i = 0; i < count; i++)
            {
                if (hold.VirtualFile.Name.IndexOf("_View") == -1)
                {
                    if (j < count)
                    {
                        temp = arry[j];
                        arry[j] = hold;
                        hold = temp;
                        j += 1;
                    }
                }
                else
                {
                    arry[0] = hold;
                    break;
                }
            }

            return arry;
        }
    }
}