Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Introducing Dynamic Bundles for ASP.NET MVC

0.00/5 (No votes)
13 Dec 2014 1  
Co-locate HTML, CSS, JavaScript and image files that make up a page or component. Auto generate your MVC bundles to ensure the right files are loaded in the right order.

Introduction

When your site grows in complexity, it makes sense to keep it maintainable by using components, such as partial views, DisplayTemplates and EditorTemplates.

The problem is that while you can organize your HTML this way, your CSS, JavaScript and images are still stored in their own directories - with no clear indication exactly what CSS, etc. is used by which partials. Essentially, the implementation of each partial is spread over up to 4 directories.

That makes it very easy to break a partial when changing a CSS or JavaScript file - because the dependencies between CSS, JavaScript and HTML are not always clear. That also makes it hard to reuse a partial somewhere else.

You can solve this to some extent by using MVC areas, but those are used mainly to create sub-sites.

The solution is to co-locate all assets (CSS, JavaScript and images) that support some HTML with that HTML, in the same directory. Assets that support all pages in a controller can go in the view directory for that controller. And assets that are shared throughout the site can go into a top level directory.

This can lead to lots of small, focused CSS and JavaScript files. That's a good thing, but it makes creating your MVC bundles harder - your pages still have to load the right CSS and JavaScript in the right order. To help with this, Dynamic Bundles generates the bundles for you when the page loads. Caching keeps overhead to a minimum.

Key benefits

Dynamic Bundles is an extension of the Razor view engine and MVC bundles. It greatly improves maintainability and code reuse of ASP.NET MVC sites:

  • Co-locate the HTML, CSS, JavaScript and images that make up a page or component in the same directory, instead of organizing these files in separate directories by type. This clearly exposes dependencies.
  • Auto generate CSS and JavaScript bundles that contain the right files in the right order, based on their file organisation. No need to recompile when files are added or deleted. Caching keeps CPU usage and disk accesses minimal.
  • Provides the same minification and file combining as standard MVC bundles.

File Structure

Lets compare the file structure of a classic MVC site with one that uses Dynamic Bundles.

Classic MVC

  • HTML, CSS, JavaScript and image files organized in separate directories by type.
  • CSS and JavaScript that support different pages and components often put in the same physical files, creating hidden dependencies.
  • Long brittle urls from CSS files to background images.
  • Unclear what CSS, JavaScript and images are required for a given component, making reuse harder.

Dynamic Bundles

  • HTML, CSS, JavaScript and image files that belong together sit in the same directory.
  • The view engine included in Dynamic Bundles lets you put partial views and layout files in their own sub directories.
  • Splitting CSS, JavaScript by component encourages developers to keep these files small and focussed.
  • Short simple image background image urls in your CSS.
  • Co-locating all assets that make up a component makes reuse much easier.

Bundles

Lets compare the way that bundles are created with a classic MVC site with one that uses Dynamic Bundles.

Classic MVC

  • You have to create and maintain bundles yourself.
  • You have to make sure to include the correct files, and in the right order.
  • When CSS and JavaScript files are added or deleted, the site needs to be recompiled.

 

public static void RegisterBundles(BundleCollection bundles)
{
    // Need to create bundles yourself, in code. To make any change,
    // you have to recompile. Must always make sure to include the
    // right files in the right order.

    bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                "~/Scripts/jquery-{version}.js"));

    bundles.Add(new ScriptBundle("~/bundles/shared/js").Include(
                "~/Scripts/SharedCode.js",
                "~/Scripts/VariousCode.js"));

    bundles.Add(new ScriptBundle("~/bundles/pile/js").Include(
                "~/Scripts/PileOfCode.js"));

    bundles.Add(new StyleBundle("~/Content/shared/css").Include(
                "~/Content/Reset.css",
                "~/Content/Site.css"));

    bundles.Add(new StyleBundle("~/Content/account/css").Include(
                "~/Content/Account.css"));
}
                        

 

@Styles.Render("~/Content/shared/css")
@Styles.Render("~/Content/account/css")
...
@RenderBody()
...
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/shared/js")
@Scripts.Render("~/bundles/pile/js")
                        

Dynamic Bundles

  • Bundles are auto generated. No need to create bundles yourself.
  • Ensures only required CSS and JavaScript files are loaded, and in the right order.
  • Optimizes client side caching, by combining files into bundles by area, controller, shared and layout.
  • When CSS and JavaScript files are added or deleted, new bundles are automatically generated without recompilation.

 

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        // No need to register bundles in BundleConfig
    }
}
                        

 

@*Nominate where to load the bundles.
The bundles themselves are automatically generated.*@
@DynamicBundlesTopRender()
...
@RenderBody()
...
@DynamicBundlesBottomRender()
                        

Installation

  1. Install Dynamic Bundles
  2. Add view engine to global.asax
  3. Add layout container
  4. Update Web.config for views
  5. Co-locate assets
  6. Create explicit dependencies

1. Install Dynamic Bundles

Install the DynamicBundles package from NuGet:

Install-Package DynamicBundles

2. Add view engine to global.asax

Update your global.asax.cs or global.asax.vb, to add the DynamicBundles view engine:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        AreaRegistration.RegisterAllAreas();
        WebApiConfig.Register(GlobalConfiguration.Configuration);
        FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
        RouteConfig.RegisterRoutes(RouteTable.Routes);
        BundleConfig.RegisterBundles(BundleTable.Bundles);

        // Add DynamicBundles view engine. This functions the same as the Razor view engine,
        // but can find views sitting in their own directories, such as ~/Views/Home/Index/Index.cshtml
        // Note: this leaves the other view engines in place, so they can still be used.
        ViewEngines.Engines.Add(new DynamicBundles.DynamicBundlesViewEngine()); 
    }

    ...
}
            

3. Add layout container

In classic MVC sites, pages sit within a _Layout.cshtml or _Layout.vbhtml file, which contains shared headers, footers, etc.

The problem when introducing Dynamic Bundles is that you want to separate CSS, JavaScript and pictures that are specific to the layout from those that are shared by the entire site.

To make this separation happen, create a new file _LayoutContainer.cshtml (you'll see the content in a moment). This and the _Layout.cshtml go into their own directory. The result looks like this:

Classic MVC

Dynamic Bundles

Contents of _LayoutContainer.cshtml

<!DOCTYPE html>
<html>
    @*Nominate where to load the bundles. The bundles themselves are automatically generated.*@
    @DynamicBundlesTopRender()

    @RenderBody()

    @DynamicBundlesBottomRender()
</html>
            

Changes to _Layout.cshtml

  1. Set Layout to _LayoutContainer.cshtml, which acts as the overall container of the site.
  2. Remove the doctype and html tags.
  3. Remove all style and script rendering, including rendering of script sections.
@{
    // Add _LayoutContainer as the container for the _Layout.cshtml file itself.
    Layout = "../_LayoutContainer/_LayoutContainer.cshtml";
}

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>Dynamic Bundles for ASP.NET MVC</title>

    @Styles.Render("~/Content/shared/css")
    @Styles.Render("~/Content/account/css")
</head>
<body>
    @RenderBody()

    @Scripts.Render("~/bundles/jquery")
    @Scripts.Render("~/bundles/shared/js")
    @Scripts.Render("~/bundles/pile/js")

    @RenderSection("scripts", required: false)
</body>
</html>
            

4. Update Web.config for views

In addition to the Web.config file in the root directory of your site, each MVC site also has a Web.config file in its Views directory. If your site uses areas, each area has a Views directory as well, with its own Web.config file.

The Web.config files in the Views directories need to be updated to:

  1. Install the Dynamic Bundles page base type. This gets each view to register the assets it needs, so bundles with the right files can be generated.
  2. Allow the web server to serve CSS, JavaScript and image files from the Views directory.
<configuration>
  <system.web.webPages.razor>
    <pages pageBaseType="System.Web.Mvc.WebViewPageDynamicBundles.WebViewPage">
      ...
    </pages>
  </system.web.webPages.razor>

  <system.webServer>
  
    <!--
    The BlockViewHandler blocks all requests. In classic MVC sites, it is used to block all requests for files from
    a Views directory. With Dynamic Bundles where CSS, JavaScript and images files are co-located with the view files,
    we only want to block requests for the view files.
    -->
    
    <handlers>
      <remove name="BlockViewHandler"/> 
      <!-- Replace path="*.cshtml" with path="*.vbhtml" if you use Visual Basic. -->
      <add name="BlockViewHandler" path="*.cshtml" verb="*" preCondition="integratedMode" type="System.Web.HttpNotFoundHandler" />
    </handlers>
  </system.webServer>
</configuration>
            

5. Co-locate assets

Finally co-locate your asset files (CSS, JavaScript, images) with the views where they are used:

  • Move all assets that are shared throughout the site in the _LayoutContainer directory.

  • Move all assets that are shared by all views for a controller into that controller's directory.

  • If there are assets specific to a single view file, create a sub directory for that view file and put all assets (including the view itself) into that sub directory. Be sure to name the sub directory the same as the view file, without the extension.

  • Dynamic Bundles will add both the assets in the sub directory and those in its parent directory(s), less specific assets first. In the example below, when /Product/List is loaded, first the assets in ~/Views/Product and then those in ~/Views/Product/List are added.

    This works for both controller specific views and shared (partial) views. Dynamic Bundles makes sure that assets are only ever added once to a bundle.

6. Create explicit dependencies

You may have dependencies from one directory on another. For example, a JavaScript file assumes that a file in another directory has been loaded.

You can specify these dependencies with .nuspec files. These have the same structure as their NuGet counterparts (definition).

If Dynamic Bundles finds a nuspec file in a directory, it will find the directories specified in that nuspec file and add the assets in those directories to the bundles. If those directories have nuspec files themselves, Dynamic Bundles processes those nuspec files as well, etc. This is a fully recurrent process.

To create a dependency from a directory X to some other directories, include a .nuspec file in directory X that looks like this:

<?xml version="1.0"?>
<package >
  <metadata>
      <dependencies>
        <dependency id="../AccountDetailsAssets" />
        <dependency id="~/Views/Shared/DetailsAssets" />
      </dependencies>
  </metadata>
</package>

Note that:

  • The name of the .nuspec file doesn't matter, as long as it has the extension .nuspec.
  • You can specify root relative paths (starting with ~/) and paths relative to the .nuspec file, but not absolute paths (such as C:\Dev\Views\Accounts).

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here