Inline JavaScript and CSS tags with ASP.NET MVC

Rendering inline JavaScript and CSS tags in document in MVC

With ASP.NET MVC Microsoft introduced bundling of JavaScript and CSS resources. These bundles work pretty well but to me it always looks not so natural to define bundles in BundleConfig.css and I had problems if I want to edit CSS or JavaScript during runtime because they are cached.

One more thing I was struggling with is injecting the script inside the document instead of referencing the script or css from an external file.

I decided to try to do this but more dynamic. First I split the problem in the following requirements:

  1. Inject the file content to a document

    To do this I used TagBuilder class rather than building the tag with simple String.Format, not because of the performance but because of the elegance and clean object approach in the code. Since using of this method need to be easy, I did it as an extension method of HtmlHelper class.
  2. Concatenate files content

    This step is the simplest one. We just need to read the files content and concatenate them by adding Environment.NewLine in between the file contents and keep all this as a single String. The only thing that needs to be taken care of is multi-threading and possible file accessing from multiple places. This can be easily done by explicitly setting the FileShare and FileAccess when initiating FileStream class instance.
  3. Minify concatentaed file content

    Once we have the concatenated content, all we need to do is to minify it. I saw a lot of articles where people tried to do minification withe regular expressions and string replacement, but there are a lot of NuGet packages available at nuget.org which can be used. I used to work with YUICompressor.NET so I decided to write this piece of code using it. It has methods for both CSS and JavaScript minification.
  4. Cache the output

    ASP.NET has some out of the box caching techniques, so not a big deal about this. I decided to use file dependency combined with sliding expiration of 1 hour. I think this is a fair time to release cache if there are no requests.

Since CSS and JavaScript processing is he same except the part where minification method is used, we can put all into one method called with different parameters:

        private static MvcHtmlString GetHtmlTag(HtmlHelper htmlHelper, TagBuilder builder, String[] paths, bool minify)
        {
            var context = HttpContext.Current;
            List<String> physicalPaths = new List<string>();
            String cacheKey = string.Join("-", paths.Select(p => String.Concat(p.ToLower(), "_", minify.ToString())));
            String cachedContent = context.Cache.Get(cacheKey) as String;
            if (cachedContent == null)
            {
                foreach (String path in paths)
                {
                    var physicalPath = htmlHelper.ViewContext.RequestContext.HttpContext.Server.MapPath(path);
                    physicalPaths.Add(physicalPath);

                    if (File.Exists(physicalPath))
                    {
                        using (var fileStream = new FileStream(physicalPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))//No lock on a file
                        {
                            using (var streamReader = new StreamReader(fileStream))
                            {
                                cachedContent = String.Concat(cachedContent, streamReader.ReadToEnd(), System.Environment.NewLine);
                            }
                        }
                    }
                }

                if (minify)
                {
                    switch (builder.TagName.ToLower().Trim())
                    {
                        case "style":
                            cachedContent = new CssCompressor().Compress(cachedContent);
                            break;
                        case "script":
                            cachedContent = new JavaScriptCompressor().Compress(cachedContent);
                            break;
                        default:
                            throw new ArgumentException(String.Format("Unknown resource type {0}", builder.TagName));
                    }
                }
                context.Cache.Insert(cacheKey, cachedContent, new System.Web.Caching.CacheDependency(physicalPaths.ToArray()), DateTime.MaxValue, TimeSpan.FromHours(1));
            }
            builder.InnerHtml = cachedContent;
            return MvcHtmlString.Create(builder.ToString());
        }
    

So now we just need to make a wrappers around this method for CSS and JavaScript. Since we are building different tags, we are going to build it in a wrapper method and pass it to this common method to finish the content building.

Since this will be implemented as an extension method, HtmlHelper instance will be also passed from the wrapper method.

For JavaScript we will do the following extension method:

        public static MvcHtmlString InlineScriptBlock(this HtmlHelper htmlHelper, String[] paths, bool minify = true)
        {
            var builder = new TagBuilder("script");
            builder.Attributes.Add("type", "text/javascript");
            return GetHtmlTag(htmlHelper, builder, paths, minify);
        }

        public static MvcHtmlString InlineScriptBlock(this HtmlHelper htmlHelper, String path, bool minify = true)
        {
            return InlineScriptBlock(htmlHelper, new String[] { path }, minify);
        }
    

I made an additional overload for a single path file to make it easier to use from Razor.

For CSS, slightly modified extension method:

        public static MvcHtmlString InlineStyleBlock(this HtmlHelper htmlHelper, String[] paths, bool minify = true)
        {
            var builder = new TagBuilder("style");
            builder.Attributes.Add("type", "text/css");
            return GetHtmlTag(htmlHelper, builder, paths, minify);
        }

        public static MvcHtmlString InlineStyleBlock(this HtmlHelper htmlHelper, String path, bool minify = true)
        {
            return InlineStyleBlock(htmlHelper, new String[] { path }, minify);
        }
    

Now we have this all ready. All we need is to put this in a static class and reference it in a web.config of Views (Views/Web.config).

Note

Complete class code is available at my GitHub account as a Gist https://gist.github.com/dejanstojanovic/3c954f23141182d745708e4fd2eade03 and for download from the upper right corner of the page

Views/Web.config should have class namespace listed in system.web.webPages.Razor section:

<system.web.webPages.razor>
    <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.3.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
    <pages pageBaseType="System.Web.Mvc.WebViewPage">
      <namespaces>
        <add namespace="System.Web.Mvc" />
        <add namespace="System.Web.Mvc.Ajax" />
        <add namespace="System.Web.Mvc.Html" />
        <add namespace="System.Web.Optimization"/>
        <add namespace="System.Web.Routing" />
        <add namespace="Mvc.Razor.Helpers" />
      </namespaces>
    </pages>
  </system.web.webPages.razor>
    

Now we are ready to use it and Visual Studio will add this extension in intelisense in any Razor view we are editing.

@Html.InlineStyleBlock(new String[] { "~/Content/bootstrap.css", "~/Content/site.css" }, true)

@Html.InlineScriptBlock(new String[] { "~/Scripts/jquery.js", "~/Scripts/main.js" }, true)
    

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article